diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index 6ec04ce0..800b266d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ -# X-ray -X-ray, Penetrate GFWs. The best v2ray-core, with XTLS support. Automatically patch and compile by GitHub Actions, fully compatible configuration. +# Project X + +[Project X](https://github.com/XTLS) originates from XTLS protocol, provides a set of network tools such as [Xray-core](https://github.com/XTLS/Xray-core) and [Xray-flutter](https://github.com/XTLS/Xray-flutter). + +## Installation + +- Linux script + - [Xray-install](https://github.com/XTLS/Xray-install) + +## Usage + +[Xray-examples](https://github.com/XTLS/Xray-examples) / [VLESS-TCP-XTLS-WHATEVER](https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-WHATEVER) + +## License + +[Mozilla Public License Version 2.0](https://github.com/XTLS/Xray-core/main/LICENSE) + +## Credits + +This repo relies on the following third-party projects: + +- Special thanks: + - [v2fly/v2ray-core](https://github.com/v2fly/v2ray-core) +- In production: + - [gorilla/websocket](https://github.com/gorilla/websocket) + - [lucas-clemente/quic-go](https://github.com/lucas-clemente/quic-go) + - [pires/go-proxyproto](https://github.com/pires/go-proxyproto) + - [seiflotfy/cuckoofilter](https://github.com/seiflotfy/cuckoofilter) + - [google/starlark-go](https://github.com/google/starlark-go) +- For testing only: + - [miekg/dns](https://github.com/miekg/dns) + - [h12w/socks](https://github.com/h12w/socks) + +## Compilation + +### Windows + +``` +go build -o xray.exe -trimpath -ldflags "-s -w -buildid=" ./main +``` + +### Linux / macOS + +``` +go build -o xray -trimpath -ldflags "-s -w -buildid=" ./main +``` + +## Telegram + +[Project X](https://t.me/projectXray) + +[Project X Channel](https://t.me/projectXtls) diff --git a/app/app.go b/app/app.go new file mode 100644 index 00000000..decf348a --- /dev/null +++ b/app/app.go @@ -0,0 +1,2 @@ +// Package app contains feature implementations of Xray. The features may be enabled during runtime. +package app diff --git a/app/commander/commander.go b/app/commander/commander.go new file mode 100644 index 00000000..1a2dfd30 --- /dev/null +++ b/app/commander/commander.go @@ -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)) + })) +} diff --git a/app/commander/config.pb.go b/app/commander/config.pb.go new file mode 100644 index 00000000..15940aee --- /dev/null +++ b/app/commander/config.pb.go @@ -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 +} diff --git a/app/commander/config.proto b/app/commander/config.proto new file mode 100644 index 00000000..d80261ab --- /dev/null +++ b/app/commander/config.proto @@ -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 {} diff --git a/app/commander/errors.generated.go b/app/commander/errors.generated.go new file mode 100644 index 00000000..058fb9cd --- /dev/null +++ b/app/commander/errors.generated.go @@ -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{}) +} diff --git a/app/commander/outbound.go b/app/commander/outbound.go new file mode 100644 index 00000000..616a61f8 --- /dev/null +++ b/app/commander/outbound.go @@ -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() +} diff --git a/app/commander/service.go b/app/commander/service.go new file mode 100644 index 00000000..8039ade9 --- /dev/null +++ b/app/commander/service.go @@ -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 + })) +} diff --git a/app/dispatcher/config.pb.go b/app/dispatcher/config.pb.go new file mode 100644 index 00000000..1d994307 --- /dev/null +++ b/app/dispatcher/config.pb.go @@ -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 +} diff --git a/app/dispatcher/config.proto b/app/dispatcher/config.proto new file mode 100644 index 00000000..7ffa96d6 --- /dev/null +++ b/app/dispatcher/config.proto @@ -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; +} diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go new file mode 100644 index 00000000..6ec313e7 --- /dev/null +++ b/app/dispatcher/default.go @@ -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) +} diff --git a/app/dispatcher/dispatcher.go b/app/dispatcher/dispatcher.go new file mode 100644 index 00000000..e8b24b51 --- /dev/null +++ b/app/dispatcher/dispatcher.go @@ -0,0 +1,5 @@ +// +build !confonly + +package dispatcher + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/app/dispatcher/errors.generated.go b/app/dispatcher/errors.generated.go new file mode 100644 index 00000000..79f6094a --- /dev/null +++ b/app/dispatcher/errors.generated.go @@ -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{}) +} diff --git a/app/dispatcher/sniffer.go b/app/dispatcher/sniffer.go new file mode 100644 index 00000000..e7bdda6d --- /dev/null +++ b/app/dispatcher/sniffer.go @@ -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 +} diff --git a/app/dispatcher/stats.go b/app/dispatcher/stats.go new file mode 100644 index 00000000..02bd00ee --- /dev/null +++ b/app/dispatcher/stats.go @@ -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) +} diff --git a/app/dispatcher/stats_test.go b/app/dispatcher/stats_test.go new file mode 100644 index 00000000..e4f2f2ed --- /dev/null +++ b/app/dispatcher/stats_test.go @@ -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()) + } +} diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go new file mode 100644 index 00000000..fda7d91f --- /dev/null +++ b/app/dns/config.pb.go @@ -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 +} diff --git a/app/dns/config.proto b/app/dns/config.proto new file mode 100644 index 00000000..7da491bc --- /dev/null +++ b/app/dns/config.proto @@ -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 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; +} diff --git a/app/dns/dns.go b/app/dns/dns.go new file mode 100644 index 00000000..b64307ab --- /dev/null +++ b/app/dns/dns.go @@ -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 diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go new file mode 100644 index 00000000..c2f1f075 --- /dev/null +++ b/app/dns/dnscommon.go @@ -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 +} diff --git a/app/dns/dnscommon_test.go b/app/dns/dnscommon_test.go new file mode 100644 index 00000000..cd715180 --- /dev/null +++ b/app/dns/dnscommon_test.go @@ -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) + } + }) + } +} diff --git a/app/dns/dohdns.go b/app/dns/dohdns.go new file mode 100644 index 00000000..ddfebb3b --- /dev/null +++ b/app/dns/dohdns.go @@ -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: + } + } +} diff --git a/app/dns/errors.generated.go b/app/dns/errors.generated.go new file mode 100644 index 00000000..ce4c0605 --- /dev/null +++ b/app/dns/errors.generated.go @@ -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{}) +} diff --git a/app/dns/hosts.go b/app/dns/hosts.go new file mode 100644 index 00000000..cd8048f6 --- /dev/null +++ b/app/dns/hosts.go @@ -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) +} diff --git a/app/dns/hosts_test.go b/app/dns/hosts_test.go new file mode 100644 index 00000000..401abdfd --- /dev/null +++ b/app/dns/hosts_test.go @@ -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) + } + } +} diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go new file mode 100644 index 00000000..02cf8f59 --- /dev/null +++ b/app/dns/nameserver.go @@ -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(), + } +} diff --git a/app/dns/nameserver_test.go b/app/dns/nameserver_test.go new file mode 100644 index 00000000..9aa90ae4 --- /dev/null +++ b/app/dns/nameserver_test.go @@ -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") + } +} diff --git a/app/dns/server.go b/app/dns/server.go new file mode 100644 index 00000000..40ff21a3 --- /dev/null +++ b/app/dns/server.go @@ -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)) + })) +} diff --git a/app/dns/server_test.go b/app/dns/server_test.go new file mode 100644 index 00000000..d57a894e --- /dev/null +++ b/app/dns/server_test.go @@ -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.") + } +} diff --git a/app/dns/udpns.go b/app/dns/udpns.go new file mode 100644 index 00000000..ffcd8c04 --- /dev/null +++ b/app/dns/udpns.go @@ -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: + } + } +} diff --git a/app/log/command/command.go b/app/log/command/command.go new file mode 100644 index 00000000..56eb22e4 --- /dev/null +++ b/app/log/command/command.go @@ -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 + })) +} diff --git a/app/log/command/command_test.go b/app/log/command/command_test.go new file mode 100644 index 00000000..6ca17ee0 --- /dev/null +++ b/app/log/command/command_test.go @@ -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{})) +} diff --git a/app/log/command/config.pb.go b/app/log/command/config.pb.go new file mode 100644 index 00000000..d782874e --- /dev/null +++ b/app/log/command/config.pb.go @@ -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 +} diff --git a/app/log/command/config.proto b/app/log/command/config.proto new file mode 100644 index 00000000..1a24080f --- /dev/null +++ b/app/log/command/config.proto @@ -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) {} +} diff --git a/app/log/command/config_grpc.pb.go b/app/log/command/config_grpc.pb.go new file mode 100644 index 00000000..464e950e --- /dev/null +++ b/app/log/command/config_grpc.pb.go @@ -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", +} diff --git a/app/log/command/errors.generated.go b/app/log/command/errors.generated.go new file mode 100644 index 00000000..76b46f51 --- /dev/null +++ b/app/log/command/errors.generated.go @@ -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{}) +} diff --git a/app/log/config.pb.go b/app/log/config.pb.go new file mode 100644 index 00000000..1253288b --- /dev/null +++ b/app/log/config.pb.go @@ -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 +} diff --git a/app/log/config.proto b/app/log/config.proto new file mode 100644 index 00000000..32ac2b88 --- /dev/null +++ b/app/log/config.proto @@ -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; +} diff --git a/app/log/errors.generated.go b/app/log/errors.generated.go new file mode 100644 index 00000000..6163bc84 --- /dev/null +++ b/app/log/errors.generated.go @@ -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{}) +} diff --git a/app/log/log.go b/app/log/log.go new file mode 100644 index 00000000..2260db4d --- /dev/null +++ b/app/log/log.go @@ -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)) + })) +} diff --git a/app/log/log_creator.go b/app/log/log_creator.go new file mode 100644 index 00000000..e62ffdcc --- /dev/null +++ b/app/log/log_creator.go @@ -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 + })) +} diff --git a/app/log/log_test.go b/app/log/log_test.go new file mode 100644 index 00000000..a3e6df2a --- /dev/null +++ b/app/log/log_test.go @@ -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()) +} diff --git a/app/policy/config.go b/app/policy/config.go new file mode 100644 index 00000000..8ead6bd8 --- /dev/null +++ b/app/policy/config.go @@ -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, + }, + } +} diff --git a/app/policy/config.pb.go b/app/policy/config.pb.go new file mode 100644 index 00000000..53986ec0 --- /dev/null +++ b/app/policy/config.pb.go @@ -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 +} diff --git a/app/policy/config.proto b/app/policy/config.proto new file mode 100644 index 00000000..14d5fa57 --- /dev/null +++ b/app/policy/config.proto @@ -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 level = 1; + SystemPolicy system = 2; +} diff --git a/app/policy/errors.generated.go b/app/policy/errors.generated.go new file mode 100644 index 00000000..506b4e7e --- /dev/null +++ b/app/policy/errors.generated.go @@ -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{}) +} diff --git a/app/policy/manager.go b/app/policy/manager.go new file mode 100644 index 00000000..c99bb32e --- /dev/null +++ b/app/policy/manager.go @@ -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)) + })) +} diff --git a/app/policy/manager_test.go b/app/policy/manager_test.go new file mode 100644 index 00000000..56167228 --- /dev/null +++ b/app/policy/manager_test.go @@ -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) + } + } +} diff --git a/app/policy/policy.go b/app/policy/policy.go new file mode 100644 index 00000000..d91e4333 --- /dev/null +++ b/app/policy/policy.go @@ -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 diff --git a/app/proxyman/command/command.go b/app/proxyman/command/command.go new file mode 100644 index 00000000..09aa4c75 --- /dev/null +++ b/app/proxyman/command/command.go @@ -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 + })) +} diff --git a/app/proxyman/command/command.pb.go b/app/proxyman/command/command.pb.go new file mode 100644 index 00000000..02b5f4b6 --- /dev/null +++ b/app/proxyman/command/command.pb.go @@ -0,0 +1,1065 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: app/proxyman/command/command.proto + +package command + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + serial "github.com/xtls/xray-core/v1/common/serial" + core "github.com/xtls/xray-core/v1/core" + 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 AddUserOperation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User *protocol.User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *AddUserOperation) Reset() { + *x = AddUserOperation{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddUserOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddUserOperation) ProtoMessage() {} + +func (x *AddUserOperation) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AddUserOperation.ProtoReflect.Descriptor instead. +func (*AddUserOperation) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{0} +} + +func (x *AddUserOperation) GetUser() *protocol.User { + if x != nil { + return x.User + } + return nil +} + +type RemoveUserOperation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *RemoveUserOperation) Reset() { + *x = RemoveUserOperation{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveUserOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveUserOperation) ProtoMessage() {} + +func (x *RemoveUserOperation) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 RemoveUserOperation.ProtoReflect.Descriptor instead. +func (*RemoveUserOperation) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{1} +} + +func (x *RemoveUserOperation) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type AddInboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Inbound *core.InboundHandlerConfig `protobuf:"bytes,1,opt,name=inbound,proto3" json:"inbound,omitempty"` +} + +func (x *AddInboundRequest) Reset() { + *x = AddInboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddInboundRequest) ProtoMessage() {} + +func (x *AddInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AddInboundRequest.ProtoReflect.Descriptor instead. +func (*AddInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{2} +} + +func (x *AddInboundRequest) GetInbound() *core.InboundHandlerConfig { + if x != nil { + return x.Inbound + } + return nil +} + +type AddInboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddInboundResponse) Reset() { + *x = AddInboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddInboundResponse) ProtoMessage() {} + +func (x *AddInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AddInboundResponse.ProtoReflect.Descriptor instead. +func (*AddInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{3} +} + +type RemoveInboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` +} + +func (x *RemoveInboundRequest) Reset() { + *x = RemoveInboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveInboundRequest) ProtoMessage() {} + +func (x *RemoveInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 RemoveInboundRequest.ProtoReflect.Descriptor instead. +func (*RemoveInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{4} +} + +func (x *RemoveInboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type RemoveInboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RemoveInboundResponse) Reset() { + *x = RemoveInboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveInboundResponse) ProtoMessage() {} + +func (x *RemoveInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 RemoveInboundResponse.ProtoReflect.Descriptor instead. +func (*RemoveInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{5} +} + +type AlterInboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Operation *serial.TypedMessage `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` +} + +func (x *AlterInboundRequest) Reset() { + *x = AlterInboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AlterInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterInboundRequest) ProtoMessage() {} + +func (x *AlterInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AlterInboundRequest.ProtoReflect.Descriptor instead. +func (*AlterInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{6} +} + +func (x *AlterInboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *AlterInboundRequest) GetOperation() *serial.TypedMessage { + if x != nil { + return x.Operation + } + return nil +} + +type AlterInboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AlterInboundResponse) Reset() { + *x = AlterInboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AlterInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterInboundResponse) ProtoMessage() {} + +func (x *AlterInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AlterInboundResponse.ProtoReflect.Descriptor instead. +func (*AlterInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{7} +} + +type AddOutboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Outbound *core.OutboundHandlerConfig `protobuf:"bytes,1,opt,name=outbound,proto3" json:"outbound,omitempty"` +} + +func (x *AddOutboundRequest) Reset() { + *x = AddOutboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOutboundRequest) ProtoMessage() {} + +func (x *AddOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[8] + 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 AddOutboundRequest.ProtoReflect.Descriptor instead. +func (*AddOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{8} +} + +func (x *AddOutboundRequest) GetOutbound() *core.OutboundHandlerConfig { + if x != nil { + return x.Outbound + } + return nil +} + +type AddOutboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddOutboundResponse) Reset() { + *x = AddOutboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOutboundResponse) ProtoMessage() {} + +func (x *AddOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[9] + 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 AddOutboundResponse.ProtoReflect.Descriptor instead. +func (*AddOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{9} +} + +type RemoveOutboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` +} + +func (x *RemoveOutboundRequest) Reset() { + *x = RemoveOutboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveOutboundRequest) ProtoMessage() {} + +func (x *RemoveOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[10] + 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 RemoveOutboundRequest.ProtoReflect.Descriptor instead. +func (*RemoveOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{10} +} + +func (x *RemoveOutboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type RemoveOutboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RemoveOutboundResponse) Reset() { + *x = RemoveOutboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveOutboundResponse) ProtoMessage() {} + +func (x *RemoveOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[11] + 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 RemoveOutboundResponse.ProtoReflect.Descriptor instead. +func (*RemoveOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{11} +} + +type AlterOutboundRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Operation *serial.TypedMessage `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` +} + +func (x *AlterOutboundRequest) Reset() { + *x = AlterOutboundRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AlterOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterOutboundRequest) ProtoMessage() {} + +func (x *AlterOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[12] + 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 AlterOutboundRequest.ProtoReflect.Descriptor instead. +func (*AlterOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{12} +} + +func (x *AlterOutboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *AlterOutboundRequest) GetOperation() *serial.TypedMessage { + if x != nil { + return x.Operation + } + return nil +} + +type AlterOutboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AlterOutboundResponse) Reset() { + *x = AlterOutboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AlterOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterOutboundResponse) ProtoMessage() {} + +func (x *AlterOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[13] + 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 AlterOutboundResponse.ProtoReflect.Descriptor instead. +func (*AlterOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{13} +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_command_command_proto_msgTypes[14] + 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_proxyman_command_command_proto_msgTypes[14] + 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_proxyman_command_command_proto_rawDescGZIP(), []int{14} +} + +var File_app_proxyman_command_command_proto protoreflect.FileDescriptor + +var file_app_proxyman_command_command_proto_rawDesc = []byte{ + 0x0a, 0x22, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2f, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, + 0x1a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x1a, 0x11, + 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x42, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x22, 0x4e, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, + 0x61, 0x67, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x67, 0x0a, 0x13, 0x41, + 0x6c, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x74, 0x61, 0x67, 0x12, 0x3e, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 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, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x16, 0x0a, 0x14, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x52, 0x0a, 0x12, + 0x41, 0x64, 0x64, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x22, 0x15, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x29, 0x0a, 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, + 0x61, 0x67, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x75, 0x74, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x68, 0x0a, 0x14, + 0x41, 0x6c, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x3e, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 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, 0x09, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x17, 0x0a, 0x15, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x4f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xc5, 0x05, 0x0a, 0x0e, 0x48, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6b, 0x0a, 0x0a, + 0x41, 0x64, 0x64, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x2c, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x0d, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x2f, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x71, 0x0a, 0x0c, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, + 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x6c, 0x74, 0x65, + 0x72, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x6c, 0x74, 0x65, + 0x72, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x6e, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x12, 0x2d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x64, + 0x64, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x64, 0x64, + 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x77, 0x0a, 0x0e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x75, 0x74, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x0d, 0x41, + 0x6c, 0x74, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x2f, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x4f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, + 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x4f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x42, 0x70, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x31, 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, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2f, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x19, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, + 0x70, 0x70, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_app_proxyman_command_command_proto_rawDescOnce sync.Once + file_app_proxyman_command_command_proto_rawDescData = file_app_proxyman_command_command_proto_rawDesc +) + +func file_app_proxyman_command_command_proto_rawDescGZIP() []byte { + file_app_proxyman_command_command_proto_rawDescOnce.Do(func() { + file_app_proxyman_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_proxyman_command_command_proto_rawDescData) + }) + return file_app_proxyman_command_command_proto_rawDescData +} + +var file_app_proxyman_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_app_proxyman_command_command_proto_goTypes = []interface{}{ + (*AddUserOperation)(nil), // 0: xray.app.proxyman.command.AddUserOperation + (*RemoveUserOperation)(nil), // 1: xray.app.proxyman.command.RemoveUserOperation + (*AddInboundRequest)(nil), // 2: xray.app.proxyman.command.AddInboundRequest + (*AddInboundResponse)(nil), // 3: xray.app.proxyman.command.AddInboundResponse + (*RemoveInboundRequest)(nil), // 4: xray.app.proxyman.command.RemoveInboundRequest + (*RemoveInboundResponse)(nil), // 5: xray.app.proxyman.command.RemoveInboundResponse + (*AlterInboundRequest)(nil), // 6: xray.app.proxyman.command.AlterInboundRequest + (*AlterInboundResponse)(nil), // 7: xray.app.proxyman.command.AlterInboundResponse + (*AddOutboundRequest)(nil), // 8: xray.app.proxyman.command.AddOutboundRequest + (*AddOutboundResponse)(nil), // 9: xray.app.proxyman.command.AddOutboundResponse + (*RemoveOutboundRequest)(nil), // 10: xray.app.proxyman.command.RemoveOutboundRequest + (*RemoveOutboundResponse)(nil), // 11: xray.app.proxyman.command.RemoveOutboundResponse + (*AlterOutboundRequest)(nil), // 12: xray.app.proxyman.command.AlterOutboundRequest + (*AlterOutboundResponse)(nil), // 13: xray.app.proxyman.command.AlterOutboundResponse + (*Config)(nil), // 14: xray.app.proxyman.command.Config + (*protocol.User)(nil), // 15: xray.common.protocol.User + (*core.InboundHandlerConfig)(nil), // 16: xray.core.InboundHandlerConfig + (*serial.TypedMessage)(nil), // 17: xray.common.serial.TypedMessage + (*core.OutboundHandlerConfig)(nil), // 18: xray.core.OutboundHandlerConfig +} +var file_app_proxyman_command_command_proto_depIdxs = []int32{ + 15, // 0: xray.app.proxyman.command.AddUserOperation.user:type_name -> xray.common.protocol.User + 16, // 1: xray.app.proxyman.command.AddInboundRequest.inbound:type_name -> xray.core.InboundHandlerConfig + 17, // 2: xray.app.proxyman.command.AlterInboundRequest.operation:type_name -> xray.common.serial.TypedMessage + 18, // 3: xray.app.proxyman.command.AddOutboundRequest.outbound:type_name -> xray.core.OutboundHandlerConfig + 17, // 4: xray.app.proxyman.command.AlterOutboundRequest.operation:type_name -> xray.common.serial.TypedMessage + 2, // 5: xray.app.proxyman.command.HandlerService.AddInbound:input_type -> xray.app.proxyman.command.AddInboundRequest + 4, // 6: xray.app.proxyman.command.HandlerService.RemoveInbound:input_type -> xray.app.proxyman.command.RemoveInboundRequest + 6, // 7: xray.app.proxyman.command.HandlerService.AlterInbound:input_type -> xray.app.proxyman.command.AlterInboundRequest + 8, // 8: xray.app.proxyman.command.HandlerService.AddOutbound:input_type -> xray.app.proxyman.command.AddOutboundRequest + 10, // 9: xray.app.proxyman.command.HandlerService.RemoveOutbound:input_type -> xray.app.proxyman.command.RemoveOutboundRequest + 12, // 10: xray.app.proxyman.command.HandlerService.AlterOutbound:input_type -> xray.app.proxyman.command.AlterOutboundRequest + 3, // 11: xray.app.proxyman.command.HandlerService.AddInbound:output_type -> xray.app.proxyman.command.AddInboundResponse + 5, // 12: xray.app.proxyman.command.HandlerService.RemoveInbound:output_type -> xray.app.proxyman.command.RemoveInboundResponse + 7, // 13: xray.app.proxyman.command.HandlerService.AlterInbound:output_type -> xray.app.proxyman.command.AlterInboundResponse + 9, // 14: xray.app.proxyman.command.HandlerService.AddOutbound:output_type -> xray.app.proxyman.command.AddOutboundResponse + 11, // 15: xray.app.proxyman.command.HandlerService.RemoveOutbound:output_type -> xray.app.proxyman.command.RemoveOutboundResponse + 13, // 16: xray.app.proxyman.command.HandlerService.AlterOutbound:output_type -> xray.app.proxyman.command.AlterOutboundResponse + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_app_proxyman_command_command_proto_init() } +func file_app_proxyman_command_command_proto_init() { + if File_app_proxyman_command_command_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_app_proxyman_command_command_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddUserOperation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveUserOperation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddInboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddInboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveInboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveInboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AlterInboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AlterInboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddOutboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddOutboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveOutboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RemoveOutboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AlterOutboundRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AlterOutboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_command_command_proto_msgTypes[14].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_proxyman_command_command_proto_rawDesc, + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_proxyman_command_command_proto_goTypes, + DependencyIndexes: file_app_proxyman_command_command_proto_depIdxs, + MessageInfos: file_app_proxyman_command_command_proto_msgTypes, + }.Build() + File_app_proxyman_command_command_proto = out.File + file_app_proxyman_command_command_proto_rawDesc = nil + file_app_proxyman_command_command_proto_goTypes = nil + file_app_proxyman_command_command_proto_depIdxs = nil +} diff --git a/app/proxyman/command/command.proto b/app/proxyman/command/command.proto new file mode 100644 index 00000000..6be2b123 --- /dev/null +++ b/app/proxyman/command/command.proto @@ -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 {} diff --git a/app/proxyman/command/command_grpc.pb.go b/app/proxyman/command/command_grpc.pb.go new file mode 100644 index 00000000..41fb3e43 --- /dev/null +++ b/app/proxyman/command/command_grpc.pb.go @@ -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", +} diff --git a/app/proxyman/command/doc.go b/app/proxyman/command/doc.go new file mode 100644 index 00000000..f6680e0d --- /dev/null +++ b/app/proxyman/command/doc.go @@ -0,0 +1,3 @@ +package command + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/app/proxyman/command/errors.generated.go b/app/proxyman/command/errors.generated.go new file mode 100644 index 00000000..76b46f51 --- /dev/null +++ b/app/proxyman/command/errors.generated.go @@ -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{}) +} diff --git a/app/proxyman/config.go b/app/proxyman/config.go new file mode 100644 index 00000000..73c00c74 --- /dev/null +++ b/app/proxyman/config.go @@ -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 +} diff --git a/app/proxyman/config.pb.go b/app/proxyman/config.pb.go new file mode 100644 index 00000000..7fa8a767 --- /dev/null +++ b/app/proxyman/config.pb.go @@ -0,0 +1,1049 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: app/proxyman/config.proto + +package proxyman + +import ( + proto "github.com/golang/protobuf/proto" + net "github.com/xtls/xray-core/v1/common/net" + serial "github.com/xtls/xray-core/v1/common/serial" + internet "github.com/xtls/xray-core/v1/transport/internet" + 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 KnownProtocols int32 + +const ( + KnownProtocols_HTTP KnownProtocols = 0 + KnownProtocols_TLS KnownProtocols = 1 +) + +// Enum value maps for KnownProtocols. +var ( + KnownProtocols_name = map[int32]string{ + 0: "HTTP", + 1: "TLS", + } + KnownProtocols_value = map[string]int32{ + "HTTP": 0, + "TLS": 1, + } +) + +func (x KnownProtocols) Enum() *KnownProtocols { + p := new(KnownProtocols) + *p = x + return p +} + +func (x KnownProtocols) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (KnownProtocols) Descriptor() protoreflect.EnumDescriptor { + return file_app_proxyman_config_proto_enumTypes[0].Descriptor() +} + +func (KnownProtocols) Type() protoreflect.EnumType { + return &file_app_proxyman_config_proto_enumTypes[0] +} + +func (x KnownProtocols) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use KnownProtocols.Descriptor instead. +func (KnownProtocols) EnumDescriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{0} +} + +type AllocationStrategy_Type int32 + +const ( + // Always allocate all connection handlers. + AllocationStrategy_Always AllocationStrategy_Type = 0 + // Randomly allocate specific range of handlers. + AllocationStrategy_Random AllocationStrategy_Type = 1 + // External. Not supported yet. + AllocationStrategy_External AllocationStrategy_Type = 2 +) + +// Enum value maps for AllocationStrategy_Type. +var ( + AllocationStrategy_Type_name = map[int32]string{ + 0: "Always", + 1: "Random", + 2: "External", + } + AllocationStrategy_Type_value = map[string]int32{ + "Always": 0, + "Random": 1, + "External": 2, + } +) + +func (x AllocationStrategy_Type) Enum() *AllocationStrategy_Type { + p := new(AllocationStrategy_Type) + *p = x + return p +} + +func (x AllocationStrategy_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AllocationStrategy_Type) Descriptor() protoreflect.EnumDescriptor { + return file_app_proxyman_config_proto_enumTypes[1].Descriptor() +} + +func (AllocationStrategy_Type) Type() protoreflect.EnumType { + return &file_app_proxyman_config_proto_enumTypes[1] +} + +func (x AllocationStrategy_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AllocationStrategy_Type.Descriptor instead. +func (AllocationStrategy_Type) EnumDescriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 0} +} + +type InboundConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *InboundConfig) Reset() { + *x = InboundConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InboundConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundConfig) ProtoMessage() {} + +func (x *InboundConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 InboundConfig.ProtoReflect.Descriptor instead. +func (*InboundConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{0} +} + +type AllocationStrategy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type AllocationStrategy_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.proxyman.AllocationStrategy_Type" json:"type,omitempty"` + // Number of handlers (ports) running in parallel. + // Default value is 3 if unset. + Concurrency *AllocationStrategy_AllocationStrategyConcurrency `protobuf:"bytes,2,opt,name=concurrency,proto3" json:"concurrency,omitempty"` + // Number of minutes before a handler is regenerated. + // Default value is 5 if unset. + Refresh *AllocationStrategy_AllocationStrategyRefresh `protobuf:"bytes,3,opt,name=refresh,proto3" json:"refresh,omitempty"` +} + +func (x *AllocationStrategy) Reset() { + *x = AllocationStrategy{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AllocationStrategy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AllocationStrategy) ProtoMessage() {} + +func (x *AllocationStrategy) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AllocationStrategy.ProtoReflect.Descriptor instead. +func (*AllocationStrategy) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1} +} + +func (x *AllocationStrategy) GetType() AllocationStrategy_Type { + if x != nil { + return x.Type + } + return AllocationStrategy_Always +} + +func (x *AllocationStrategy) GetConcurrency() *AllocationStrategy_AllocationStrategyConcurrency { + if x != nil { + return x.Concurrency + } + return nil +} + +func (x *AllocationStrategy) GetRefresh() *AllocationStrategy_AllocationStrategyRefresh { + if x != nil { + return x.Refresh + } + return nil +} + +type SniffingConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Whether or not to enable content sniffing on an inbound connection. + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Override target destination if sniff'ed protocol is in the given list. + // Supported values are "http", "tls". + DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` +} + +func (x *SniffingConfig) Reset() { + *x = SniffingConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SniffingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SniffingConfig) ProtoMessage() {} + +func (x *SniffingConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 SniffingConfig.ProtoReflect.Descriptor instead. +func (*SniffingConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{2} +} + +func (x *SniffingConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *SniffingConfig) GetDestinationOverride() []string { + if x != nil { + return x.DestinationOverride + } + return nil +} + +type ReceiverConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // PortRange specifies the ports which the Receiver should listen on. + PortRange *net.PortRange `protobuf:"bytes,1,opt,name=port_range,json=portRange,proto3" json:"port_range,omitempty"` + // Listen specifies the IP address that the Receiver should listen on. + Listen *net.IPOrDomain `protobuf:"bytes,2,opt,name=listen,proto3" json:"listen,omitempty"` + AllocationStrategy *AllocationStrategy `protobuf:"bytes,3,opt,name=allocation_strategy,json=allocationStrategy,proto3" json:"allocation_strategy,omitempty"` + StreamSettings *internet.StreamConfig `protobuf:"bytes,4,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` + ReceiveOriginalDestination bool `protobuf:"varint,5,opt,name=receive_original_destination,json=receiveOriginalDestination,proto3" json:"receive_original_destination,omitempty"` + // Override domains for the given protocol. + // Deprecated. Use sniffing_settings. + // + // Deprecated: Do not use. + DomainOverride []KnownProtocols `protobuf:"varint,7,rep,packed,name=domain_override,json=domainOverride,proto3,enum=xray.app.proxyman.KnownProtocols" json:"domain_override,omitempty"` + SniffingSettings *SniffingConfig `protobuf:"bytes,8,opt,name=sniffing_settings,json=sniffingSettings,proto3" json:"sniffing_settings,omitempty"` +} + +func (x *ReceiverConfig) Reset() { + *x = ReceiverConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReceiverConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReceiverConfig) ProtoMessage() {} + +func (x *ReceiverConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 ReceiverConfig.ProtoReflect.Descriptor instead. +func (*ReceiverConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ReceiverConfig) GetPortRange() *net.PortRange { + if x != nil { + return x.PortRange + } + return nil +} + +func (x *ReceiverConfig) GetListen() *net.IPOrDomain { + if x != nil { + return x.Listen + } + return nil +} + +func (x *ReceiverConfig) GetAllocationStrategy() *AllocationStrategy { + if x != nil { + return x.AllocationStrategy + } + return nil +} + +func (x *ReceiverConfig) GetStreamSettings() *internet.StreamConfig { + if x != nil { + return x.StreamSettings + } + return nil +} + +func (x *ReceiverConfig) GetReceiveOriginalDestination() bool { + if x != nil { + return x.ReceiveOriginalDestination + } + return false +} + +// Deprecated: Do not use. +func (x *ReceiverConfig) GetDomainOverride() []KnownProtocols { + if x != nil { + return x.DomainOverride + } + return nil +} + +func (x *ReceiverConfig) GetSniffingSettings() *SniffingConfig { + if x != nil { + return x.SniffingSettings + } + return nil +} + +type InboundHandlerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + ReceiverSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=receiver_settings,json=receiverSettings,proto3" json:"receiver_settings,omitempty"` + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` +} + +func (x *InboundHandlerConfig) Reset() { + *x = InboundHandlerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundHandlerConfig) ProtoMessage() {} + +func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 InboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*InboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{4} +} + +func (x *InboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *InboundHandlerConfig) GetReceiverSettings() *serial.TypedMessage { + if x != nil { + return x.ReceiverSettings + } + return nil +} + +func (x *InboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +type OutboundConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *OutboundConfig) Reset() { + *x = OutboundConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OutboundConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundConfig) ProtoMessage() {} + +func (x *OutboundConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 OutboundConfig.ProtoReflect.Descriptor instead. +func (*OutboundConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{5} +} + +type SenderConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Send traffic through the given IP. Only IP is allowed. + Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"` + StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` + ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"` +} + +func (x *SenderConfig) Reset() { + *x = SenderConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SenderConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SenderConfig) ProtoMessage() {} + +func (x *SenderConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 SenderConfig.ProtoReflect.Descriptor instead. +func (*SenderConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{6} +} + +func (x *SenderConfig) GetVia() *net.IPOrDomain { + if x != nil { + return x.Via + } + return nil +} + +func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig { + if x != nil { + return x.StreamSettings + } + return nil +} + +func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig { + if x != nil { + return x.ProxySettings + } + return nil +} + +func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig { + if x != nil { + return x.MultiplexSettings + } + return nil +} + +type MultiplexingConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Whether or not Mux is enabled. + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Max number of concurrent connections that one Mux connection can handle. + Concurrency uint32 `protobuf:"varint,2,opt,name=concurrency,proto3" json:"concurrency,omitempty"` +} + +func (x *MultiplexingConfig) Reset() { + *x = MultiplexingConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MultiplexingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiplexingConfig) ProtoMessage() {} + +func (x *MultiplexingConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 MultiplexingConfig.ProtoReflect.Descriptor instead. +func (*MultiplexingConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{7} +} + +func (x *MultiplexingConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *MultiplexingConfig) GetConcurrency() uint32 { + if x != nil { + return x.Concurrency + } + return 0 +} + +type AllocationStrategy_AllocationStrategyConcurrency struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *AllocationStrategy_AllocationStrategyConcurrency) Reset() { + *x = AllocationStrategy_AllocationStrategyConcurrency{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AllocationStrategy_AllocationStrategyConcurrency) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AllocationStrategy_AllocationStrategyConcurrency) ProtoMessage() {} + +func (x *AllocationStrategy_AllocationStrategyConcurrency) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[8] + 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 AllocationStrategy_AllocationStrategyConcurrency.ProtoReflect.Descriptor instead. +func (*AllocationStrategy_AllocationStrategyConcurrency) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *AllocationStrategy_AllocationStrategyConcurrency) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +type AllocationStrategy_AllocationStrategyRefresh struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *AllocationStrategy_AllocationStrategyRefresh) Reset() { + *x = AllocationStrategy_AllocationStrategyRefresh{} + if protoimpl.UnsafeEnabled { + mi := &file_app_proxyman_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AllocationStrategy_AllocationStrategyRefresh) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AllocationStrategy_AllocationStrategyRefresh) ProtoMessage() {} + +func (x *AllocationStrategy_AllocationStrategyRefresh) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[9] + 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 AllocationStrategy_AllocationStrategyRefresh.ProtoReflect.Descriptor instead. +func (*AllocationStrategy_AllocationStrategyRefresh) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 1} +} + +func (x *AllocationStrategy_AllocationStrategyRefresh) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +var File_app_proxyman_config_proto protoreflect.FileDescriptor + +var file_app_proxyman_config_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 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, 0x15, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 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, 0x0f, 0x0a, 0x0d, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0xae, 0x03, 0x0a, 0x12, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x3e, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, + 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x65, 0x0a, 0x0b, 0x63, + 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x43, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x63, 0x79, 0x12, 0x59, 0x0a, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x52, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x1a, 0x35, 0x0a, + 0x1d, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, + 0x65, 0x67, 0x79, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x31, 0x0a, 0x19, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, + 0x68, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52, + 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x10, 0x02, 0x22, 0x5d, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, + 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x31, 0x0a, 0x14, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x13, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x22, 0x90, 0x04, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x5f, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 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, + 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x12, 0x56, 0x0a, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x12, 0x61, 0x6c, 0x6c, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, + 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, + 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, + 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x4e, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4b, + 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, + 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, + 0x2e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x10, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xc0, 0x01, 0x0a, 0x14, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 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, 0x4d, 0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x73, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 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, + 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x47, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 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, 0x0d, 0x70, 0x72, 0x6f, + 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb0, 0x02, 0x0a, + 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x0a, + 0x03, 0x76, 0x69, 0x61, 0x18, 0x01, 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, 0x03, 0x76, 0x69, 0x61, 0x12, 0x4e, 0x0a, 0x0f, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4b, 0x0a, 0x0e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x54, 0x0a, 0x12, 0x6d, 0x75, 0x6c, + 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, + 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x6d, 0x75, + 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, + 0x50, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, + 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, + 0x79, 0x2a, 0x23, 0x0a, 0x0e, 0x4b, 0x6e, 0x6f, 0x77, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x73, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x54, 0x4c, 0x53, 0x10, 0x01, 0x42, 0x58, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x50, + 0x01, 0x5a, 0x29, 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, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, + 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_app_proxyman_config_proto_rawDescOnce sync.Once + file_app_proxyman_config_proto_rawDescData = file_app_proxyman_config_proto_rawDesc +) + +func file_app_proxyman_config_proto_rawDescGZIP() []byte { + file_app_proxyman_config_proto_rawDescOnce.Do(func() { + file_app_proxyman_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_proxyman_config_proto_rawDescData) + }) + return file_app_proxyman_config_proto_rawDescData +} + +var file_app_proxyman_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_app_proxyman_config_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_app_proxyman_config_proto_goTypes = []interface{}{ + (KnownProtocols)(0), // 0: xray.app.proxyman.KnownProtocols + (AllocationStrategy_Type)(0), // 1: xray.app.proxyman.AllocationStrategy.Type + (*InboundConfig)(nil), // 2: xray.app.proxyman.InboundConfig + (*AllocationStrategy)(nil), // 3: xray.app.proxyman.AllocationStrategy + (*SniffingConfig)(nil), // 4: xray.app.proxyman.SniffingConfig + (*ReceiverConfig)(nil), // 5: xray.app.proxyman.ReceiverConfig + (*InboundHandlerConfig)(nil), // 6: xray.app.proxyman.InboundHandlerConfig + (*OutboundConfig)(nil), // 7: xray.app.proxyman.OutboundConfig + (*SenderConfig)(nil), // 8: xray.app.proxyman.SenderConfig + (*MultiplexingConfig)(nil), // 9: xray.app.proxyman.MultiplexingConfig + (*AllocationStrategy_AllocationStrategyConcurrency)(nil), // 10: xray.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency + (*AllocationStrategy_AllocationStrategyRefresh)(nil), // 11: xray.app.proxyman.AllocationStrategy.AllocationStrategyRefresh + (*net.PortRange)(nil), // 12: xray.common.net.PortRange + (*net.IPOrDomain)(nil), // 13: xray.common.net.IPOrDomain + (*internet.StreamConfig)(nil), // 14: xray.transport.internet.StreamConfig + (*serial.TypedMessage)(nil), // 15: xray.common.serial.TypedMessage + (*internet.ProxyConfig)(nil), // 16: xray.transport.internet.ProxyConfig +} +var file_app_proxyman_config_proto_depIdxs = []int32{ + 1, // 0: xray.app.proxyman.AllocationStrategy.type:type_name -> xray.app.proxyman.AllocationStrategy.Type + 10, // 1: xray.app.proxyman.AllocationStrategy.concurrency:type_name -> xray.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency + 11, // 2: xray.app.proxyman.AllocationStrategy.refresh:type_name -> xray.app.proxyman.AllocationStrategy.AllocationStrategyRefresh + 12, // 3: xray.app.proxyman.ReceiverConfig.port_range:type_name -> xray.common.net.PortRange + 13, // 4: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain + 3, // 5: xray.app.proxyman.ReceiverConfig.allocation_strategy:type_name -> xray.app.proxyman.AllocationStrategy + 14, // 6: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 0, // 7: xray.app.proxyman.ReceiverConfig.domain_override:type_name -> xray.app.proxyman.KnownProtocols + 4, // 8: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig + 15, // 9: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage + 15, // 10: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 13, // 11: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain + 14, // 12: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 16, // 13: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig + 9, // 14: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name +} + +func init() { file_app_proxyman_config_proto_init() } +func file_app_proxyman_config_proto_init() { + if File_app_proxyman_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_app_proxyman_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InboundConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AllocationStrategy); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SniffingConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReceiverConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InboundHandlerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OutboundConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SenderConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MultiplexingConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AllocationStrategy_AllocationStrategyConcurrency); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_proxyman_config_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AllocationStrategy_AllocationStrategyRefresh); 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_proxyman_config_proto_rawDesc, + NumEnums: 2, + NumMessages: 10, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_proxyman_config_proto_goTypes, + DependencyIndexes: file_app_proxyman_config_proto_depIdxs, + EnumInfos: file_app_proxyman_config_proto_enumTypes, + MessageInfos: file_app_proxyman_config_proto_msgTypes, + }.Build() + File_app_proxyman_config_proto = out.File + file_app_proxyman_config_proto_rawDesc = nil + file_app_proxyman_config_proto_goTypes = nil + file_app_proxyman_config_proto_depIdxs = nil +} diff --git a/app/proxyman/config.proto b/app/proxyman/config.proto new file mode 100644 index 00000000..3d84b575 --- /dev/null +++ b/app/proxyman/config.proto @@ -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; +} diff --git a/app/proxyman/inbound/always.go b/app/proxyman/inbound/always.go new file mode 100644 index 00000000..c2a99646 --- /dev/null +++ b/app/proxyman/inbound/always.go @@ -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 +} diff --git a/app/proxyman/inbound/dynamic.go b/app/proxyman/inbound/dynamic.go new file mode 100644 index 00000000..90772e50 --- /dev/null +++ b/app/proxyman/inbound/dynamic.go @@ -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 +} diff --git a/app/proxyman/inbound/errors.generated.go b/app/proxyman/inbound/errors.generated.go new file mode 100644 index 00000000..f1f4116f --- /dev/null +++ b/app/proxyman/inbound/errors.generated.go @@ -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{}) +} diff --git a/app/proxyman/inbound/inbound.go b/app/proxyman/inbound/inbound.go new file mode 100644 index 00000000..e734c7d4 --- /dev/null +++ b/app/proxyman/inbound/inbound.go @@ -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)) + })) +} diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go new file mode 100644 index 00000000..b59fde8e --- /dev/null +++ b/app/proxyman/inbound/worker.go @@ -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 +} diff --git a/app/proxyman/outbound/errors.generated.go b/app/proxyman/outbound/errors.generated.go new file mode 100644 index 00000000..b6bfdd86 --- /dev/null +++ b/app/proxyman/outbound/errors.generated.go @@ -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{}) +} diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go new file mode 100644 index 00000000..d71c0179 --- /dev/null +++ b/app/proxyman/outbound/handler.go @@ -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 +} diff --git a/app/proxyman/outbound/handler_test.go b/app/proxyman/outbound/handler_test.go new file mode 100644 index 00000000..cdb3047e --- /dev/null +++ b/app/proxyman/outbound/handler_test.go @@ -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") + } +} diff --git a/app/proxyman/outbound/outbound.go b/app/proxyman/outbound/outbound.go new file mode 100644 index 00000000..5a526c43 --- /dev/null +++ b/app/proxyman/outbound/outbound.go @@ -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)) + })) +} diff --git a/app/reverse/bridge.go b/app/reverse/bridge.go new file mode 100644 index 00000000..2f49a865 --- /dev/null +++ b/app/reverse/bridge.go @@ -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 +} diff --git a/app/reverse/config.go b/app/reverse/config.go new file mode 100644 index 00000000..38b3bc83 --- /dev/null +++ b/app/reverse/config.go @@ -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) +} diff --git a/app/reverse/config.pb.go b/app/reverse/config.pb.go new file mode 100644 index 00000000..810c113f --- /dev/null +++ b/app/reverse/config.pb.go @@ -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 +} diff --git a/app/reverse/config.proto b/app/reverse/config.proto new file mode 100644 index 00000000..da5b5aa3 --- /dev/null +++ b/app/reverse/config.proto @@ -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; +} diff --git a/app/reverse/errors.generated.go b/app/reverse/errors.generated.go new file mode 100644 index 00000000..821ec7b0 --- /dev/null +++ b/app/reverse/errors.generated.go @@ -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{}) +} diff --git a/app/reverse/portal.go b/app/reverse/portal.go new file mode 100644 index 00000000..9c4ee190 --- /dev/null +++ b/app/reverse/portal.go @@ -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() +} diff --git a/app/reverse/portal_test.go b/app/reverse/portal_test.go new file mode 100644 index 00000000..7a951f4a --- /dev/null +++ b/app/reverse/portal_test.go @@ -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") + } +} diff --git a/app/reverse/reverse.go b/app/reverse/reverse.go new file mode 100644 index 00000000..c411fcf0 --- /dev/null +++ b/app/reverse/reverse.go @@ -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...) +} diff --git a/app/router/balancing.go b/app/router/balancing.go new file mode 100644 index 00000000..6e84a805 --- /dev/null +++ b/app/router/balancing.go @@ -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 +} diff --git a/app/router/command/command.go b/app/router/command/command.go new file mode 100644 index 00000000..322708f0 --- /dev/null +++ b/app/router/command/command.go @@ -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 + })) +} diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go new file mode 100644 index 00000000..fb3b9479 --- /dev/null +++ b/app/router/command/command.pb.go @@ -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 +} diff --git a/app/router/command/command.proto b/app/router/command/command.proto new file mode 100644 index 00000000..453797f7 --- /dev/null +++ b/app/router/command/command.proto @@ -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 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 {} diff --git a/app/router/command/command_grpc.pb.go b/app/router/command/command_grpc.pb.go new file mode 100644 index 00000000..796212dd --- /dev/null +++ b/app/router/command/command_grpc.pb.go @@ -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", +} diff --git a/app/router/command/command_test.go b/app/router/command/command_test.go new file mode 100644 index 00000000..a609ba32 --- /dev/null +++ b/app/router/command/command_test.go @@ -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) + } + } +} diff --git a/app/router/command/config.go b/app/router/command/config.go new file mode 100644 index 00000000..de0840b6 --- /dev/null +++ b/app/router/command/config.go @@ -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 +} diff --git a/app/router/command/errors.generated.go b/app/router/command/errors.generated.go new file mode 100644 index 00000000..76b46f51 --- /dev/null +++ b/app/router/command/errors.generated.go @@ -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{}) +} diff --git a/app/router/condition.go b/app/router/condition.go new file mode 100644 index 00000000..7b4a8e8d --- /dev/null +++ b/app/router/condition.go @@ -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) +} diff --git a/app/router/condition_geoip.go b/app/router/condition_geoip.go new file mode 100644 index 00000000..005f4cd1 --- /dev/null +++ b/app/router/condition_geoip.go @@ -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 +) diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go new file mode 100644 index 00000000..b5d81ad8 --- /dev/null +++ b/app/router/condition_geoip_test.go @@ -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()) + } +} diff --git a/app/router/condition_test.go b/app/router/condition_test.go new file mode 100644 index 00000000..a6096b83 --- /dev/null +++ b/app/router/condition_test.go @@ -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) + } +} diff --git a/app/router/config.go b/app/router/config.go new file mode 100644 index 00000000..b965b532 --- /dev/null +++ b/app/router/config.go @@ -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 +} diff --git a/app/router/config.pb.go b/app/router/config.pb.go new file mode 100644 index 00000000..e8d569eb --- /dev/null +++ b/app/router/config.pb.go @@ -0,0 +1,1242 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: app/router/config.proto + +package router + +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 + +// Type of domain value. +type Domain_Type int32 + +const ( + // The value is used as is. + Domain_Plain Domain_Type = 0 + // The value is used as a regular expression. + Domain_Regex Domain_Type = 1 + // The value is a root domain. + Domain_Domain Domain_Type = 2 + // The value is a domain. + Domain_Full Domain_Type = 3 +) + +// Enum value maps for Domain_Type. +var ( + Domain_Type_name = map[int32]string{ + 0: "Plain", + 1: "Regex", + 2: "Domain", + 3: "Full", + } + Domain_Type_value = map[string]int32{ + "Plain": 0, + "Regex": 1, + "Domain": 2, + "Full": 3, + } +) + +func (x Domain_Type) Enum() *Domain_Type { + p := new(Domain_Type) + *p = x + return p +} + +func (x Domain_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { + return file_app_router_config_proto_enumTypes[0].Descriptor() +} + +func (Domain_Type) Type() protoreflect.EnumType { + return &file_app_router_config_proto_enumTypes[0] +} + +func (x Domain_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Domain_Type.Descriptor instead. +func (Domain_Type) EnumDescriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Config_DomainStrategy int32 + +const ( + // Use domain as is. + Config_AsIs Config_DomainStrategy = 0 + // Always resolve IP for domains. + Config_UseIp Config_DomainStrategy = 1 + // Resolve to IP if the domain doesn't match any rules. + Config_IpIfNonMatch Config_DomainStrategy = 2 + // Resolve to IP if any rule requires IP matching. + Config_IpOnDemand Config_DomainStrategy = 3 +) + +// Enum value maps for Config_DomainStrategy. +var ( + Config_DomainStrategy_name = map[int32]string{ + 0: "AsIs", + 1: "UseIp", + 2: "IpIfNonMatch", + 3: "IpOnDemand", + } + Config_DomainStrategy_value = map[string]int32{ + "AsIs": 0, + "UseIp": 1, + "IpIfNonMatch": 2, + "IpOnDemand": 3, + } +) + +func (x Config_DomainStrategy) Enum() *Config_DomainStrategy { + p := new(Config_DomainStrategy) + *p = x + return p +} + +func (x Config_DomainStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Config_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_app_router_config_proto_enumTypes[1].Descriptor() +} + +func (Config_DomainStrategy) Type() protoreflect.EnumType { + return &file_app_router_config_proto_enumTypes[1] +} + +func (x Config_DomainStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Config_DomainStrategy.Descriptor instead. +func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{8, 0} +} + +// Domain for routing decision. +type Domain struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Domain matching type. + Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.router.Domain_Type" json:"type,omitempty"` + // Domain value. + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + // Attributes of this domain. May be used for filtering. + Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` +} + +func (x *Domain) Reset() { + *x = Domain{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Domain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain) ProtoMessage() {} + +func (x *Domain) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 Domain.ProtoReflect.Descriptor instead. +func (*Domain) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Domain) GetType() Domain_Type { + if x != nil { + return x.Type + } + return Domain_Plain +} + +func (x *Domain) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Domain) GetAttribute() []*Domain_Attribute { + if x != nil { + return x.Attribute + } + return nil +} + +// IP for routing decision, in CIDR form. +type CIDR struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IP address, should be either 4 or 16 bytes. + Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + // Number of leading ones in the network mask. + Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` +} + +func (x *CIDR) Reset() { + *x = CIDR{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CIDR) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CIDR) ProtoMessage() {} + +func (x *CIDR) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 CIDR.ProtoReflect.Descriptor instead. +func (*CIDR) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{1} +} + +func (x *CIDR) GetIp() []byte { + if x != nil { + return x.Ip + } + return nil +} + +func (x *CIDR) GetPrefix() uint32 { + if x != nil { + return x.Prefix + } + return 0 +} + +type GeoIP struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` +} + +func (x *GeoIP) Reset() { + *x = GeoIP{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoIP) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIP) ProtoMessage() {} + +func (x *GeoIP) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 GeoIP.ProtoReflect.Descriptor instead. +func (*GeoIP) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{2} +} + +func (x *GeoIP) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoIP) GetCidr() []*CIDR { + if x != nil { + return x.Cidr + } + return nil +} + +type GeoIPList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` +} + +func (x *GeoIPList) Reset() { + *x = GeoIPList{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoIPList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIPList) ProtoMessage() {} + +func (x *GeoIPList) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 GeoIPList.ProtoReflect.Descriptor instead. +func (*GeoIPList) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{3} +} + +func (x *GeoIPList) GetEntry() []*GeoIP { + if x != nil { + return x.Entry + } + return nil +} + +type GeoSite struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` +} + +func (x *GeoSite) Reset() { + *x = GeoSite{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoSite) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSite) ProtoMessage() {} + +func (x *GeoSite) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 GeoSite.ProtoReflect.Descriptor instead. +func (*GeoSite) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{4} +} + +func (x *GeoSite) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoSite) GetDomain() []*Domain { + if x != nil { + return x.Domain + } + return nil +} + +type GeoSiteList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` +} + +func (x *GeoSiteList) Reset() { + *x = GeoSiteList{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoSiteList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSiteList) ProtoMessage() {} + +func (x *GeoSiteList) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 GeoSiteList.ProtoReflect.Descriptor instead. +func (*GeoSiteList) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{5} +} + +func (x *GeoSiteList) GetEntry() []*GeoSite { + if x != nil { + return x.Entry + } + return nil +} + +type RoutingRule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to TargetTag: + // *RoutingRule_Tag + // *RoutingRule_BalancingTag + TargetTag isRoutingRule_TargetTag `protobuf_oneof:"target_tag"` + // List of domains for target domain matching. + Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` + // List of CIDRs for target IP address matching. + // Deprecated. Use geoip below. + // + // Deprecated: Do not use. + Cidr []*CIDR `protobuf:"bytes,3,rep,name=cidr,proto3" json:"cidr,omitempty"` + // 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. + Geoip []*GeoIP `protobuf:"bytes,10,rep,name=geoip,proto3" json:"geoip,omitempty"` + // A range of port [from, to]. If the destination port is in this range, this + // rule takes effect. Deprecated. Use port_list. + // + // Deprecated: Do not use. + PortRange *net.PortRange `protobuf:"bytes,4,opt,name=port_range,json=portRange,proto3" json:"port_range,omitempty"` + // List of ports. + PortList *net.PortList `protobuf:"bytes,14,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` + // List of networks. Deprecated. Use networks. + // + // Deprecated: Do not use. + NetworkList *net.NetworkList `protobuf:"bytes,5,opt,name=network_list,json=networkList,proto3" json:"network_list,omitempty"` + // List of networks for matching. + Networks []net.Network `protobuf:"varint,13,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` + // List of CIDRs for source IP address matching. + // + // Deprecated: Do not use. + SourceCidr []*CIDR `protobuf:"bytes,6,rep,name=source_cidr,json=sourceCidr,proto3" json:"source_cidr,omitempty"` + // List of GeoIPs for source IP address matching. If this entry exists, the + // source_cidr above will have no effect. + SourceGeoip []*GeoIP `protobuf:"bytes,11,rep,name=source_geoip,json=sourceGeoip,proto3" json:"source_geoip,omitempty"` + // List of ports for source port matching. + SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"` + UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` + InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` + Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"` + Attributes string `protobuf:"bytes,15,opt,name=attributes,proto3" json:"attributes,omitempty"` +} + +func (x *RoutingRule) Reset() { + *x = RoutingRule{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RoutingRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutingRule) ProtoMessage() {} + +func (x *RoutingRule) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 RoutingRule.ProtoReflect.Descriptor instead. +func (*RoutingRule) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{6} +} + +func (m *RoutingRule) GetTargetTag() isRoutingRule_TargetTag { + if m != nil { + return m.TargetTag + } + return nil +} + +func (x *RoutingRule) GetTag() string { + if x, ok := x.GetTargetTag().(*RoutingRule_Tag); ok { + return x.Tag + } + return "" +} + +func (x *RoutingRule) GetBalancingTag() string { + if x, ok := x.GetTargetTag().(*RoutingRule_BalancingTag); ok { + return x.BalancingTag + } + return "" +} + +func (x *RoutingRule) GetDomain() []*Domain { + if x != nil { + return x.Domain + } + return nil +} + +// Deprecated: Do not use. +func (x *RoutingRule) GetCidr() []*CIDR { + if x != nil { + return x.Cidr + } + return nil +} + +func (x *RoutingRule) GetGeoip() []*GeoIP { + if x != nil { + return x.Geoip + } + return nil +} + +// Deprecated: Do not use. +func (x *RoutingRule) GetPortRange() *net.PortRange { + if x != nil { + return x.PortRange + } + return nil +} + +func (x *RoutingRule) GetPortList() *net.PortList { + if x != nil { + return x.PortList + } + return nil +} + +// Deprecated: Do not use. +func (x *RoutingRule) GetNetworkList() *net.NetworkList { + if x != nil { + return x.NetworkList + } + return nil +} + +func (x *RoutingRule) GetNetworks() []net.Network { + if x != nil { + return x.Networks + } + return nil +} + +// Deprecated: Do not use. +func (x *RoutingRule) GetSourceCidr() []*CIDR { + if x != nil { + return x.SourceCidr + } + return nil +} + +func (x *RoutingRule) GetSourceGeoip() []*GeoIP { + if x != nil { + return x.SourceGeoip + } + return nil +} + +func (x *RoutingRule) GetSourcePortList() *net.PortList { + if x != nil { + return x.SourcePortList + } + return nil +} + +func (x *RoutingRule) GetUserEmail() []string { + if x != nil { + return x.UserEmail + } + return nil +} + +func (x *RoutingRule) GetInboundTag() []string { + if x != nil { + return x.InboundTag + } + return nil +} + +func (x *RoutingRule) GetProtocol() []string { + if x != nil { + return x.Protocol + } + return nil +} + +func (x *RoutingRule) GetAttributes() string { + if x != nil { + return x.Attributes + } + return "" +} + +type isRoutingRule_TargetTag interface { + isRoutingRule_TargetTag() +} + +type RoutingRule_Tag struct { + // Tag of outbound that this rule is pointing to. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3,oneof"` +} + +type RoutingRule_BalancingTag struct { + // Tag of routing balancer. + BalancingTag string `protobuf:"bytes,12,opt,name=balancing_tag,json=balancingTag,proto3,oneof"` +} + +func (*RoutingRule_Tag) isRoutingRule_TargetTag() {} + +func (*RoutingRule_BalancingTag) isRoutingRule_TargetTag() {} + +type BalancingRule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + OutboundSelector []string `protobuf:"bytes,2,rep,name=outbound_selector,json=outboundSelector,proto3" json:"outbound_selector,omitempty"` +} + +func (x *BalancingRule) Reset() { + *x = BalancingRule{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BalancingRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BalancingRule) ProtoMessage() {} + +func (x *BalancingRule) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 BalancingRule.ProtoReflect.Descriptor instead. +func (*BalancingRule) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{7} +} + +func (x *BalancingRule) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *BalancingRule) GetOutboundSelector() []string { + if x != nil { + return x.OutboundSelector + } + return nil +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` + Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` + BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[8] + 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_config_proto_msgTypes[8] + 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_config_proto_rawDescGZIP(), []int{8} +} + +func (x *Config) GetDomainStrategy() Config_DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return Config_AsIs +} + +func (x *Config) GetRule() []*RoutingRule { + if x != nil { + return x.Rule + } + return nil +} + +func (x *Config) GetBalancingRule() []*BalancingRule { + if x != nil { + return x.BalancingRule + } + return nil +} + +type Domain_Attribute struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Types that are assignable to TypedValue: + // *Domain_Attribute_BoolValue + // *Domain_Attribute_IntValue + TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` +} + +func (x *Domain_Attribute) Reset() { + *x = Domain_Attribute{} + if protoimpl.UnsafeEnabled { + mi := &file_app_router_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Domain_Attribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain_Attribute) ProtoMessage() {} + +func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[9] + 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 Domain_Attribute.ProtoReflect.Descriptor instead. +func (*Domain_Attribute) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Domain_Attribute) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { + if m != nil { + return m.TypedValue + } + return nil +} + +func (x *Domain_Attribute) GetBoolValue() bool { + if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok { + return x.BoolValue + } + return false +} + +func (x *Domain_Attribute) GetIntValue() int64 { + if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok { + return x.IntValue + } + return 0 +} + +type isDomain_Attribute_TypedValue interface { + isDomain_Attribute_TypedValue() +} + +type Domain_Attribute_BoolValue struct { + BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type Domain_Attribute_IntValue struct { + IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` +} + +func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} + +func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} + +var File_app_router_config_proto protoreflect.FileDescriptor + +var file_app_router_config_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x1a, 0x15, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 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, 0xb3, 0x02, 0x0a, 0x06, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, + 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x1a, + 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, + 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0d, + 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x32, 0x0a, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e, 0x10, 0x00, + 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, + 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x22, 0x55, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x29, 0x0a, + 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, + 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x22, 0x39, 0x0a, 0x09, 0x47, 0x65, 0x6f, 0x49, + 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, + 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, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x22, 0x5d, 0x0a, 0x07, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x22, 0x8e, 0x06, 0x0a, 0x0b, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, + 0x65, 0x12, 0x12, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x25, 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, + 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, + 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x61, 0x67, 0x12, 0x2f, 0x0a, 0x06, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2d, 0x0a, + 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, + 0x44, 0x52, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12, 0x2c, 0x0a, 0x05, + 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0a, 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, 0x3d, 0x0a, 0x0a, 0x70, 0x6f, + 0x72, 0x74, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, + 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x6f, 0x72, + 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, + 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x43, 0x0a, 0x0c, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x69, 0x73, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x18, 0x0d, 0x20, 0x03, 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, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x3a, 0x0a, 0x0b, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x43, 0x69, 0x64, 0x72, 0x12, 0x39, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0b, 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, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x65, + 0x6f, 0x69, 0x70, 0x12, 0x43, 0x0a, 0x10, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, + 0x72, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x50, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, + 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x74, + 0x61, 0x67, 0x22, 0x4e, 0x0a, 0x0d, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, + 0x75, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x22, 0x9b, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, + 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, + 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x52, + 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, + 0x12, 0x45, 0x0a, 0x0e, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, + 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x22, 0x47, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, + 0x73, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x49, 0x70, 0x10, 0x01, 0x12, 0x10, + 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, + 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, + 0x42, 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 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, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_app_router_config_proto_rawDescOnce sync.Once + file_app_router_config_proto_rawDescData = file_app_router_config_proto_rawDesc +) + +func file_app_router_config_proto_rawDescGZIP() []byte { + file_app_router_config_proto_rawDescOnce.Do(func() { + file_app_router_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_router_config_proto_rawDescData) + }) + return file_app_router_config_proto_rawDescData +} + +var file_app_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_app_router_config_proto_goTypes = []interface{}{ + (Domain_Type)(0), // 0: xray.app.router.Domain.Type + (Config_DomainStrategy)(0), // 1: xray.app.router.Config.DomainStrategy + (*Domain)(nil), // 2: xray.app.router.Domain + (*CIDR)(nil), // 3: xray.app.router.CIDR + (*GeoIP)(nil), // 4: xray.app.router.GeoIP + (*GeoIPList)(nil), // 5: xray.app.router.GeoIPList + (*GeoSite)(nil), // 6: xray.app.router.GeoSite + (*GeoSiteList)(nil), // 7: xray.app.router.GeoSiteList + (*RoutingRule)(nil), // 8: xray.app.router.RoutingRule + (*BalancingRule)(nil), // 9: xray.app.router.BalancingRule + (*Config)(nil), // 10: xray.app.router.Config + (*Domain_Attribute)(nil), // 11: xray.app.router.Domain.Attribute + (*net.PortRange)(nil), // 12: xray.common.net.PortRange + (*net.PortList)(nil), // 13: xray.common.net.PortList + (*net.NetworkList)(nil), // 14: xray.common.net.NetworkList + (net.Network)(0), // 15: xray.common.net.Network +} +var file_app_router_config_proto_depIdxs = []int32{ + 0, // 0: xray.app.router.Domain.type:type_name -> xray.app.router.Domain.Type + 11, // 1: xray.app.router.Domain.attribute:type_name -> xray.app.router.Domain.Attribute + 3, // 2: xray.app.router.GeoIP.cidr:type_name -> xray.app.router.CIDR + 4, // 3: xray.app.router.GeoIPList.entry:type_name -> xray.app.router.GeoIP + 2, // 4: xray.app.router.GeoSite.domain:type_name -> xray.app.router.Domain + 6, // 5: xray.app.router.GeoSiteList.entry:type_name -> xray.app.router.GeoSite + 2, // 6: xray.app.router.RoutingRule.domain:type_name -> xray.app.router.Domain + 3, // 7: xray.app.router.RoutingRule.cidr:type_name -> xray.app.router.CIDR + 4, // 8: xray.app.router.RoutingRule.geoip:type_name -> xray.app.router.GeoIP + 12, // 9: xray.app.router.RoutingRule.port_range:type_name -> xray.common.net.PortRange + 13, // 10: xray.app.router.RoutingRule.port_list:type_name -> xray.common.net.PortList + 14, // 11: xray.app.router.RoutingRule.network_list:type_name -> xray.common.net.NetworkList + 15, // 12: xray.app.router.RoutingRule.networks:type_name -> xray.common.net.Network + 3, // 13: xray.app.router.RoutingRule.source_cidr:type_name -> xray.app.router.CIDR + 4, // 14: xray.app.router.RoutingRule.source_geoip:type_name -> xray.app.router.GeoIP + 13, // 15: xray.app.router.RoutingRule.source_port_list:type_name -> xray.common.net.PortList + 1, // 16: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy + 8, // 17: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule + 9, // 18: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule + 19, // [19:19] is the sub-list for method output_type + 19, // [19:19] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name +} + +func init() { file_app_router_config_proto_init() } +func file_app_router_config_proto_init() { + if File_app_router_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_app_router_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Domain); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CIDR); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoIP); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoIPList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoSite); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoSiteList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RoutingRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BalancingRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_router_config_proto_msgTypes[8].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_router_config_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Domain_Attribute); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_app_router_config_proto_msgTypes[6].OneofWrappers = []interface{}{ + (*RoutingRule_Tag)(nil), + (*RoutingRule_BalancingTag)(nil), + } + file_app_router_config_proto_msgTypes[9].OneofWrappers = []interface{}{ + (*Domain_Attribute_BoolValue)(nil), + (*Domain_Attribute_IntValue)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_app_router_config_proto_rawDesc, + NumEnums: 2, + NumMessages: 10, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_router_config_proto_goTypes, + DependencyIndexes: file_app_router_config_proto_depIdxs, + EnumInfos: file_app_router_config_proto_enumTypes, + MessageInfos: file_app_router_config_proto_msgTypes, + }.Build() + File_app_router_config_proto = out.File + file_app_router_config_proto_rawDesc = nil + file_app_router_config_proto_goTypes = nil + file_app_router_config_proto_depIdxs = nil +} diff --git a/app/router/config.proto b/app/router/config.proto new file mode 100644 index 00000000..d7a04f3c --- /dev/null +++ b/app/router/config.proto @@ -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; +} diff --git a/app/router/errors.generated.go b/app/router/errors.generated.go new file mode 100644 index 00000000..76bbc9c7 --- /dev/null +++ b/app/router/errors.generated.go @@ -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{}) +} diff --git a/app/router/router.go b/app/router/router.go new file mode 100644 index 00000000..bc9b4bf6 --- /dev/null +++ b/app/router/router.go @@ -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 + })) +} diff --git a/app/router/router_test.go b/app/router/router_test.go new file mode 100644 index 00000000..7644cee9 --- /dev/null +++ b/app/router/router_test.go @@ -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) + } +} diff --git a/app/stats/channel.go b/app/stats/channel.go new file mode 100644 index 00000000..29822029 --- /dev/null +++ b/app/stats/channel.go @@ -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) + } +} diff --git a/app/stats/channel_test.go b/app/stats/channel_test.go new file mode 100644 index 00000000..dc83711d --- /dev/null +++ b/app/stats/channel_test.go @@ -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: + } +} diff --git a/app/stats/command/command.go b/app/stats/command/command.go new file mode 100644 index 00000000..eace983e --- /dev/null +++ b/app/stats/command/command.go @@ -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 + })) +} diff --git a/app/stats/command/command.pb.go b/app/stats/command/command.pb.go new file mode 100644 index 00000000..8040b470 --- /dev/null +++ b/app/stats/command/command.pb.go @@ -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 +} diff --git a/app/stats/command/command.proto b/app/stats/command/command.proto new file mode 100644 index 00000000..99f31816 --- /dev/null +++ b/app/stats/command/command.proto @@ -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 {} diff --git a/app/stats/command/command_grpc.pb.go b/app/stats/command/command_grpc.pb.go new file mode 100644 index 00000000..149ee996 --- /dev/null +++ b/app/stats/command/command_grpc.pb.go @@ -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", +} diff --git a/app/stats/command/command_test.go b/app/stats/command/command_test.go new file mode 100644 index 00000000..2534b87c --- /dev/null +++ b/app/stats/command/command_test.go @@ -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) + } +} diff --git a/app/stats/command/errors.generated.go b/app/stats/command/errors.generated.go new file mode 100644 index 00000000..76b46f51 --- /dev/null +++ b/app/stats/command/errors.generated.go @@ -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{}) +} diff --git a/app/stats/config.pb.go b/app/stats/config.pb.go new file mode 100644 index 00000000..cfb896bb --- /dev/null +++ b/app/stats/config.pb.go @@ -0,0 +1,225 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: app/stats/config.proto + +package stats + +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_stats_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_stats_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_stats_config_proto_rawDescGZIP(), []int{0} +} + +type ChannelConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Blocking bool `protobuf:"varint,1,opt,name=Blocking,proto3" json:"Blocking,omitempty"` + SubscriberLimit int32 `protobuf:"varint,2,opt,name=SubscriberLimit,proto3" json:"SubscriberLimit,omitempty"` + BufferSize int32 `protobuf:"varint,3,opt,name=BufferSize,proto3" json:"BufferSize,omitempty"` +} + +func (x *ChannelConfig) Reset() { + *x = ChannelConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_app_stats_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelConfig) ProtoMessage() {} + +func (x *ChannelConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_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 ChannelConfig.ProtoReflect.Descriptor instead. +func (*ChannelConfig) Descriptor() ([]byte, []int) { + return file_app_stats_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ChannelConfig) GetBlocking() bool { + if x != nil { + return x.Blocking + } + return false +} + +func (x *ChannelConfig) GetSubscriberLimit() int32 { + if x != nil { + return x.SubscriberLimit + } + return 0 +} + +func (x *ChannelConfig) GetBufferSize() int32 { + if x != nil { + return x.BufferSize + } + return 0 +} + +var File_app_stats_config_proto protoreflect.FileDescriptor + +var file_app_stats_config_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x22, 0x75, 0x0a, 0x0d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x12, + 0x28, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x72, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x72, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x42, 0x75, 0x66, + 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x42, + 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x4f, 0x0a, 0x12, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x50, + 0x01, 0x5a, 0x26, 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, 0xaa, 0x02, 0x0e, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_app_stats_config_proto_rawDescOnce sync.Once + file_app_stats_config_proto_rawDescData = file_app_stats_config_proto_rawDesc +) + +func file_app_stats_config_proto_rawDescGZIP() []byte { + file_app_stats_config_proto_rawDescOnce.Do(func() { + file_app_stats_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_stats_config_proto_rawDescData) + }) + return file_app_stats_config_proto_rawDescData +} + +var file_app_stats_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_stats_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.app.stats.Config + (*ChannelConfig)(nil), // 1: xray.app.stats.ChannelConfig +} +var file_app_stats_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_stats_config_proto_init() } +func file_app_stats_config_proto_init() { + if File_app_stats_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_app_stats_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_stats_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChannelConfig); 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_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_stats_config_proto_goTypes, + DependencyIndexes: file_app_stats_config_proto_depIdxs, + MessageInfos: file_app_stats_config_proto_msgTypes, + }.Build() + File_app_stats_config_proto = out.File + file_app_stats_config_proto_rawDesc = nil + file_app_stats_config_proto_goTypes = nil + file_app_stats_config_proto_depIdxs = nil +} diff --git a/app/stats/config.proto b/app/stats/config.proto new file mode 100644 index 00000000..3ba1e551 --- /dev/null +++ b/app/stats/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.app.stats; +option csharp_namespace = "Xray.App.Stats"; +option go_package = "github.com/xtls/xray-core/v1/app/stats"; +option java_package = "com.xray.app.stats"; +option java_multiple_files = true; + +message Config {} + +message ChannelConfig { + bool Blocking = 1; + int32 SubscriberLimit = 2; + int32 BufferSize = 3; +} diff --git a/app/stats/counter.go b/app/stats/counter.go new file mode 100644 index 00000000..c4e12013 --- /dev/null +++ b/app/stats/counter.go @@ -0,0 +1,25 @@ +// +build !confonly + +package stats + +import "sync/atomic" + +// Counter is an implementation of stats.Counter. +type Counter struct { + value int64 +} + +// Value implements stats.Counter. +func (c *Counter) Value() int64 { + return atomic.LoadInt64(&c.value) +} + +// Set implements stats.Counter. +func (c *Counter) Set(newValue int64) int64 { + return atomic.SwapInt64(&c.value, newValue) +} + +// Add implements stats.Counter. +func (c *Counter) Add(delta int64) int64 { + return atomic.AddInt64(&c.value, delta) +} diff --git a/app/stats/counter_test.go b/app/stats/counter_test.go new file mode 100644 index 00000000..32bcb66e --- /dev/null +++ b/app/stats/counter_test.go @@ -0,0 +1,31 @@ +package stats_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/v1/app/stats" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/features/stats" +) + +func TestStatsCounter(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterCounter("test.counter") + common.Must(err) + + if v := c.Add(1); v != 1 { + t.Fatal("unpexcted Add(1) return: ", v, ", wanted ", 1) + } + + if v := c.Set(0); v != 1 { + t.Fatal("unexpected Set(0) return: ", v, ", wanted ", 1) + } + + if v := c.Value(); v != 0 { + t.Fatal("unexpected Value() return: ", v, ", wanted ", 0) + } +} diff --git a/app/stats/errors.generated.go b/app/stats/errors.generated.go new file mode 100644 index 00000000..d0bc052a --- /dev/null +++ b/app/stats/errors.generated.go @@ -0,0 +1,9 @@ +package stats + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/app/stats/stats.go b/app/stats/stats.go new file mode 100644 index 00000000..ef7d080d --- /dev/null +++ b/app/stats/stats.go @@ -0,0 +1,169 @@ +// +build !confonly + +package stats + +//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/errors" + "github.com/xtls/xray-core/v1/features/stats" +) + +// Manager is an implementation of stats.Manager. +type Manager struct { + access sync.RWMutex + counters map[string]*Counter + channels map[string]*Channel + running bool +} + +// NewManager creates an instance of Statistics Manager. +func NewManager(ctx context.Context, config *Config) (*Manager, error) { + m := &Manager{ + counters: make(map[string]*Counter), + channels: make(map[string]*Channel), + } + + return m, nil +} + +// Type implements common.HasType. +func (*Manager) Type() interface{} { + return stats.ManagerType() +} + +// RegisterCounter implements stats.Manager. +func (m *Manager) RegisterCounter(name string) (stats.Counter, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.counters[name]; found { + return nil, newError("Counter ", name, " already registered.") + } + newError("create new counter ", name).AtDebug().WriteToLog() + c := new(Counter) + m.counters[name] = c + return c, nil +} + +// UnregisterCounter implements stats.Manager. +func (m *Manager) UnregisterCounter(name string) error { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.counters[name]; found { + newError("remove counter ", name).AtDebug().WriteToLog() + delete(m.counters, name) + } + return nil +} + +// GetCounter implements stats.Manager. +func (m *Manager) GetCounter(name string) stats.Counter { + m.access.RLock() + defer m.access.RUnlock() + + if c, found := m.counters[name]; found { + return c + } + return nil +} + +// VisitCounters calls visitor function on all managed counters. +func (m *Manager) VisitCounters(visitor func(string, stats.Counter) bool) { + m.access.RLock() + defer m.access.RUnlock() + + for name, c := range m.counters { + if !visitor(name, c) { + break + } + } +} + +// RegisterChannel implements stats.Manager. +func (m *Manager) RegisterChannel(name string) (stats.Channel, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.channels[name]; found { + return nil, newError("Channel ", name, " already registered.") + } + newError("create new channel ", name).AtDebug().WriteToLog() + c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false}) + m.channels[name] = c + if m.running { + return c, c.Start() + } + return c, nil +} + +// UnregisterChannel implements stats.Manager. +func (m *Manager) UnregisterChannel(name string) error { + m.access.Lock() + defer m.access.Unlock() + + if c, found := m.channels[name]; found { + newError("remove channel ", name).AtDebug().WriteToLog() + delete(m.channels, name) + return c.Close() + } + return nil +} + +// GetChannel implements stats.Manager. +func (m *Manager) GetChannel(name string) stats.Channel { + m.access.RLock() + defer m.access.RUnlock() + + if c, found := m.channels[name]; found { + return c + } + return nil +} + +// Start implements common.Runnable. +func (m *Manager) Start() error { + m.access.Lock() + defer m.access.Unlock() + m.running = true + errs := []error{} + for _, channel := range m.channels { + if err := channel.Start(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { + return errors.Combine(errs...) + } + return nil +} + +// Close implement common.Closable. +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + m.running = false + errs := []error{} + for name, channel := range m.channels { + newError("remove channel ", name).AtDebug().WriteToLog() + delete(m.channels, name) + if err := channel.Close(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { + return errors.Combine(errs...) + } + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewManager(ctx, config.(*Config)) + })) +} diff --git a/app/stats/stats_test.go b/app/stats/stats_test.go new file mode 100644 index 00000000..3a525818 --- /dev/null +++ b/app/stats/stats_test.go @@ -0,0 +1,86 @@ +package stats_test + +import ( + "context" + "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 TestInterface(t *testing.T) { + _ = (stats.Manager)(new(Manager)) +} + +func TestStatsChannelRunnable(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + + ch1, err := m.RegisterChannel("test.channel.1") + c1 := ch1.(*Channel) + common.Must(err) + + if c1.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 1) + } + + common.Must(m.Start()) + + if !c1.Running() { + t.Fatalf("unexpected non-running channel: test.channel.%d", 1) + } + + ch2, err := m.RegisterChannel("test.channel.2") + c2 := ch2.(*Channel) + common.Must(err) + + if !c2.Running() { + t.Fatalf("unexpected non-running channel: test.channel.%d", 2) + } + + s1, err := c1.Subscribe() + common.Must(err) + common.Must(c1.Close()) + + if c1.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 1) + } + + select { // Check all subscribers in closed channel are closed + case _, ok := <-s1: + if ok { + t.Fatalf("unexpected non-closed subscriber in channel: test.channel.%d", 1) + } + case <-time.After(500 * time.Millisecond): + t.Fatalf("unexpected non-closed subscriber in channel: test.channel.%d", 1) + } + + if len(c1.Subscribers()) != 0 { // Check subscribers in closed channel are emptied + t.Fatalf("unexpected non-empty subscribers in channel: test.channel.%d", 1) + } + + common.Must(m.Close()) + + if c2.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 2) + } + + ch3, err := m.RegisterChannel("test.channel.3") + c3 := ch3.(*Channel) + common.Must(err) + + if c3.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 3) + } + + common.Must(c3.Start()) + common.Must(m.UnregisterChannel("test.channel.3")) + + if c3.Running() { // Test that unregistering will close the channel. + t.Fatalf("unexpected running channel: test.channel.%d", 3) + } +} diff --git a/common/antireplay/replayfilter.go b/common/antireplay/replayfilter.go new file mode 100644 index 00000000..4e0783d1 --- /dev/null +++ b/common/antireplay/replayfilter.go @@ -0,0 +1,58 @@ +package antireplay + +import ( + "sync" + "time" + + cuckoo "github.com/seiflotfy/cuckoofilter" +) + +const replayFilterCapacity = 100000 + +// ReplayFilter check for replay attacks. +type ReplayFilter struct { + lock sync.Mutex + poolA *cuckoo.Filter + poolB *cuckoo.Filter + poolSwap bool + lastSwap int64 + interval int64 +} + +// NewReplayFilter create a new filter with specifying the expiration time interval in seconds. +func NewReplayFilter(interval int64) *ReplayFilter { + filter := &ReplayFilter{} + filter.interval = interval + return filter +} + +// Interval in second for expiration time for duplicate records. +func (filter *ReplayFilter) Interval() int64 { + return filter.interval +} + +// Check determine if there are duplicate records. +func (filter *ReplayFilter) Check(sum []byte) bool { + filter.lock.Lock() + defer filter.lock.Unlock() + + now := time.Now().Unix() + if filter.lastSwap == 0 { + filter.lastSwap = now + filter.poolA = cuckoo.NewFilter(replayFilterCapacity) + filter.poolB = cuckoo.NewFilter(replayFilterCapacity) + } + + elapsed := now - filter.lastSwap + if elapsed >= filter.Interval() { + if filter.poolSwap { + filter.poolA.Reset() + } else { + filter.poolB.Reset() + } + filter.poolSwap = !filter.poolSwap + filter.lastSwap = now + } + + return filter.poolA.InsertUnique(sum) && filter.poolB.InsertUnique(sum) +} diff --git a/common/bitmask/byte.go b/common/bitmask/byte.go new file mode 100644 index 00000000..8dcc5c0c --- /dev/null +++ b/common/bitmask/byte.go @@ -0,0 +1,21 @@ +package bitmask + +// Byte is a bitmask in byte. +type Byte byte + +// Has returns true if this bitmask contains another bitmask. +func (b Byte) Has(bb Byte) bool { + return (b & bb) != 0 +} + +func (b *Byte) Set(bb Byte) { + *b |= bb +} + +func (b *Byte) Clear(bb Byte) { + *b &= ^bb +} + +func (b *Byte) Toggle(bb Byte) { + *b ^= bb +} diff --git a/common/bitmask/byte_test.go b/common/bitmask/byte_test.go new file mode 100644 index 00000000..f9f46cfe --- /dev/null +++ b/common/bitmask/byte_test.go @@ -0,0 +1,36 @@ +package bitmask_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/bitmask" +) + +func TestBitmaskByte(t *testing.T) { + b := Byte(0) + b.Set(Byte(1)) + if !b.Has(1) { + t.Fatal("expected ", b, " to contain 1, but actually not") + } + + b.Set(Byte(2)) + if !b.Has(2) { + t.Fatal("expected ", b, " to contain 2, but actually not") + } + if !b.Has(1) { + t.Fatal("expected ", b, " to contain 1, but actually not") + } + + b.Clear(Byte(1)) + if !b.Has(2) { + t.Fatal("expected ", b, " to contain 2, but actually not") + } + if b.Has(1) { + t.Fatal("expected ", b, " to not contain 1, but actually did") + } + + b.Toggle(Byte(2)) + if b.Has(2) { + t.Fatal("expected ", b, " to not contain 2, but actually did") + } +} diff --git a/common/buf/buf.go b/common/buf/buf.go new file mode 100644 index 00000000..a0004905 --- /dev/null +++ b/common/buf/buf.go @@ -0,0 +1,4 @@ +// Package buf provides a light-weight memory allocation mechanism. +package buf // import "github.com/xtls/xray-core/v1/common/buf" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/common/buf/buffer.go b/common/buf/buffer.go new file mode 100644 index 00000000..f8298e5a --- /dev/null +++ b/common/buf/buffer.go @@ -0,0 +1,212 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/v1/common/bytespool" +) + +const ( + // Size of a regular buffer. + Size = 8192 +) + +var pool = bytespool.GetPool(Size) + +// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles +// the buffer into an internal buffer pool, in order to recreate a buffer more +// quickly. +type Buffer struct { + v []byte + start int32 + end int32 +} + +// New creates a Buffer with 0 length and 2K capacity. +func New() *Buffer { + return &Buffer{ + v: pool.Get().([]byte), + } +} + +// StackNew creates a new Buffer object on stack. +// This method is for buffers that is released in the same function. +func StackNew() Buffer { + return Buffer{ + v: pool.Get().([]byte), + } +} + +// Release recycles the buffer into an internal buffer pool. +func (b *Buffer) Release() { + if b == nil || b.v == nil { + return + } + + p := b.v + b.v = nil + b.Clear() + pool.Put(p) +} + +// Clear clears the content of the buffer, results an empty buffer with +// Len() = 0. +func (b *Buffer) Clear() { + b.start = 0 + b.end = 0 +} + +// Byte returns the bytes at index. +func (b *Buffer) Byte(index int32) byte { + return b.v[b.start+index] +} + +// SetByte sets the byte value at index. +func (b *Buffer) SetByte(index int32, value byte) { + b.v[b.start+index] = value +} + +// Bytes returns the content bytes of this Buffer. +func (b *Buffer) Bytes() []byte { + return b.v[b.start:b.end] +} + +// Extend increases the buffer size by n bytes, and returns the extended part. +// It panics if result size is larger than buf.Size. +func (b *Buffer) Extend(n int32) []byte { + end := b.end + n + if end > int32(len(b.v)) { + panic("extending out of bound") + } + ext := b.v[b.end:end] + b.end = end + return ext +} + +// BytesRange returns a slice of this buffer with given from and to boundary. +func (b *Buffer) BytesRange(from, to int32) []byte { + if from < 0 { + from += b.Len() + } + if to < 0 { + to += b.Len() + } + return b.v[b.start+from : b.start+to] +} + +// BytesFrom returns a slice of this Buffer starting from the given position. +func (b *Buffer) BytesFrom(from int32) []byte { + if from < 0 { + from += b.Len() + } + return b.v[b.start+from : b.end] +} + +// BytesTo returns a slice of this Buffer from start to the given position. +func (b *Buffer) BytesTo(to int32) []byte { + if to < 0 { + to += b.Len() + } + return b.v[b.start : b.start+to] +} + +// Resize cuts the buffer at the given position. +func (b *Buffer) Resize(from, to int32) { + if from < 0 { + from += b.Len() + } + if to < 0 { + to += b.Len() + } + if to < from { + panic("Invalid slice") + } + b.end = b.start + to + b.start += from +} + +// Advance cuts the buffer at the given position. +func (b *Buffer) Advance(from int32) { + if from < 0 { + from += b.Len() + } + b.start += from +} + +// Len returns the length of the buffer content. +func (b *Buffer) Len() int32 { + if b == nil { + return 0 + } + return b.end - b.start +} + +// IsEmpty returns true if the buffer is empty. +func (b *Buffer) IsEmpty() bool { + return b.Len() == 0 +} + +// IsFull returns true if the buffer has no more room to grow. +func (b *Buffer) IsFull() bool { + return b != nil && b.end == int32(len(b.v)) +} + +// Write implements Write method in io.Writer. +func (b *Buffer) Write(data []byte) (int, error) { + nBytes := copy(b.v[b.end:], data) + b.end += int32(nBytes) + return nBytes, nil +} + +// WriteByte writes a single byte into the buffer. +func (b *Buffer) WriteByte(v byte) error { + if b.IsFull() { + return newError("buffer full") + } + b.v[b.end] = v + b.end++ + return nil +} + +// WriteString implements io.StringWriter. +func (b *Buffer) WriteString(s string) (int, error) { + return b.Write([]byte(s)) +} + +// Read implements io.Reader.Read(). +func (b *Buffer) Read(data []byte) (int, error) { + if b.Len() == 0 { + return 0, io.EOF + } + nBytes := copy(data, b.v[b.start:b.end]) + if int32(nBytes) == b.Len() { + b.Clear() + } else { + b.start += int32(nBytes) + } + return nBytes, nil +} + +// ReadFrom implements io.ReaderFrom. +func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) { + n, err := reader.Read(b.v[b.end:]) + b.end += int32(n) + return int64(n), err +} + +// ReadFullFrom reads exact size of bytes from given reader, or until error occurs. +func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) { + end := b.end + size + if end > int32(len(b.v)) { + v := end + return 0, newError("out of bound: ", v) + } + n, err := io.ReadFull(reader, b.v[b.end:end]) + b.end += int32(n) + return int64(n), err +} + +// String returns the string form of this Buffer. +func (b *Buffer) String() string { + return string(b.Bytes()) +} diff --git a/common/buf/buffer_test.go b/common/buf/buffer_test.go new file mode 100644 index 00000000..ebaf8ea0 --- /dev/null +++ b/common/buf/buffer_test.go @@ -0,0 +1,223 @@ +package buf_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/buf" +) + +func TestBufferClear(t *testing.T) { + buffer := New() + defer buffer.Release() + + payload := "Bytes" + buffer.Write([]byte(payload)) + if diff := cmp.Diff(buffer.Bytes(), []byte(payload)); diff != "" { + t.Error(diff) + } + + buffer.Clear() + if buffer.Len() != 0 { + t.Error("expect 0 length, but got ", buffer.Len()) + } +} + +func TestBufferIsEmpty(t *testing.T) { + buffer := New() + defer buffer.Release() + + if buffer.IsEmpty() != true { + t.Error("expect empty buffer, but not") + } +} + +func TestBufferString(t *testing.T) { + buffer := New() + defer buffer.Release() + + const payload = "Test String" + common.Must2(buffer.WriteString(payload)) + if buffer.String() != payload { + t.Error("expect buffer content as ", payload, " but actually ", buffer.String()) + } +} + +func TestBufferByte(t *testing.T) { + { + buffer := New() + common.Must(buffer.WriteByte('m')) + if buffer.String() != "m" { + t.Error("expect buffer content as ", "m", " but actually ", buffer.String()) + } + buffer.Release() + } + { + buffer := StackNew() + common.Must(buffer.WriteByte('n')) + if buffer.String() != "n" { + t.Error("expect buffer content as ", "n", " but actually ", buffer.String()) + } + buffer.Release() + } + { + buffer := StackNew() + common.Must2(buffer.WriteString("HELLOWORLD")) + if b := buffer.Byte(5); b != 'W' { + t.Error("unexpected byte ", b) + } + + buffer.SetByte(5, 'M') + if buffer.String() != "HELLOMORLD" { + t.Error("expect buffer content as ", "n", " but actually ", buffer.String()) + } + buffer.Release() + } +} +func TestBufferResize(t *testing.T) { + buffer := New() + defer buffer.Release() + + const payload = "Test String" + common.Must2(buffer.WriteString(payload)) + if buffer.String() != payload { + t.Error("expect buffer content as ", payload, " but actually ", buffer.String()) + } + + buffer.Resize(-6, -3) + if l := buffer.Len(); int(l) != 3 { + t.Error("len error ", l) + } + + if s := buffer.String(); s != "Str" { + t.Error("unexpect buffer ", s) + } + + buffer.Resize(int32(len(payload)), 200) + if l := buffer.Len(); int(l) != 200-len(payload) { + t.Error("len error ", l) + } +} + +func TestBufferSlice(t *testing.T) { + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesFrom(-2) + if diff := cmp.Diff(bytes, []byte{'c', 'd'}); diff != "" { + t.Error(diff) + } + } + + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesTo(-2) + if diff := cmp.Diff(bytes, []byte{'a', 'b'}); diff != "" { + t.Error(diff) + } + } + + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesRange(-3, -1) + if diff := cmp.Diff(bytes, []byte{'b', 'c'}); diff != "" { + t.Error(diff) + } + } +} + +func TestBufferReadFullFrom(t *testing.T) { + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + reader := bytes.NewReader(payload) + b := New() + n, err := b.ReadFullFrom(reader, 1024) + common.Must(err) + if n != 1024 { + t.Error("expect reading 1024 bytes, but actually ", n) + } + + if diff := cmp.Diff(payload, b.Bytes()); diff != "" { + t.Error(diff) + } +} + +func BenchmarkNewBuffer(b *testing.B) { + for i := 0; i < b.N; i++ { + buffer := New() + buffer.Release() + } +} + +func BenchmarkNewBufferStack(b *testing.B) { + for i := 0; i < b.N; i++ { + buffer := StackNew() + buffer.Release() + } +} + +func BenchmarkWrite2(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write([]byte{'a', 'b'}) + buffer.Clear() + } +} + +func BenchmarkWrite8(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write([]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}) + buffer.Clear() + } +} + +func BenchmarkWrite32(b *testing.B) { + buffer := New() + payload := make([]byte, 32) + rand.Read(payload) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write(payload) + buffer.Clear() + } +} + +func BenchmarkWriteByte2(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buffer.WriteByte('a') + _ = buffer.WriteByte('b') + buffer.Clear() + } +} + +func BenchmarkWriteByte8(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buffer.WriteByte('a') + _ = buffer.WriteByte('b') + _ = buffer.WriteByte('c') + _ = buffer.WriteByte('d') + _ = buffer.WriteByte('e') + _ = buffer.WriteByte('f') + _ = buffer.WriteByte('g') + _ = buffer.WriteByte('h') + buffer.Clear() + } +} diff --git a/common/buf/copy.go b/common/buf/copy.go new file mode 100644 index 00000000..a01a0638 --- /dev/null +++ b/common/buf/copy.go @@ -0,0 +1,123 @@ +package buf + +import ( + "io" + "time" + + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/signal" +) + +type dataHandler func(MultiBuffer) + +type copyHandler struct { + onData []dataHandler +} + +// SizeCounter is for counting bytes copied by Copy(). +type SizeCounter struct { + Size int64 +} + +// CopyOption is an option for copying data. +type CopyOption func(*copyHandler) + +// UpdateActivity is a CopyOption to update activity on each data copy operation. +func UpdateActivity(timer signal.ActivityUpdater) CopyOption { + return func(handler *copyHandler) { + handler.onData = append(handler.onData, func(MultiBuffer) { + timer.Update() + }) + } +} + +// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter. +func CountSize(sc *SizeCounter) CopyOption { + return func(handler *copyHandler) { + handler.onData = append(handler.onData, func(b MultiBuffer) { + sc.Size += int64(b.Len()) + }) + } +} + +type readError struct { + error +} + +func (e readError) Error() string { + return e.error.Error() +} + +func (e readError) Inner() error { + return e.error +} + +// IsReadError returns true if the error in Copy() comes from reading. +func IsReadError(err error) bool { + _, ok := err.(readError) + return ok +} + +type writeError struct { + error +} + +func (e writeError) Error() string { + return e.error.Error() +} + +func (e writeError) Inner() error { + return e.error +} + +// IsWriteError returns true if the error in Copy() comes from writing. +func IsWriteError(err error) bool { + _, ok := err.(writeError) + return ok +} + +func copyInternal(reader Reader, writer Writer, handler *copyHandler) error { + for { + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + for _, handler := range handler.onData { + handler(buffer) + } + + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return writeError{werr} + } + } + + if err != nil { + return readError{err} + } + } +} + +// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF. +func Copy(reader Reader, writer Writer, options ...CopyOption) error { + var handler copyHandler + for _, option := range options { + option(&handler) + } + err := copyInternal(reader, writer, &handler) + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil +} + +var ErrNotTimeoutReader = newError("not a TimeoutReader") + +func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error { + timeoutReader, ok := reader.(TimeoutReader) + if !ok { + return ErrNotTimeoutReader + } + mb, err := timeoutReader.ReadMultiBufferTimeout(timeout) + if err != nil { + return err + } + return writer.WriteMultiBuffer(mb) +} diff --git a/common/buf/copy_test.go b/common/buf/copy_test.go new file mode 100644 index 00000000..98c5c055 --- /dev/null +++ b/common/buf/copy_test.go @@ -0,0 +1,71 @@ +package buf_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/testing/mocks" +) + +func TestReadError(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockReader := mocks.NewReader(mockCtl) + mockReader.EXPECT().Read(gomock.Any()).Return(0, errors.New("error")) + + err := buf.Copy(buf.NewReader(mockReader), buf.Discard) + if err == nil { + t.Fatal("expected error, but nil") + } + + if !buf.IsReadError(err) { + t.Error("expected to be ReadError, but not") + } + + if err.Error() != "error" { + t.Fatal("unexpected error message: ", err.Error()) + } +} + +func TestWriteError(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockWriter := mocks.NewWriter(mockCtl) + mockWriter.EXPECT().Write(gomock.Any()).Return(0, errors.New("error")) + + err := buf.Copy(buf.NewReader(rand.Reader), buf.NewWriter(mockWriter)) + if err == nil { + t.Fatal("expected error, but nil") + } + + if !buf.IsWriteError(err) { + t.Error("expected to be WriteError, but not") + } + + if err.Error() != "error" { + t.Fatal("unexpected error message: ", err.Error()) + } +} + +type TestReader struct{} + +func (TestReader) Read(b []byte) (int, error) { + return len(b), nil +} + +func BenchmarkCopy(b *testing.B) { + reader := buf.NewReader(io.LimitReader(TestReader{}, 10240)) + writer := buf.Discard + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buf.Copy(reader, writer) + } +} diff --git a/common/buf/data/test_MultiBufferReadAllToByte.dat b/common/buf/data/test_MultiBufferReadAllToByte.dat new file mode 100644 index 00000000..83cb9032 --- /dev/null +++ b/common/buf/data/test_MultiBufferReadAllToByte.dat @@ -0,0 +1,39 @@ + +???8+?$??I????+??????+?+++?IO7$ZD88ZDMD8OZ$7II7+++++++++++++ ++??7++???I????????+?+++?+I?IZI$OND7ODDDDDD7Z$IZI++++++++++++ +???I????????????~,...~?++I?777$DD8O8DDD88O$O7$7I++++++++++?+ +???????????????.,::~...,+?I77ZZD8ZDNDDDDD8ZZ7$$7+++++++?+?+? +??????????????.,,:~~~==,I?7$$ZOD8ODNDD8DDZ$87777++?+++?????+ +?????????I?=...:~~~~=~=+I?$$ZODD88ND8N8DDOZOZ77?????++?????? +???II?????.,,,:==~~===I?IIZ$O$88ODD8ODNDDDOO$7$??I?++?++++?? +???I????+..,,~=+???+?????7OOZZ8O$$778DDDDDO87I$I++++++++???? +I??????..,,:~=??????+=,~?ZZZ$$I??II$DDDDD8Z8I~,+=?II$777IIII +II???,.,,::~??I?I?....,,~==I?+===+?$ODN8DD$O=,......+?????II +I?I?..,,:~~????,...,,::::~~~~~~~~=+$88ODD88=~,,,.......IIIII +II,..,,:~~I?:..,,,::::~~~~~~~~~~~~~+IOZ87?~~~::::,,,,...=?II +I,...,:::....,:::::::~~~~~~~~~~~~~~~=++=~~~~~~~~~~~:::,,,?II +,,,,~....,,,::::::::::::::~~~~~~~~~~~~~~~~~~~~~~~~~~~::,,,?? +:~:...,,,:::::::::::::::::::~~~~~~~~~~~~~~~~~~~~~~~~~~::,,II +:::::::::::::::::::~+++::::::~~~~~~~~~~~~~~~~~~~~~~~~::::,,7 +::::::::::::::~IIII?????:::::::~~~~~~~~~~~~~~~~~~~~~::::::,I +:,,,,,,,:+ZIIIIIIIIIIIII:::~::~~~~~~~~~~~~~~~~~~~~=~:::::::: +7I777IIZI7ZIIIIIIIIIIII7?:~~~~~~~~~~~~~~~~~~~~~~~~~=~::::::: +$$$77$7Z77$7I77IIII7III$$:~~~~~~~~~~~~~~~~~~~~~~~=II~::::::: +$$$8$Z7$7$Z777777777777Z7~:~~~~~~~~~~~~~~~~~~~~~~$777::::::, +ZOZOZOZZ$7$$ZZ$8DDDZ777$$=~~~~~~~~~~~~~~~~~~~~~~~$$$7~:::::, +OOZOOOZZOOZO$ZZZ$O$$$$7ZZ$~~~~~~~~~~~~~~~~~~~~~~~ZZ$ZZ:::::, +O88OOOOO8ODOZZZZZOOZ8OOOOO:~~~~~~~~~~~~~~~~~~~~~ZOZZZZ~::::: +8888O8OODZ8ZOZOZZOOZOOOOOZ:::~~~~~~~~~~~~~~~~~~~,Z$ZOOO::::: +Z88O88D8Z88ZZOOZZOZ$$Z$$OZ:::~~~~~~~~~~~~~~~~~~~,,ZOOOOO:::: +888D88OODD8DNDNDNNDDDD88OI:::::~~~~~~~~~~~~~~~~~.,:8ZO8O:::: +D8D88DO88ZOOZOO8DDDNOZ$$O8~::::~~~~~~~~~~~~~===~..,88O8OO::: +8OD8O8OODO$D8DO88DO8O8888O~~::~~::~~~~~~~~~~~===...:8OOOZ~:: +:..................,~,..~,~~:~:~~~~~~~~~~~~~~===...,+.....~~ +.........................~~~:~~~~~~~~~~~~~~~~~==:..,......:~ +.Made with love.........,~~~~~~~~:~~~~::~~~~~~~==..,,......: +........................~~~~~~~~~~~~~~:~~~~~~~~===,.,......~ +...................,,..~~~~~~~~~~~~~~~~~~~~~~~~~==~,,....... +..................,,::~~~~~~~~~~~~~~~~~~~~~~~~~====~.,....,. +....................:~~~~~~~~~~~~~~~~~~~~~~~~~~~~==~:......, +......................,~================,.==~~~=~===~,...... +.Thank you for your support.....................:~=,,,,,,,.. diff --git a/common/buf/errors.generated.go b/common/buf/errors.generated.go new file mode 100644 index 00000000..3c44bcef --- /dev/null +++ b/common/buf/errors.generated.go @@ -0,0 +1,9 @@ +package buf + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/buf/io.go b/common/buf/io.go new file mode 100644 index 00000000..2a4cd670 --- /dev/null +++ b/common/buf/io.go @@ -0,0 +1,116 @@ +package buf + +import ( + "io" + "net" + "os" + "syscall" + "time" +) + +// Reader extends io.Reader with MultiBuffer. +type Reader interface { + // ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer. + ReadMultiBuffer() (MultiBuffer, error) +} + +// ErrReadTimeout is an error that happens with IO timeout. +var ErrReadTimeout = newError("IO timeout") + +// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout. +type TimeoutReader interface { + ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error) +} + +// Writer extends io.Writer with MultiBuffer. +type Writer interface { + // WriteMultiBuffer writes a MultiBuffer into underlying writer. + WriteMultiBuffer(MultiBuffer) error +} + +// WriteAllBytes ensures all bytes are written into the given writer. +func WriteAllBytes(writer io.Writer, payload []byte) error { + for len(payload) > 0 { + n, err := writer.Write(payload) + if err != nil { + return err + } + payload = payload[n:] + } + return nil +} + +func isPacketReader(reader io.Reader) bool { + _, ok := reader.(net.PacketConn) + return ok +} + +// NewReader creates a new Reader. +// The Reader instance doesn't take the ownership of reader. +func NewReader(reader io.Reader) Reader { + if mr, ok := reader.(Reader); ok { + return mr + } + + if isPacketReader(reader) { + return &PacketReader{ + Reader: reader, + } + } + + _, isFile := reader.(*os.File) + if !isFile && useReadv { + if sc, ok := reader.(syscall.Conn); ok { + rawConn, err := sc.SyscallConn() + if err != nil { + newError("failed to get sysconn").Base(err).WriteToLog() + } else { + return NewReadVReader(reader, rawConn) + } + } + } + + return &SingleReader{ + Reader: reader, + } +} + +// NewPacketReader creates a new PacketReader based on the given reader. +func NewPacketReader(reader io.Reader) Reader { + if mr, ok := reader.(Reader); ok { + return mr + } + + return &PacketReader{ + Reader: reader, + } +} + +func isPacketWriter(writer io.Writer) bool { + if _, ok := writer.(net.PacketConn); ok { + return true + } + + // If the writer doesn't implement syscall.Conn, it is probably not a TCP connection. + if _, ok := writer.(syscall.Conn); !ok { + return true + } + return false +} + +// NewWriter creates a new Writer. +func NewWriter(writer io.Writer) Writer { + if mw, ok := writer.(Writer); ok { + return mw + } + + if isPacketWriter(writer) { + return &SequentialWriter{ + Writer: writer, + } + } + + return &BufferToBytesWriter{ + Writer: writer, + } +} diff --git a/common/buf/io_test.go b/common/buf/io_test.go new file mode 100644 index 00000000..40fdb9fd --- /dev/null +++ b/common/buf/io_test.go @@ -0,0 +1,50 @@ +package buf_test + +import ( + "crypto/tls" + "io" + "testing" + + . "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func TestWriterCreation(t *testing.T) { + tcpServer := tcp.Server{} + dest, err := tcpServer.Start() + if err != nil { + t.Fatal("failed to start tcp server: ", err) + } + defer tcpServer.Close() + + conn, err := net.Dial("tcp", dest.NetAddr()) + if err != nil { + t.Fatal("failed to dial a TCP connection: ", err) + } + defer conn.Close() + + { + writer := NewWriter(conn) + if _, ok := writer.(*BufferToBytesWriter); !ok { + t.Fatal("writer is not a BufferToBytesWriter") + } + + writer2 := NewWriter(writer.(io.Writer)) + if writer2 != writer { + t.Fatal("writer is not reused") + } + } + + tlsConn := tls.Client(conn, &tls.Config{ + InsecureSkipVerify: true, + }) + defer tlsConn.Close() + + { + writer := NewWriter(tlsConn) + if _, ok := writer.(*SequentialWriter); !ok { + t.Fatal("writer is not a SequentialWriter") + } + } +} diff --git a/common/buf/multi_buffer.go b/common/buf/multi_buffer.go new file mode 100644 index 00000000..9be6c0af --- /dev/null +++ b/common/buf/multi_buffer.go @@ -0,0 +1,297 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/serial" +) + +// ReadAllToBytes reads all content from the reader into a byte array, until EOF. +func ReadAllToBytes(reader io.Reader) ([]byte, error) { + mb, err := ReadFrom(reader) + if err != nil { + return nil, err + } + if mb.Len() == 0 { + return nil, nil + } + b := make([]byte, mb.Len()) + mb, _ = SplitBytes(mb, b) + ReleaseMulti(mb) + return b, nil +} + +// MultiBuffer is a list of Buffers. The order of Buffer matters. +type MultiBuffer []*Buffer + +// MergeMulti merges content from src to dest, and returns the new address of dest and src +func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) { + dest = append(dest, src...) + for idx := range src { + src[idx] = nil + } + return dest, src[:0] +} + +// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer. +func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer { + n := len(dest) + if n > 0 && !(dest)[n-1].IsFull() { + nBytes, _ := (dest)[n-1].Write(src) + src = src[nBytes:] + } + + for len(src) > 0 { + b := New() + nBytes, _ := b.Write(src) + src = src[nBytes:] + dest = append(dest, b) + } + + return dest +} + +// ReleaseMulti release all content of the MultiBuffer, and returns an empty MultiBuffer. +func ReleaseMulti(mb MultiBuffer) MultiBuffer { + for i := range mb { + mb[i].Release() + mb[i] = nil + } + return mb[:0] +} + +// Copy copied the beginning part of the MultiBuffer into the given byte array. +func (mb MultiBuffer) Copy(b []byte) int { + total := 0 + for _, bb := range mb { + nBytes := copy(b[total:], bb.Bytes()) + total += nBytes + if int32(nBytes) < bb.Len() { + break + } + } + return total +} + +// ReadFrom reads all content from reader until EOF. +func ReadFrom(reader io.Reader) (MultiBuffer, error) { + mb := make(MultiBuffer, 0, 16) + for { + b := New() + _, err := b.ReadFullFrom(reader, Size) + if b.IsEmpty() { + b.Release() + } else { + mb = append(mb, b) + } + if err != nil { + if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF { + return mb, nil + } + return mb, err + } + } +} + +// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer. +// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice. +func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) { + totalBytes := 0 + endIndex := -1 + for i := range mb { + pBuffer := mb[i] + nBytes, _ := pBuffer.Read(b) + totalBytes += nBytes + b = b[nBytes:] + if !pBuffer.IsEmpty() { + endIndex = i + break + } + pBuffer.Release() + mb[i] = nil + } + + if endIndex == -1 { + mb = mb[:0] + } else { + mb = mb[endIndex:] + } + + return mb, totalBytes +} + +// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice. +func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) { + mb, b := SplitFirst(mb) + if b == nil { + return mb, 0 + } + n := copy(p, b.Bytes()) + b.Release() + return mb, n +} + +// Compact returns another MultiBuffer by merging all content of the given one together. +func Compact(mb MultiBuffer) MultiBuffer { + if len(mb) == 0 { + return mb + } + + mb2 := make(MultiBuffer, 0, len(mb)) + last := mb[0] + + for i := 1; i < len(mb); i++ { + curr := mb[i] + if last.Len()+curr.Len() > Size { + mb2 = append(mb2, last) + last = curr + } else { + common.Must2(last.ReadFrom(curr)) + curr.Release() + } + } + + mb2 = append(mb2, last) + return mb2 +} + +// SplitFirst splits the first Buffer from the beginning of the MultiBuffer. +func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) { + if len(mb) == 0 { + return mb, nil + } + + b := mb[0] + mb[0] = nil + mb = mb[1:] + return mb, b +} + +// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes. +func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) { + if len(mb) == 0 { + return mb, nil + } + + if mb[0].Len() > size { + b := New() + copy(b.Extend(size), mb[0].BytesTo(size)) + mb[0].Advance(size) + return mb, MultiBuffer{b} + } + + totalBytes := int32(0) + var r MultiBuffer + endIndex := -1 + for i := range mb { + if totalBytes+mb[i].Len() > size { + endIndex = i + break + } + totalBytes += mb[i].Len() + r = append(r, mb[i]) + mb[i] = nil + } + if endIndex == -1 { + // To reuse mb array + mb = mb[:0] + } else { + mb = mb[endIndex:] + } + return mb, r +} + +// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer. +func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) { + for { + mb2, b := SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + + _, err := writer.Write(b.Bytes()) + b.Release() + if err != nil { + return mb, err + } + } + + return nil, nil +} + +// Len returns the total number of bytes in the MultiBuffer. +func (mb MultiBuffer) Len() int32 { + if mb == nil { + return 0 + } + + size := int32(0) + for _, b := range mb { + size += b.Len() + } + return size +} + +// IsEmpty return true if the MultiBuffer has no content. +func (mb MultiBuffer) IsEmpty() bool { + for _, b := range mb { + if !b.IsEmpty() { + return false + } + } + return true +} + +// String returns the content of the MultiBuffer in string. +func (mb MultiBuffer) String() string { + v := make([]interface{}, len(mb)) + for i, b := range mb { + v[i] = b + } + return serial.Concat(v...) +} + +// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer. +type MultiBufferContainer struct { + MultiBuffer +} + +// Read implements io.Reader. +func (c *MultiBufferContainer) Read(b []byte) (int, error) { + if c.MultiBuffer.IsEmpty() { + return 0, io.EOF + } + + mb, nBytes := SplitBytes(c.MultiBuffer, b) + c.MultiBuffer = mb + return nBytes, nil +} + +// ReadMultiBuffer implements Reader. +func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) { + mb := c.MultiBuffer + c.MultiBuffer = nil + return mb, nil +} + +// Write implements io.Writer. +func (c *MultiBufferContainer) Write(b []byte) (int, error) { + c.MultiBuffer = MergeBytes(c.MultiBuffer, b) + return len(b), nil +} + +// WriteMultiBuffer implement Writer. +func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error { + mb, _ := MergeMulti(c.MultiBuffer, b) + c.MultiBuffer = mb + return nil +} + +// Close implement io.Closer. +func (c *MultiBufferContainer) Close() error { + c.MultiBuffer = ReleaseMulti(c.MultiBuffer) + return nil +} diff --git a/common/buf/multi_buffer_test.go b/common/buf/multi_buffer_test.go new file mode 100644 index 00000000..1cff7758 --- /dev/null +++ b/common/buf/multi_buffer_test.go @@ -0,0 +1,190 @@ +package buf_test + +import ( + "bytes" + "crypto/rand" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/buf" +) + +func TestMultiBufferRead(t *testing.T) { + b1 := New() + common.Must2(b1.WriteString("ab")) + + b2 := New() + common.Must2(b2.WriteString("cd")) + mb := MultiBuffer{b1, b2} + + bs := make([]byte, 32) + _, nBytes := SplitBytes(mb, bs) + if nBytes != 4 { + t.Error("expect 4 bytes split, but got ", nBytes) + } + if r := cmp.Diff(bs[:nBytes], []byte("abcd")); r != "" { + t.Error(r) + } +} + +func TestMultiBufferAppend(t *testing.T) { + var mb MultiBuffer + b := New() + common.Must2(b.WriteString("ab")) + mb = append(mb, b) + if mb.Len() != 2 { + t.Error("expected length 2, but got ", mb.Len()) + } +} + +func TestMultiBufferSliceBySizeLarge(t *testing.T) { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + + mb := MergeBytes(nil, lb) + + mb, mb2 := SplitSize(mb, 1024) + if mb2.Len() != 1024 { + t.Error("expect length 1024, but got ", mb2.Len()) + } + if mb.Len() != 7*1024 { + t.Error("expect length 7*1024, but got ", mb.Len()) + } + + mb, mb3 := SplitSize(mb, 7*1024) + if mb3.Len() != 7*1024 { + t.Error("expect length 7*1024, but got", mb.Len()) + } + + if !mb.IsEmpty() { + t.Error("expect empty buffer, but got ", mb.Len()) + } +} + +func TestMultiBufferSplitFirst(t *testing.T) { + b1 := New() + b1.WriteString("b1") + + b2 := New() + b2.WriteString("b2") + + b3 := New() + b3.WriteString("b3") + + var mb MultiBuffer + mb = append(mb, b1, b2, b3) + + mb, c1 := SplitFirst(mb) + if diff := cmp.Diff(b1.String(), c1.String()); diff != "" { + t.Error(diff) + } + + mb, c2 := SplitFirst(mb) + if diff := cmp.Diff(b2.String(), c2.String()); diff != "" { + t.Error(diff) + } + + mb, c3 := SplitFirst(mb) + if diff := cmp.Diff(b3.String(), c3.String()); diff != "" { + t.Error(diff) + } + + if !mb.IsEmpty() { + t.Error("expect empty buffer, but got ", mb.String()) + } +} + +func TestMultiBufferReadAllToByte(t *testing.T) { + { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + rd := bytes.NewBuffer(lb) + b, err := ReadAllToBytes(rd) + common.Must(err) + + if l := len(b); l != 8*1024 { + t.Error("unexpceted length from ReadAllToBytes", l) + } + } + { + const dat = "data/test_MultiBufferReadAllToByte.dat" + f, err := os.Open(dat) + common.Must(err) + + buf2, err := ReadAllToBytes(f) + common.Must(err) + f.Close() + + cnt, err := ioutil.ReadFile(dat) + common.Must(err) + + if d := cmp.Diff(buf2, cnt); d != "" { + t.Error("fail to read from file: ", d) + } + } +} + +func TestMultiBufferCopy(t *testing.T) { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + reader := bytes.NewBuffer(lb) + + mb, err := ReadFrom(reader) + common.Must(err) + + lbdst := make([]byte, 8*1024) + mb.Copy(lbdst) + + if d := cmp.Diff(lb, lbdst); d != "" { + t.Error("unexpceted different from MultiBufferCopy ", d) + } +} + +func TestSplitFirstBytes(t *testing.T) { + a := New() + common.Must2(a.WriteString("ab")) + b := New() + common.Must2(b.WriteString("bc")) + + mb := MultiBuffer{a, b} + + o := make([]byte, 2) + _, cnt := SplitFirstBytes(mb, o) + if cnt != 2 { + t.Error("unexpected cnt from SplitFirstBytes ", cnt) + } + if d := cmp.Diff(string(o), "ab"); d != "" { + t.Error("unexpected splited result from SplitFirstBytes ", d) + } +} + +func TestCompact(t *testing.T) { + a := New() + common.Must2(a.WriteString("ab")) + b := New() + common.Must2(b.WriteString("bc")) + + mb := MultiBuffer{a, b} + cmb := Compact(mb) + + if w := cmb.String(); w != "abbc" { + t.Error("unexpected Compact result ", w) + } +} + +func BenchmarkSplitBytes(b *testing.B) { + var mb MultiBuffer + raw := make([]byte, Size) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + buffer := StackNew() + buffer.Extend(Size) + mb = append(mb, &buffer) + mb, _ = SplitBytes(mb, raw) + } +} diff --git a/common/buf/reader.go b/common/buf/reader.go new file mode 100644 index 00000000..432dd1aa --- /dev/null +++ b/common/buf/reader.go @@ -0,0 +1,174 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" +) + +func readOneUDP(r io.Reader) (*Buffer, error) { + b := New() + for i := 0; i < 64; i++ { + _, err := b.ReadFrom(r) + if !b.IsEmpty() { + return b, nil + } + if err != nil { + b.Release() + return nil, err + } + } + + b.Release() + return nil, newError("Reader returns too many empty payloads.") +} + +// ReadBuffer reads a Buffer from the given reader. +func ReadBuffer(r io.Reader) (*Buffer, error) { + b := New() + n, err := b.ReadFrom(r) + if n > 0 { + return b, err + } + b.Release() + return nil, err +} + +// BufferedReader is a Reader that keeps its internal buffer. +type BufferedReader struct { + // Reader is the underlying reader to be read from + Reader Reader + // Buffer is the internal buffer to be read from first + Buffer MultiBuffer + // Spliter is a function to read bytes from MultiBuffer + Spliter func(MultiBuffer, []byte) (MultiBuffer, int) +} + +// BufferedBytes returns the number of bytes that is cached in this reader. +func (r *BufferedReader) BufferedBytes() int32 { + return r.Buffer.Len() +} + +// ReadByte implements io.ByteReader. +func (r *BufferedReader) ReadByte() (byte, error) { + var b [1]byte + _, err := r.Read(b[:]) + return b[0], err +} + +// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader. +func (r *BufferedReader) Read(b []byte) (int, error) { + spliter := r.Spliter + if spliter == nil { + spliter = SplitBytes + } + + if !r.Buffer.IsEmpty() { + buffer, nBytes := spliter(r.Buffer, b) + r.Buffer = buffer + if r.Buffer.IsEmpty() { + r.Buffer = nil + } + return nBytes, nil + } + + mb, err := r.Reader.ReadMultiBuffer() + if err != nil { + return 0, err + } + + mb, nBytes := spliter(mb, b) + if !mb.IsEmpty() { + r.Buffer = mb + } + return nBytes, nil +} + +// ReadMultiBuffer implements Reader. +func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) { + if !r.Buffer.IsEmpty() { + mb := r.Buffer + r.Buffer = nil + return mb, nil + } + + return r.Reader.ReadMultiBuffer() +} + +// ReadAtMost returns a MultiBuffer with at most size. +func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) { + if r.Buffer.IsEmpty() { + mb, err := r.Reader.ReadMultiBuffer() + if mb.IsEmpty() && err != nil { + return nil, err + } + r.Buffer = mb + } + + rb, mb := SplitSize(r.Buffer, size) + r.Buffer = rb + if r.Buffer.IsEmpty() { + r.Buffer = nil + } + return mb, nil +} + +func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) { + mbWriter := NewWriter(writer) + var sc SizeCounter + if r.Buffer != nil { + sc.Size = int64(r.Buffer.Len()) + if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil { + return 0, err + } + r.Buffer = nil + } + + err := Copy(r.Reader, mbWriter, CountSize(&sc)) + return sc.Size, err +} + +// WriteTo implements io.WriterTo. +func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) { + nBytes, err := r.writeToInternal(writer) + if errors.Cause(err) == io.EOF { + return nBytes, nil + } + return nBytes, err +} + +// Interrupt implements common.Interruptible. +func (r *BufferedReader) Interrupt() { + common.Interrupt(r.Reader) +} + +// Close implements io.Closer. +func (r *BufferedReader) Close() error { + return common.Close(r.Reader) +} + +// SingleReader is a Reader that read one Buffer every time. +type SingleReader struct { + io.Reader +} + +// ReadMultiBuffer implements Reader. +func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) { + b, err := ReadBuffer(r.Reader) + return MultiBuffer{b}, err +} + +// PacketReader is a Reader that read one Buffer every time. +type PacketReader struct { + io.Reader +} + +// ReadMultiBuffer implements Reader. +func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) { + b, err := readOneUDP(r.Reader) + if err != nil { + return nil, err + } + return MultiBuffer{b}, nil +} diff --git a/common/buf/reader_test.go b/common/buf/reader_test.go new file mode 100644 index 00000000..800201db --- /dev/null +++ b/common/buf/reader_test.go @@ -0,0 +1,131 @@ +package buf_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +func TestBytesReaderWriteTo(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + reader := &BufferedReader{Reader: pReader} + b1 := New() + b1.WriteString("abc") + b2 := New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(MultiBuffer{b1, b2})) + pWriter.Close() + + pReader2, pWriter2 := pipe.New(pipe.WithSizeLimit(1024)) + writer := NewBufferedWriter(pWriter2) + writer.SetBuffered(false) + + nBytes, err := io.Copy(writer, reader) + common.Must(err) + if nBytes != 6 { + t.Error("copy: ", nBytes) + } + + mb, err := pReader2.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != "abcefg" { + t.Error("content: ", s) + } +} + +func TestBytesReaderMultiBuffer(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + reader := &BufferedReader{Reader: pReader} + b1 := New() + b1.WriteString("abc") + b2 := New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(MultiBuffer{b1, b2})) + pWriter.Close() + + mbReader := NewReader(reader) + mb, err := mbReader.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != "abcefg" { + t.Error("content: ", s) + } +} + +func TestReadByte(t *testing.T) { + sr := strings.NewReader("abcd") + reader := &BufferedReader{ + Reader: NewReader(sr), + } + b, err := reader.ReadByte() + common.Must(err) + if b != 'a' { + t.Error("unexpected byte: ", b, " want a") + } + if reader.BufferedBytes() != 3 { // 3 bytes left in buffer + t.Error("unexpected buffered Bytes: ", reader.BufferedBytes()) + } + + nBytes, err := reader.WriteTo(DiscardBytes) + common.Must(err) + if nBytes != 3 { + t.Error("unexpect bytes written: ", nBytes) + } +} + +func TestReadBuffer(t *testing.T) { + { + sr := strings.NewReader("abcd") + buf, err := ReadBuffer(sr) + common.Must(err) + + if s := buf.String(); s != "abcd" { + t.Error("unexpected str: ", s, " want abcd") + } + buf.Release() + } +} + +func TestReadAtMost(t *testing.T) { + sr := strings.NewReader("abcd") + reader := &BufferedReader{ + Reader: NewReader(sr), + } + + mb, err := reader.ReadAtMost(3) + common.Must(err) + if s := mb.String(); s != "abc" { + t.Error("unexpected read result: ", s) + } + + nBytes, err := reader.WriteTo(DiscardBytes) + common.Must(err) + if nBytes != 1 { + t.Error("unexpect bytes written: ", nBytes) + } +} + +func TestPacketReader_ReadMultiBuffer(t *testing.T) { + const alpha = "abcefg" + buf := bytes.NewBufferString(alpha) + reader := &PacketReader{buf} + mb, err := reader.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != alpha { + t.Error("content: ", s) + } +} + +func TestReaderInterface(t *testing.T) { + _ = (io.Reader)(new(ReadVReader)) + _ = (Reader)(new(ReadVReader)) + + _ = (Reader)(new(BufferedReader)) + _ = (io.Reader)(new(BufferedReader)) + _ = (io.ByteReader)(new(BufferedReader)) + _ = (io.WriterTo)(new(BufferedReader)) +} diff --git a/common/buf/readv_posix.go b/common/buf/readv_posix.go new file mode 100644 index 00000000..9fbee794 --- /dev/null +++ b/common/buf/readv_posix.go @@ -0,0 +1,47 @@ +// +build !windows +// +build !wasm +// +build !illumos + +package buf + +import ( + "syscall" + "unsafe" +) + +type posixReader struct { + iovecs []syscall.Iovec +} + +func (r *posixReader) Init(bs []*Buffer) { + iovecs := r.iovecs + if iovecs == nil { + iovecs = make([]syscall.Iovec, 0, len(bs)) + } + for idx, b := range bs { + iovecs = append(iovecs, syscall.Iovec{ + Base: &(b.v[0]), + }) + iovecs[idx].SetLen(int(Size)) + } + r.iovecs = iovecs +} + +func (r *posixReader) Read(fd uintptr) int32 { + n, _, e := syscall.Syscall(syscall.SYS_READV, fd, uintptr(unsafe.Pointer(&r.iovecs[0])), uintptr(len(r.iovecs))) + if e != 0 { + return -1 + } + return int32(n) +} + +func (r *posixReader) Clear() { + for idx := range r.iovecs { + r.iovecs[idx].Base = nil + } + r.iovecs = r.iovecs[:0] +} + +func newMultiReader() multiReader { + return &posixReader{} +} diff --git a/common/buf/readv_reader.go b/common/buf/readv_reader.go new file mode 100644 index 00000000..0107d7a7 --- /dev/null +++ b/common/buf/readv_reader.go @@ -0,0 +1,150 @@ +// +build !wasm + +package buf + +import ( + "io" + "runtime" + "syscall" + + "github.com/xtls/xray-core/v1/common/platform" +) + +type allocStrategy struct { + current uint32 +} + +func (s *allocStrategy) Current() uint32 { + return s.current +} + +func (s *allocStrategy) Adjust(n uint32) { + if n >= s.current { + s.current *= 2 + } else { + s.current = n + } + + if s.current > 8 { + s.current = 8 + } + + if s.current == 0 { + s.current = 1 + } +} + +func (s *allocStrategy) Alloc() []*Buffer { + bs := make([]*Buffer, s.current) + for i := range bs { + bs[i] = New() + } + return bs +} + +type multiReader interface { + Init([]*Buffer) + Read(fd uintptr) int32 + Clear() +} + +// ReadVReader is a Reader that uses readv(2) syscall to read data. +type ReadVReader struct { + io.Reader + rawConn syscall.RawConn + mr multiReader + alloc allocStrategy +} + +// NewReadVReader creates a new ReadVReader. +func NewReadVReader(reader io.Reader, rawConn syscall.RawConn) *ReadVReader { + return &ReadVReader{ + Reader: reader, + rawConn: rawConn, + alloc: allocStrategy{ + current: 1, + }, + mr: newMultiReader(), + } +} + +func (r *ReadVReader) readMulti() (MultiBuffer, error) { + bs := r.alloc.Alloc() + + r.mr.Init(bs) + var nBytes int32 + err := r.rawConn.Read(func(fd uintptr) bool { + n := r.mr.Read(fd) + if n < 0 { + return false + } + + nBytes = n + return true + }) + r.mr.Clear() + + if err != nil { + ReleaseMulti(MultiBuffer(bs)) + return nil, err + } + + if nBytes == 0 { + ReleaseMulti(MultiBuffer(bs)) + return nil, io.EOF + } + + nBuf := 0 + for nBuf < len(bs) { + if nBytes <= 0 { + break + } + end := nBytes + if end > Size { + end = Size + } + bs[nBuf].end = end + nBytes -= end + nBuf++ + } + + for i := nBuf; i < len(bs); i++ { + bs[i].Release() + bs[i] = nil + } + + return MultiBuffer(bs[:nBuf]), nil +} + +// ReadMultiBuffer implements Reader. +func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) { + if r.alloc.Current() == 1 { + b, err := ReadBuffer(r.Reader) + if b.IsFull() { + r.alloc.Adjust(1) + } + return MultiBuffer{b}, err + } + + mb, err := r.readMulti() + if err != nil { + return nil, err + } + r.alloc.Adjust(uint32(len(mb))) + return mb, nil +} + +var useReadv = true + +func init() { + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + value := platform.NewEnvFlag("xray.buf.readv").GetValue(func() string { return defaultFlagValue }) + switch value { + case defaultFlagValue, "auto": + if (runtime.GOARCH == "386" || runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x") && (runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows") { + useReadv = true + } + case "enable": + useReadv = true + } +} diff --git a/common/buf/readv_reader_wasm.go b/common/buf/readv_reader_wasm.go new file mode 100644 index 00000000..a4d9fa67 --- /dev/null +++ b/common/buf/readv_reader_wasm.go @@ -0,0 +1,14 @@ +// +build wasm + +package buf + +import ( + "io" + "syscall" +) + +const useReadv = false + +func NewReadVReader(reader io.Reader, rawConn syscall.RawConn) Reader { + panic("not implemented") +} diff --git a/common/buf/readv_test.go b/common/buf/readv_test.go new file mode 100644 index 00000000..553e9b93 --- /dev/null +++ b/common/buf/readv_test.go @@ -0,0 +1,72 @@ +// +build !wasm + +package buf_test + +import ( + "crypto/rand" + "net" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "golang.org/x/sync/errgroup" +) + +func TestReadvReader(t *testing.T) { + tcpServer := &tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + conn, err := net.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + const size = 8192 + data := make([]byte, 8192) + common.Must2(rand.Read(data)) + + var errg errgroup.Group + errg.Go(func() error { + writer := NewWriter(conn) + mb := MergeBytes(nil, data) + + return writer.WriteMultiBuffer(mb) + }) + + defer func() { + if err := errg.Wait(); err != nil { + t.Error(err) + } + }() + + rawConn, err := conn.(*net.TCPConn).SyscallConn() + common.Must(err) + + reader := NewReadVReader(conn, rawConn) + var rmb MultiBuffer + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + t.Fatal("unexpected error: ", err) + } + rmb, _ = MergeMulti(rmb, mb) + if rmb.Len() == size { + break + } + } + + rdata := make([]byte, size) + SplitBytes(rmb, rdata) + + if r := cmp.Diff(data, rdata); r != "" { + t.Fatal(r) + } +} diff --git a/common/buf/readv_unix.go b/common/buf/readv_unix.go new file mode 100644 index 00000000..8c8bb970 --- /dev/null +++ b/common/buf/readv_unix.go @@ -0,0 +1,36 @@ +// +build illumos + +package buf + +import "golang.org/x/sys/unix" + +type unixReader struct { + iovs [][]byte +} + +func (r *unixReader) Init(bs []*Buffer) { + iovs := r.iovs + if iovs == nil { + iovs = make([][]byte, 0, len(bs)) + } + for _, b := range bs { + iovs = append(iovs, b.v) + } + r.iovs = iovs +} + +func (r *unixReader) Read(fd uintptr) int32 { + n, e := unix.Readv(int(fd), r.iovs) + if e != nil { + return -1 + } + return int32(n) +} + +func (r *unixReader) Clear() { + r.iovs = r.iovs[:0] +} + +func newMultiReader() multiReader { + return &unixReader{} +} diff --git a/common/buf/readv_windows.go b/common/buf/readv_windows.go new file mode 100644 index 00000000..a812ee04 --- /dev/null +++ b/common/buf/readv_windows.go @@ -0,0 +1,39 @@ +package buf + +import ( + "syscall" +) + +type windowsReader struct { + bufs []syscall.WSABuf +} + +func (r *windowsReader) Init(bs []*Buffer) { + if r.bufs == nil { + r.bufs = make([]syscall.WSABuf, 0, len(bs)) + } + for _, b := range bs { + r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]}) + } +} + +func (r *windowsReader) Clear() { + for idx := range r.bufs { + r.bufs[idx].Buf = nil + } + r.bufs = r.bufs[:0] +} + +func (r *windowsReader) Read(fd uintptr) int32 { + var nBytes uint32 + var flags uint32 + err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil) + if err != nil { + return -1 + } + return int32(nBytes) +} + +func newMultiReader() multiReader { + return new(windowsReader) +} diff --git a/common/buf/writer.go b/common/buf/writer.go new file mode 100644 index 00000000..169c9bb8 --- /dev/null +++ b/common/buf/writer.go @@ -0,0 +1,262 @@ +package buf + +import ( + "io" + "net" + "sync" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" +) + +// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer. +type BufferToBytesWriter struct { + io.Writer + + cache [][]byte +} + +// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer. +func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error { + defer ReleaseMulti(mb) + + size := mb.Len() + if size == 0 { + return nil + } + + if len(mb) == 1 { + return WriteAllBytes(w.Writer, mb[0].Bytes()) + } + + if cap(w.cache) < len(mb) { + w.cache = make([][]byte, 0, len(mb)) + } + + bs := w.cache + for _, b := range mb { + bs = append(bs, b.Bytes()) + } + + defer func() { + for idx := range bs { + bs[idx] = nil + } + }() + + nb := net.Buffers(bs) + + for size > 0 { + n, err := nb.WriteTo(w.Writer) + if err != nil { + return err + } + size -= int32(n) + } + + return nil +} + +// ReadFrom implements io.ReaderFrom. +func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) { + var sc SizeCounter + err := Copy(NewReader(reader), w, CountSize(&sc)) + return sc.Size, err +} + +// BufferedWriter is a Writer with internal buffer. +type BufferedWriter struct { + sync.Mutex + writer Writer + buffer *Buffer + buffered bool +} + +// NewBufferedWriter creates a new BufferedWriter. +func NewBufferedWriter(writer Writer) *BufferedWriter { + return &BufferedWriter{ + writer: writer, + buffer: New(), + buffered: true, + } +} + +// WriteByte implements io.ByteWriter. +func (w *BufferedWriter) WriteByte(c byte) error { + return common.Error2(w.Write([]byte{c})) +} + +// Write implements io.Writer. +func (w *BufferedWriter) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + + w.Lock() + defer w.Unlock() + + if !w.buffered { + if writer, ok := w.writer.(io.Writer); ok { + return writer.Write(b) + } + } + + totalBytes := 0 + for len(b) > 0 { + if w.buffer == nil { + w.buffer = New() + } + + nBytes, err := w.buffer.Write(b) + totalBytes += nBytes + if err != nil { + return totalBytes, err + } + if !w.buffered || w.buffer.IsFull() { + if err := w.flushInternal(); err != nil { + return totalBytes, err + } + } + b = b[nBytes:] + } + + return totalBytes, nil +} + +// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer. +func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error { + if b.IsEmpty() { + return nil + } + + w.Lock() + defer w.Unlock() + + if !w.buffered { + return w.writer.WriteMultiBuffer(b) + } + + reader := MultiBufferContainer{ + MultiBuffer: b, + } + defer reader.Close() + + for !reader.MultiBuffer.IsEmpty() { + if w.buffer == nil { + w.buffer = New() + } + common.Must2(w.buffer.ReadFrom(&reader)) + if w.buffer.IsFull() { + if err := w.flushInternal(); err != nil { + return err + } + } + } + + return nil +} + +// Flush flushes buffered content into underlying writer. +func (w *BufferedWriter) Flush() error { + w.Lock() + defer w.Unlock() + + return w.flushInternal() +} + +func (w *BufferedWriter) flushInternal() error { + if w.buffer.IsEmpty() { + return nil + } + + b := w.buffer + w.buffer = nil + + if writer, ok := w.writer.(io.Writer); ok { + err := WriteAllBytes(writer, b.Bytes()) + b.Release() + return err + } + + return w.writer.WriteMultiBuffer(MultiBuffer{b}) +} + +// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer. +func (w *BufferedWriter) SetBuffered(f bool) error { + w.Lock() + defer w.Unlock() + + w.buffered = f + if !f { + return w.flushInternal() + } + return nil +} + +// ReadFrom implements io.ReaderFrom. +func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) { + if err := w.SetBuffered(false); err != nil { + return 0, err + } + + var sc SizeCounter + err := Copy(NewReader(reader), w, CountSize(&sc)) + return sc.Size, err +} + +// Close implements io.Closable. +func (w *BufferedWriter) Close() error { + if err := w.Flush(); err != nil { + return err + } + return common.Close(w.writer) +} + +// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer. +type SequentialWriter struct { + io.Writer +} + +// WriteMultiBuffer implements Writer. +func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error { + mb, err := WriteMultiBuffer(w.Writer, mb) + ReleaseMulti(mb) + return err +} + +type noOpWriter byte + +func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error { + ReleaseMulti(b) + return nil +} + +func (noOpWriter) Write(b []byte) (int, error) { + return len(b), nil +} + +func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) { + b := New() + defer b.Release() + + totalBytes := int64(0) + for { + b.Clear() + _, err := b.ReadFrom(reader) + totalBytes += int64(b.Len()) + if err != nil { + if errors.Cause(err) == io.EOF { + return totalBytes, nil + } + return totalBytes, err + } + } +} + +var ( + // Discard is a Writer that swallows all contents written in. + Discard Writer = noOpWriter(0) + + // DiscardBytes is an io.Writer that swallows all contents written in. + DiscardBytes io.Writer = noOpWriter(0) +) diff --git a/common/buf/writer_test.go b/common/buf/writer_test.go new file mode 100644 index 00000000..f28fef02 --- /dev/null +++ b/common/buf/writer_test.go @@ -0,0 +1,98 @@ +package buf_test + +import ( + "bufio" + "bytes" + "crypto/rand" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +func TestWriter(t *testing.T) { + lb := New() + common.Must2(lb.ReadFrom(rand.Reader)) + + expectedBytes := append([]byte(nil), lb.Bytes()...) + + writeBuffer := bytes.NewBuffer(make([]byte, 0, 1024*1024)) + + writer := NewBufferedWriter(NewWriter(writeBuffer)) + writer.SetBuffered(false) + common.Must(writer.WriteMultiBuffer(MultiBuffer{lb})) + common.Must(writer.Flush()) + + if r := cmp.Diff(expectedBytes, writeBuffer.Bytes()); r != "" { + t.Error(r) + } +} + +func TestBytesWriterReadFrom(t *testing.T) { + const size = 50000 + pReader, pWriter := pipe.New(pipe.WithSizeLimit(size)) + reader := bufio.NewReader(io.LimitReader(rand.Reader, size)) + writer := NewBufferedWriter(pWriter) + writer.SetBuffered(false) + nBytes, err := reader.WriteTo(writer) + if nBytes != size { + t.Fatal("unexpected size of bytes written: ", nBytes) + } + if err != nil { + t.Fatal("expect success, but actually error: ", err.Error()) + } + + mb, err := pReader.ReadMultiBuffer() + common.Must(err) + if mb.Len() != size { + t.Fatal("unexpected size read: ", mb.Len()) + } +} + +func TestDiscardBytes(t *testing.T) { + b := New() + common.Must2(b.ReadFullFrom(rand.Reader, Size)) + + nBytes, err := io.Copy(DiscardBytes, b) + common.Must(err) + if nBytes != Size { + t.Error("copy size: ", nBytes) + } +} + +func TestDiscardBytesMultiBuffer(t *testing.T) { + const size = 10240*1024 + 1 + buffer := bytes.NewBuffer(make([]byte, 0, size)) + common.Must2(buffer.ReadFrom(io.LimitReader(rand.Reader, size))) + + r := NewReader(buffer) + nBytes, err := io.Copy(DiscardBytes, &BufferedReader{Reader: r}) + common.Must(err) + if nBytes != size { + t.Error("copy size: ", nBytes) + } +} + +func TestWriterInterface(t *testing.T) { + { + var writer interface{} = (*BufferToBytesWriter)(nil) + switch writer.(type) { + case Writer, io.Writer, io.ReaderFrom: + default: + t.Error("BufferToBytesWriter is not Writer, io.Writer or io.ReaderFrom") + } + } + + { + var writer interface{} = (*BufferedWriter)(nil) + switch writer.(type) { + case Writer, io.Writer, io.ReaderFrom, io.ByteWriter: + default: + t.Error("BufferedWriter is not Writer, io.Writer, io.ReaderFrom or io.ByteWriter") + } + } +} diff --git a/common/bytespool/pool.go b/common/bytespool/pool.go new file mode 100644 index 00000000..6f632d52 --- /dev/null +++ b/common/bytespool/pool.go @@ -0,0 +1,72 @@ +package bytespool + +import "sync" + +func createAllocFunc(size int32) func() interface{} { + return func() interface{} { + return make([]byte, size) + } +} + +// The following parameters controls the size of buffer pools. +// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one. +// Package buf is guaranteed to not use buffers larger than the largest pool. +// Other packets may use larger buffers. +const ( + numPools = 4 + sizeMulti = 4 +) + +var ( + pool [numPools]sync.Pool + poolSize [numPools]int32 +) + +func init() { + size := int32(2048) + for i := 0; i < numPools; i++ { + pool[i] = sync.Pool{ + New: createAllocFunc(size), + } + poolSize[i] = size + size *= sizeMulti + } +} + +// GetPool returns a sync.Pool that generates bytes array with at least the given size. +// It may return nil if no such pool exists. +// +// xray:api:stable +func GetPool(size int32) *sync.Pool { + for idx, ps := range poolSize { + if size <= ps { + return &pool[idx] + } + } + return nil +} + +// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048. +// +// xray:api:stable +func Alloc(size int32) []byte { + pool := GetPool(size) + if pool != nil { + return pool.Get().([]byte) + } + return make([]byte, size) +} + +// Free puts a byte slice into the internal pool. +// +// xray:api:stable +func Free(b []byte) { + size := int32(cap(b)) + b = b[0:cap(b)] + for i := numPools - 1; i >= 0; i-- { + if size >= poolSize[i] { + pool[i].Put(b) + return + } + } +} diff --git a/common/cmdarg/cmdarg.go b/common/cmdarg/cmdarg.go new file mode 100644 index 00000000..f524eb37 --- /dev/null +++ b/common/cmdarg/cmdarg.go @@ -0,0 +1,16 @@ +package cmdarg + +import "strings" + +// Arg is used by flag to accept multiple argument. +type Arg []string + +func (c *Arg) String() string { + return strings.Join([]string(*c), " ") +} + +// Set is the method flag package calls +func (c *Arg) Set(value string) error { + *c = append(*c, value) + return nil +} diff --git a/common/common.go b/common/common.go new file mode 100644 index 00000000..6173efde --- /dev/null +++ b/common/common.go @@ -0,0 +1,158 @@ +// Package common contains common utilities that are shared among other packages. +// See each sub-package for detail. +package common + +import ( + "fmt" + "go/build" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/xtls/xray-core/v1/common/errors" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +var ( + // ErrNoClue is for the situation that existing information is not enough to make a decision. For example, Router may return this error when there is no suitable route. + ErrNoClue = errors.New("not enough information for making a decision") +) + +// Must panics if err is not nil. +func Must(err error) { + if err != nil { + panic(err) + } +} + +// Must2 panics if the second parameter is not nil, otherwise returns the first parameter. +func Must2(v interface{}, err error) interface{} { + Must(err) + return v +} + +// Error2 returns the err from the 2nd parameter. +func Error2(v interface{}, err error) error { + return err +} + +// envFile returns the name of the Go environment configuration file. +// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 +func envFile() (string, error) { + if file := os.Getenv("GOENV"); file != "" { + if file == "off" { + return "", fmt.Errorf("GOENV=off") + } + return file, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + if dir == "" { + return "", fmt.Errorf("missing user-config dir") + } + return filepath.Join(dir, "go", "env"), nil +} + +// GetRuntimeEnv returns the value of runtime environment variable, +// that is set by running following command: `go env -w key=value`. +func GetRuntimeEnv(key string) (string, error) { + file, err := envFile() + if err != nil { + return "", err + } + if file == "" { + return "", fmt.Errorf("missing runtime env file") + } + var data []byte + var runtimeEnv string + data, readErr := ioutil.ReadFile(file) + if readErr != nil { + return "", readErr + } + envStrings := strings.Split(string(data), "\n") + for _, envItem := range envStrings { + envItem = strings.TrimSuffix(envItem, "\r") + envKeyValue := strings.Split(envItem, "=") + if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { + runtimeEnv = strings.TrimSpace(envKeyValue[1]) + } + } + return runtimeEnv, nil +} + +// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. +func GetGOBIN() string { + // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` + GOBIN := os.Getenv("GOBIN") + if GOBIN == "" { + var err error + // The one set by user by running `go env -w GOBIN=/path` + GOBIN, err = GetRuntimeEnv("GOBIN") + if err != nil { + // The default one that Golang uses + return filepath.Join(build.Default.GOPATH, "bin") + } + if GOBIN == "" { + return filepath.Join(build.Default.GOPATH, "bin") + } + return GOBIN + } + return GOBIN +} + +// GetGOPATH returns GOPATH environment variable as a string. It will NOT be empty. +func GetGOPATH() string { + // The one set by user explicitly by `export GOPATH=/path` or `env GOPATH=/path command` + GOPATH := os.Getenv("GOPATH") + if GOPATH == "" { + var err error + // The one set by user by running `go env -w GOPATH=/path` + GOPATH, err = GetRuntimeEnv("GOPATH") + if err != nil { + // The default one that Golang uses + return build.Default.GOPATH + } + if GOPATH == "" { + return build.Default.GOPATH + } + return GOPATH + } + return GOPATH +} + +// GetModuleName returns the value of module in `go.mod` file. +func GetModuleName(pathToProjectRoot string) (string, error) { + var moduleName string + loopPath := pathToProjectRoot + for { + if idx := strings.LastIndex(loopPath, string(filepath.Separator)); idx >= 0 { + gomodPath := filepath.Join(loopPath, "go.mod") + gomodBytes, err := ioutil.ReadFile(gomodPath) + if err != nil { + loopPath = loopPath[:idx] + continue + } + + gomodContent := string(gomodBytes) + moduleIdx := strings.Index(gomodContent, "module ") + newLineIdx := strings.Index(gomodContent, "\n") + + if moduleIdx >= 0 { + if newLineIdx >= 0 { + moduleName = strings.TrimSpace(gomodContent[moduleIdx+6 : newLineIdx]) + moduleName = strings.TrimSuffix(moduleName, "\r") + } else { + moduleName = strings.TrimSpace(gomodContent[moduleIdx+6:]) + } + return moduleName, nil + } + return "", fmt.Errorf("can not get module path in `%s`", gomodPath) + } + break + } + return moduleName, fmt.Errorf("no `go.mod` file in every parent directory of `%s`", pathToProjectRoot) +} diff --git a/common/common_test.go b/common/common_test.go new file mode 100644 index 00000000..b8992887 --- /dev/null +++ b/common/common_test.go @@ -0,0 +1,44 @@ +package common_test + +import ( + "errors" + "testing" + + . "github.com/xtls/xray-core/v1/common" +) + +func TestMust(t *testing.T) { + hasPanic := func(f func()) (ret bool) { + defer func() { + if r := recover(); r != nil { + ret = true + } + }() + f() + return false + } + + testCases := []struct { + Input func() + Panic bool + }{ + { + Panic: true, + Input: func() { Must(func() error { return errors.New("test error") }()) }, + }, + { + Panic: true, + Input: func() { Must2(func() (int, error) { return 0, errors.New("test error") }()) }, + }, + { + Panic: false, + Input: func() { Must(func() error { return nil }()) }, + }, + } + + for idx, test := range testCases { + if hasPanic(test.Input) != test.Panic { + t.Error("test case #", idx, " expect panic ", test.Panic, " but actually not") + } + } +} diff --git a/common/crypto/aes.go b/common/crypto/aes.go new file mode 100644 index 00000000..547b036d --- /dev/null +++ b/common/crypto/aes.go @@ -0,0 +1,40 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + + "github.com/xtls/xray-core/v1/common" +) + +// NewAesDecryptionStream creates a new AES encryption stream based on given key and IV. +// Caller must ensure the length of key and IV is either 16, 24 or 32 bytes. +func NewAesDecryptionStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCFBDecrypter) +} + +// NewAesEncryptionStream creates a new AES description stream based on given key and IV. +// Caller must ensure the length of key and IV is either 16, 24 or 32 bytes. +func NewAesEncryptionStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCFBEncrypter) +} + +func NewAesStreamMethod(key []byte, iv []byte, f func(cipher.Block, []byte) cipher.Stream) cipher.Stream { + aesBlock, err := aes.NewCipher(key) + common.Must(err) + return f(aesBlock, iv) +} + +// NewAesCTRStream creates a stream cipher based on AES-CTR. +func NewAesCTRStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCTR) +} + +// NewAesGcm creates a AEAD cipher based on AES-GCM. +func NewAesGcm(key []byte) cipher.AEAD { + block, err := aes.NewCipher(key) + common.Must(err) + aead, err := cipher.NewGCM(block) + common.Must(err) + return aead +} diff --git a/common/crypto/auth.go b/common/crypto/auth.go new file mode 100644 index 00000000..e17b3efa --- /dev/null +++ b/common/crypto/auth.go @@ -0,0 +1,345 @@ +package crypto + +import ( + "crypto/cipher" + "io" + "math/rand" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/bytespool" + "github.com/xtls/xray-core/v1/common/protocol" +) + +type BytesGenerator func() []byte + +func GenerateEmptyBytes() BytesGenerator { + var b [1]byte + return func() []byte { + return b[:0] + } +} + +func GenerateStaticBytes(content []byte) BytesGenerator { + return func() []byte { + return content + } +} + +func GenerateIncreasingNonce(nonce []byte) BytesGenerator { + c := append([]byte(nil), nonce...) + return func() []byte { + for i := range c { + c[i]++ + if c[i] != 0 { + break + } + } + return c + } +} + +func GenerateInitialAEADNonce() BytesGenerator { + return GenerateIncreasingNonce([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) +} + +type Authenticator interface { + NonceSize() int + Overhead() int + Open(dst, cipherText []byte) ([]byte, error) + Seal(dst, plainText []byte) ([]byte, error) +} + +type AEADAuthenticator struct { + cipher.AEAD + NonceGenerator BytesGenerator + AdditionalDataGenerator BytesGenerator +} + +func (v *AEADAuthenticator) Open(dst, cipherText []byte) ([]byte, error) { + iv := v.NonceGenerator() + if len(iv) != v.AEAD.NonceSize() { + return nil, newError("invalid AEAD nonce size: ", len(iv)) + } + + var additionalData []byte + if v.AdditionalDataGenerator != nil { + additionalData = v.AdditionalDataGenerator() + } + return v.AEAD.Open(dst, iv, cipherText, additionalData) +} + +func (v *AEADAuthenticator) Seal(dst, plainText []byte) ([]byte, error) { + iv := v.NonceGenerator() + if len(iv) != v.AEAD.NonceSize() { + return nil, newError("invalid AEAD nonce size: ", len(iv)) + } + + var additionalData []byte + if v.AdditionalDataGenerator != nil { + additionalData = v.AdditionalDataGenerator() + } + return v.AEAD.Seal(dst, iv, plainText, additionalData), nil +} + +type AuthenticationReader struct { + auth Authenticator + reader *buf.BufferedReader + sizeParser ChunkSizeDecoder + sizeBytes []byte + transferType protocol.TransferType + padding PaddingLengthGenerator + size uint16 + paddingLen uint16 + hasSize bool + done bool +} + +func NewAuthenticationReader(auth Authenticator, sizeParser ChunkSizeDecoder, reader io.Reader, transferType protocol.TransferType, paddingLen PaddingLengthGenerator) *AuthenticationReader { + r := &AuthenticationReader{ + auth: auth, + sizeParser: sizeParser, + transferType: transferType, + padding: paddingLen, + sizeBytes: make([]byte, sizeParser.SizeBytes()), + } + if breader, ok := reader.(*buf.BufferedReader); ok { + r.reader = breader + } else { + r.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } + return r +} + +func (r *AuthenticationReader) readSize() (uint16, uint16, error) { + if r.hasSize { + r.hasSize = false + return r.size, r.paddingLen, nil + } + if _, err := io.ReadFull(r.reader, r.sizeBytes); err != nil { + return 0, 0, err + } + var padding uint16 + if r.padding != nil { + padding = r.padding.NextPaddingLen() + } + size, err := r.sizeParser.Decode(r.sizeBytes) + return size, padding, err +} + +var errSoft = newError("waiting for more data") + +func (r *AuthenticationReader) readBuffer(size int32, padding int32) (*buf.Buffer, error) { + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, size); err != nil { + b.Release() + return nil, err + } + size -= padding + rb, err := r.auth.Open(b.BytesTo(0), b.BytesTo(size)) + if err != nil { + b.Release() + return nil, err + } + b.Resize(0, int32(len(rb))) + return b, nil +} + +func (r *AuthenticationReader) readInternal(soft bool, mb *buf.MultiBuffer) error { + if soft && r.reader.BufferedBytes() < r.sizeParser.SizeBytes() { + return errSoft + } + + if r.done { + return io.EOF + } + + size, padding, err := r.readSize() + if err != nil { + return err + } + + if size == uint16(r.auth.Overhead())+padding { + r.done = true + return io.EOF + } + + if soft && int32(size) > r.reader.BufferedBytes() { + r.size = size + r.paddingLen = padding + r.hasSize = true + return errSoft + } + + if size <= buf.Size { + b, err := r.readBuffer(int32(size), int32(padding)) + if err != nil { + return nil + } + *mb = append(*mb, b) + return nil + } + + payload := bytespool.Alloc(int32(size)) + defer bytespool.Free(payload) + + if _, err := io.ReadFull(r.reader, payload[:size]); err != nil { + return err + } + + size -= padding + + rb, err := r.auth.Open(payload[:0], payload[:size]) + if err != nil { + return err + } + + *mb = buf.MergeBytes(*mb, rb) + return nil +} + +func (r *AuthenticationReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + const readSize = 16 + mb := make(buf.MultiBuffer, 0, readSize) + if err := r.readInternal(false, &mb); err != nil { + buf.ReleaseMulti(mb) + return nil, err + } + + for i := 1; i < readSize; i++ { + err := r.readInternal(true, &mb) + if err == errSoft || err == io.EOF { + break + } + if err != nil { + buf.ReleaseMulti(mb) + return nil, err + } + } + + return mb, nil +} + +type AuthenticationWriter struct { + auth Authenticator + writer buf.Writer + sizeParser ChunkSizeEncoder + transferType protocol.TransferType + padding PaddingLengthGenerator +} + +func NewAuthenticationWriter(auth Authenticator, sizeParser ChunkSizeEncoder, writer io.Writer, transferType protocol.TransferType, padding PaddingLengthGenerator) *AuthenticationWriter { + w := &AuthenticationWriter{ + auth: auth, + writer: buf.NewWriter(writer), + sizeParser: sizeParser, + transferType: transferType, + } + if padding != nil { + w.padding = padding + } + return w +} + +func (w *AuthenticationWriter) seal(b []byte) (*buf.Buffer, error) { + encryptedSize := int32(len(b) + w.auth.Overhead()) + var paddingSize int32 + if w.padding != nil { + paddingSize = int32(w.padding.NextPaddingLen()) + } + + sizeBytes := w.sizeParser.SizeBytes() + totalSize := sizeBytes + encryptedSize + paddingSize + if totalSize > buf.Size { + return nil, newError("size too large: ", totalSize) + } + + eb := buf.New() + w.sizeParser.Encode(uint16(encryptedSize+paddingSize), eb.Extend(sizeBytes)) + if _, err := w.auth.Seal(eb.Extend(encryptedSize)[:0], b); err != nil { + eb.Release() + return nil, err + } + if paddingSize > 0 { + // With size of the chunk and padding length encrypted, the content of padding doesn't matter much. + paddingBytes := eb.Extend(paddingSize) + common.Must2(rand.Read(paddingBytes)) + } + + return eb, nil +} + +func (w *AuthenticationWriter) writeStream(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + var maxPadding int32 + if w.padding != nil { + maxPadding = int32(w.padding.MaxPaddingLen()) + } + + payloadSize := buf.Size - int32(w.auth.Overhead()) - w.sizeParser.SizeBytes() - maxPadding + mb2Write := make(buf.MultiBuffer, 0, len(mb)+10) + + temp := buf.New() + defer temp.Release() + + rawBytes := temp.Extend(payloadSize) + + for { + nb, nBytes := buf.SplitBytes(mb, rawBytes) + mb = nb + + eb, err := w.seal(rawBytes[:nBytes]) + + if err != nil { + buf.ReleaseMulti(mb2Write) + return err + } + mb2Write = append(mb2Write, eb) + if mb.IsEmpty() { + break + } + } + + return w.writer.WriteMultiBuffer(mb2Write) +} + +func (w *AuthenticationWriter) writePacket(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + mb2Write := make(buf.MultiBuffer, 0, len(mb)+1) + + for _, b := range mb { + if b.IsEmpty() { + continue + } + + eb, err := w.seal(b.Bytes()) + if err != nil { + continue + } + + mb2Write = append(mb2Write, eb) + } + + if mb2Write.IsEmpty() { + return nil + } + + return w.writer.WriteMultiBuffer(mb2Write) +} + +// WriteMultiBuffer implements buf.Writer. +func (w *AuthenticationWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + if mb.IsEmpty() { + eb, err := w.seal([]byte{}) + common.Must(err) + return w.writer.WriteMultiBuffer(buf.MultiBuffer{eb}) + } + + if w.transferType == protocol.TransferTypeStream { + return w.writeStream(mb) + } + + return w.writePacket(mb) +} diff --git a/common/crypto/auth_test.go b/common/crypto/auth_test.go new file mode 100644 index 00000000..5a389f04 --- /dev/null +++ b/common/crypto/auth_test.go @@ -0,0 +1,143 @@ +package crypto_test + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/common/crypto" + "github.com/xtls/xray-core/v1/common/protocol" +) + +func TestAuthenticationReaderWriter(t *testing.T) { + key := make([]byte, 16) + rand.Read(key) + block, err := aes.NewCipher(key) + common.Must(err) + + aead, err := cipher.NewGCM(block) + common.Must(err) + + const payloadSize = 1024 * 80 + rawPayload := make([]byte, payloadSize) + rand.Read(rawPayload) + + payload := buf.MergeBytes(nil, rawPayload) + + cache := bytes.NewBuffer(nil) + iv := make([]byte, 12) + rand.Read(iv) + + writer := NewAuthenticationWriter(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypeStream, nil) + + common.Must(writer.WriteMultiBuffer(payload)) + if cache.Len() <= 1024*80 { + t.Error("cache len: ", cache.Len()) + } + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + reader := NewAuthenticationReader(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypeStream, nil) + + var mb buf.MultiBuffer + + for mb.Len() < payloadSize { + mb2, err := reader.ReadMultiBuffer() + common.Must(err) + + mb, _ = buf.MergeMulti(mb, mb2) + } + + if mb.Len() != payloadSize { + t.Error("mb len: ", mb.Len()) + } + + mbContent := make([]byte, payloadSize) + buf.SplitBytes(mb, mbContent) + if r := cmp.Diff(mbContent, rawPayload); r != "" { + t.Error(r) + } + + _, err = reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} + +func TestAuthenticationReaderWriterPacket(t *testing.T) { + key := make([]byte, 16) + common.Must2(rand.Read(key)) + block, err := aes.NewCipher(key) + common.Must(err) + + aead, err := cipher.NewGCM(block) + common.Must(err) + + cache := buf.New() + iv := make([]byte, 12) + rand.Read(iv) + + writer := NewAuthenticationWriter(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypePacket, nil) + + var payload buf.MultiBuffer + pb1 := buf.New() + pb1.Write([]byte("abcd")) + payload = append(payload, pb1) + + pb2 := buf.New() + pb2.Write([]byte("efgh")) + payload = append(payload, pb2) + + common.Must(writer.WriteMultiBuffer(payload)) + if cache.Len() == 0 { + t.Error("cache len: ", cache.Len()) + } + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + reader := NewAuthenticationReader(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypePacket, nil) + + mb, err := reader.ReadMultiBuffer() + common.Must(err) + + mb, b1 := buf.SplitFirst(mb) + if b1.String() != "abcd" { + t.Error("b1: ", b1.String()) + } + + mb, b2 := buf.SplitFirst(mb) + if b2.String() != "efgh" { + t.Error("b2: ", b2.String()) + } + + if !mb.IsEmpty() { + t.Error("not empty") + } + + _, err = reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} diff --git a/common/crypto/benchmark_test.go b/common/crypto/benchmark_test.go new file mode 100644 index 00000000..faffa333 --- /dev/null +++ b/common/crypto/benchmark_test.go @@ -0,0 +1,50 @@ +package crypto_test + +import ( + "crypto/cipher" + "testing" + + . "github.com/xtls/xray-core/v1/common/crypto" +) + +const benchSize = 1024 * 1024 + +func benchmarkStream(b *testing.B, c cipher.Stream) { + b.SetBytes(benchSize) + input := make([]byte, benchSize) + output := make([]byte, benchSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.XORKeyStream(output, input) + } +} + +func BenchmarkChaCha20(b *testing.B) { + key := make([]byte, 32) + nonce := make([]byte, 8) + c := NewChaCha20Stream(key, nonce) + benchmarkStream(b, c) +} + +func BenchmarkChaCha20IETF(b *testing.B) { + key := make([]byte, 32) + nonce := make([]byte, 12) + c := NewChaCha20Stream(key, nonce) + benchmarkStream(b, c) +} + +func BenchmarkAESEncryption(b *testing.B) { + key := make([]byte, 32) + iv := make([]byte, 16) + c := NewAesEncryptionStream(key, iv) + + benchmarkStream(b, c) +} + +func BenchmarkAESDecryption(b *testing.B) { + key := make([]byte, 32) + iv := make([]byte, 16) + c := NewAesDecryptionStream(key, iv) + + benchmarkStream(b, c) +} diff --git a/common/crypto/chacha20.go b/common/crypto/chacha20.go new file mode 100644 index 00000000..1015030e --- /dev/null +++ b/common/crypto/chacha20.go @@ -0,0 +1,13 @@ +package crypto + +import ( + "crypto/cipher" + + "github.com/xtls/xray-core/v1/common/crypto/internal" +) + +// NewChaCha20Stream creates a new Chacha20 encryption/descryption stream based on give key and IV. +// Caller must ensure the length of key is 32 bytes, and length of IV is either 8 or 12 bytes. +func NewChaCha20Stream(key []byte, iv []byte) cipher.Stream { + return internal.NewChaCha20Stream(key, iv, 20) +} diff --git a/common/crypto/chacha20_test.go b/common/crypto/chacha20_test.go new file mode 100644 index 00000000..474ed196 --- /dev/null +++ b/common/crypto/chacha20_test.go @@ -0,0 +1,77 @@ +package crypto_test + +import ( + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/crypto" +) + +func mustDecodeHex(s string) []byte { + b, err := hex.DecodeString(s) + common.Must(err) + return b +} + +func TestChaCha20Stream(t *testing.T) { + var cases = []struct { + key []byte + iv []byte + output []byte + }{ + { + key: mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), + iv: mustDecodeHex("0000000000000000"), + output: mustDecodeHex("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586" + + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" + + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"), + }, + { + key: mustDecodeHex("5555555555555555555555555555555555555555555555555555555555555555"), + iv: mustDecodeHex("5555555555555555"), + output: mustDecodeHex("bea9411aa453c5434a5ae8c92862f564396855a9ea6e22d6d3b50ae1b3663311" + + "a4a3606c671d605ce16c3aece8e61ea145c59775017bee2fa6f88afc758069f7" + + "e0b8f676e644216f4d2a3422d7fa36c6c4931aca950e9da42788e6d0b6d1cd83" + + "8ef652e97b145b14871eae6c6804c7004db5ac2fce4c68c726d004b10fcaba86"), + }, + { + key: mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), + iv: mustDecodeHex("000000000000000000000000"), + output: mustDecodeHex("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"), + }, + } + for _, c := range cases { + s := NewChaCha20Stream(c.key, c.iv) + input := make([]byte, len(c.output)) + actualOutout := make([]byte, len(c.output)) + s.XORKeyStream(actualOutout, input) + if r := cmp.Diff(c.output, actualOutout); r != "" { + t.Fatal(r) + } + } +} + +func TestChaCha20Decoding(t *testing.T) { + key := make([]byte, 32) + common.Must2(rand.Read(key)) + iv := make([]byte, 8) + common.Must2(rand.Read(iv)) + stream := NewChaCha20Stream(key, iv) + + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + x := make([]byte, len(payload)) + stream.XORKeyStream(x, payload) + + stream2 := NewChaCha20Stream(key, iv) + stream2.XORKeyStream(x, x) + if r := cmp.Diff(x, payload); r != "" { + t.Fatal(r) + } +} diff --git a/common/crypto/chunk.go b/common/crypto/chunk.go new file mode 100644 index 00000000..657d8c0f --- /dev/null +++ b/common/crypto/chunk.go @@ -0,0 +1,160 @@ +package crypto + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" +) + +// ChunkSizeDecoder is a utility class to decode size value from bytes. +type ChunkSizeDecoder interface { + SizeBytes() int32 + Decode([]byte) (uint16, error) +} + +// ChunkSizeEncoder is a utility class to encode size value into bytes. +type ChunkSizeEncoder interface { + SizeBytes() int32 + Encode(uint16, []byte) []byte +} + +type PaddingLengthGenerator interface { + MaxPaddingLen() uint16 + NextPaddingLen() uint16 +} + +type PlainChunkSizeParser struct{} + +func (PlainChunkSizeParser) SizeBytes() int32 { + return 2 +} + +func (PlainChunkSizeParser) Encode(size uint16, b []byte) []byte { + binary.BigEndian.PutUint16(b, size) + return b[:2] +} + +func (PlainChunkSizeParser) Decode(b []byte) (uint16, error) { + return binary.BigEndian.Uint16(b), nil +} + +type AEADChunkSizeParser struct { + Auth *AEADAuthenticator +} + +func (p *AEADChunkSizeParser) SizeBytes() int32 { + return 2 + int32(p.Auth.Overhead()) +} + +func (p *AEADChunkSizeParser) Encode(size uint16, b []byte) []byte { + binary.BigEndian.PutUint16(b, size-uint16(p.Auth.Overhead())) + b, err := p.Auth.Seal(b[:0], b[:2]) + common.Must(err) + return b +} + +func (p *AEADChunkSizeParser) Decode(b []byte) (uint16, error) { + b, err := p.Auth.Open(b[:0], b) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b) + uint16(p.Auth.Overhead()), nil +} + +type ChunkStreamReader struct { + sizeDecoder ChunkSizeDecoder + reader *buf.BufferedReader + + buffer []byte + leftOverSize int32 + maxNumChunk uint32 + numChunk uint32 +} + +func NewChunkStreamReader(sizeDecoder ChunkSizeDecoder, reader io.Reader) *ChunkStreamReader { + return NewChunkStreamReaderWithChunkCount(sizeDecoder, reader, 0) +} + +func NewChunkStreamReaderWithChunkCount(sizeDecoder ChunkSizeDecoder, reader io.Reader, maxNumChunk uint32) *ChunkStreamReader { + r := &ChunkStreamReader{ + sizeDecoder: sizeDecoder, + buffer: make([]byte, sizeDecoder.SizeBytes()), + maxNumChunk: maxNumChunk, + } + if breader, ok := reader.(*buf.BufferedReader); ok { + r.reader = breader + } else { + r.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } + + return r +} + +func (r *ChunkStreamReader) readSize() (uint16, error) { + if _, err := io.ReadFull(r.reader, r.buffer); err != nil { + return 0, err + } + return r.sizeDecoder.Decode(r.buffer) +} + +func (r *ChunkStreamReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + size := r.leftOverSize + if size == 0 { + r.numChunk++ + if r.maxNumChunk > 0 && r.numChunk > r.maxNumChunk { + return nil, io.EOF + } + nextSize, err := r.readSize() + if err != nil { + return nil, err + } + if nextSize == 0 { + return nil, io.EOF + } + size = int32(nextSize) + } + r.leftOverSize = size + + mb, err := r.reader.ReadAtMost(size) + if !mb.IsEmpty() { + r.leftOverSize -= mb.Len() + return mb, nil + } + return nil, err +} + +type ChunkStreamWriter struct { + sizeEncoder ChunkSizeEncoder + writer buf.Writer +} + +func NewChunkStreamWriter(sizeEncoder ChunkSizeEncoder, writer io.Writer) *ChunkStreamWriter { + return &ChunkStreamWriter{ + sizeEncoder: sizeEncoder, + writer: buf.NewWriter(writer), + } +} + +func (w *ChunkStreamWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + const sliceSize = 8192 + mbLen := mb.Len() + mb2Write := make(buf.MultiBuffer, 0, mbLen/buf.Size+mbLen/sliceSize+2) + + for { + mb2, slice := buf.SplitSize(mb, sliceSize) + mb = mb2 + + b := buf.New() + w.sizeEncoder.Encode(uint16(slice.Len()), b.Extend(w.sizeEncoder.SizeBytes())) + mb2Write = append(mb2Write, b) + mb2Write = append(mb2Write, slice...) + + if mb.IsEmpty() { + break + } + } + + return w.writer.WriteMultiBuffer(mb2Write) +} diff --git a/common/crypto/chunk_test.go b/common/crypto/chunk_test.go new file mode 100644 index 00000000..ddfba889 --- /dev/null +++ b/common/crypto/chunk_test.go @@ -0,0 +1,51 @@ +package crypto_test + +import ( + "bytes" + "io" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/common/crypto" +) + +func TestChunkStreamIO(t *testing.T) { + cache := bytes.NewBuffer(make([]byte, 0, 8192)) + + writer := NewChunkStreamWriter(PlainChunkSizeParser{}, cache) + reader := NewChunkStreamReader(PlainChunkSizeParser{}, cache) + + b := buf.New() + b.WriteString("abcd") + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + b = buf.New() + b.WriteString("efg") + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + if cache.Len() != 13 { + t.Fatalf("Cache length is %d, want 13", cache.Len()) + } + + mb, err := reader.ReadMultiBuffer() + common.Must(err) + + if s := mb.String(); s != "abcd" { + t.Error("content: ", s) + } + + mb, err = reader.ReadMultiBuffer() + common.Must(err) + + if s := mb.String(); s != "efg" { + t.Error("content: ", s) + } + + _, err = reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} diff --git a/common/crypto/crypto.go b/common/crypto/crypto.go new file mode 100644 index 00000000..96ee32d7 --- /dev/null +++ b/common/crypto/crypto.go @@ -0,0 +1,4 @@ +// Package crypto provides common crypto libraries for Xray. +package crypto // import "github.com/xtls/xray-core/v1/common/crypto" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/common/crypto/errors.generated.go b/common/crypto/errors.generated.go new file mode 100644 index 00000000..bc627cb2 --- /dev/null +++ b/common/crypto/errors.generated.go @@ -0,0 +1,9 @@ +package crypto + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/crypto/internal/chacha.go b/common/crypto/internal/chacha.go new file mode 100644 index 00000000..988ac984 --- /dev/null +++ b/common/crypto/internal/chacha.go @@ -0,0 +1,80 @@ +package internal + +//go:generate go run chacha_core_gen.go + +import ( + "encoding/binary" +) + +const ( + wordSize = 4 // the size of ChaCha20's words + stateSize = 16 // the size of ChaCha20's state, in words + blockSize = stateSize * wordSize // the size of ChaCha20's block, in bytes +) + +type ChaCha20Stream struct { + state [stateSize]uint32 // the state as an array of 16 32-bit words + block [blockSize]byte // the keystream as an array of 64 bytes + offset int // the offset of used bytes in block + rounds int +} + +func NewChaCha20Stream(key []byte, nonce []byte, rounds int) *ChaCha20Stream { + s := new(ChaCha20Stream) + // the magic constants for 256-bit keys + s.state[0] = 0x61707865 + s.state[1] = 0x3320646e + s.state[2] = 0x79622d32 + s.state[3] = 0x6b206574 + + for i := 0; i < 8; i++ { + s.state[i+4] = binary.LittleEndian.Uint32(key[i*4 : i*4+4]) + } + + switch len(nonce) { + case 8: + s.state[14] = binary.LittleEndian.Uint32(nonce[0:]) + s.state[15] = binary.LittleEndian.Uint32(nonce[4:]) + case 12: + s.state[13] = binary.LittleEndian.Uint32(nonce[0:4]) + s.state[14] = binary.LittleEndian.Uint32(nonce[4:8]) + s.state[15] = binary.LittleEndian.Uint32(nonce[8:12]) + default: + panic("bad nonce length") + } + + s.rounds = rounds + ChaCha20Block(&s.state, s.block[:], s.rounds) + return s +} + +func (s *ChaCha20Stream) XORKeyStream(dst, src []byte) { + // Stride over the input in 64-byte blocks, minus the amount of keystream + // previously used. This will produce best results when processing blocks + // of a size evenly divisible by 64. + i := 0 + max := len(src) + for i < max { + gap := blockSize - s.offset + + limit := i + gap + if limit > max { + limit = max + } + + o := s.offset + for j := i; j < limit; j++ { + dst[j] = src[j] ^ s.block[o] + o++ + } + + i += gap + s.offset = o + + if o == blockSize { + s.offset = 0 + s.state[12]++ + ChaCha20Block(&s.state, s.block[:], s.rounds) + } + } +} diff --git a/common/crypto/internal/chacha_core.generated.go b/common/crypto/internal/chacha_core.generated.go new file mode 100644 index 00000000..a0b59788 --- /dev/null +++ b/common/crypto/internal/chacha_core.generated.go @@ -0,0 +1,123 @@ +package internal + +import "encoding/binary" + +func ChaCha20Block(s *[16]uint32, out []byte, rounds int) { + var x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 = s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15] + for i := 0; i < rounds; i += 2 { + var x uint32 + + x0 += x4 + x = x12 ^ x0 + x12 = (x << 16) | (x >> (32 - 16)) + x8 += x12 + x = x4 ^ x8 + x4 = (x << 12) | (x >> (32 - 12)) + x0 += x4 + x = x12 ^ x0 + x12 = (x << 8) | (x >> (32 - 8)) + x8 += x12 + x = x4 ^ x8 + x4 = (x << 7) | (x >> (32 - 7)) + x1 += x5 + x = x13 ^ x1 + x13 = (x << 16) | (x >> (32 - 16)) + x9 += x13 + x = x5 ^ x9 + x5 = (x << 12) | (x >> (32 - 12)) + x1 += x5 + x = x13 ^ x1 + x13 = (x << 8) | (x >> (32 - 8)) + x9 += x13 + x = x5 ^ x9 + x5 = (x << 7) | (x >> (32 - 7)) + x2 += x6 + x = x14 ^ x2 + x14 = (x << 16) | (x >> (32 - 16)) + x10 += x14 + x = x6 ^ x10 + x6 = (x << 12) | (x >> (32 - 12)) + x2 += x6 + x = x14 ^ x2 + x14 = (x << 8) | (x >> (32 - 8)) + x10 += x14 + x = x6 ^ x10 + x6 = (x << 7) | (x >> (32 - 7)) + x3 += x7 + x = x15 ^ x3 + x15 = (x << 16) | (x >> (32 - 16)) + x11 += x15 + x = x7 ^ x11 + x7 = (x << 12) | (x >> (32 - 12)) + x3 += x7 + x = x15 ^ x3 + x15 = (x << 8) | (x >> (32 - 8)) + x11 += x15 + x = x7 ^ x11 + x7 = (x << 7) | (x >> (32 - 7)) + x0 += x5 + x = x15 ^ x0 + x15 = (x << 16) | (x >> (32 - 16)) + x10 += x15 + x = x5 ^ x10 + x5 = (x << 12) | (x >> (32 - 12)) + x0 += x5 + x = x15 ^ x0 + x15 = (x << 8) | (x >> (32 - 8)) + x10 += x15 + x = x5 ^ x10 + x5 = (x << 7) | (x >> (32 - 7)) + x1 += x6 + x = x12 ^ x1 + x12 = (x << 16) | (x >> (32 - 16)) + x11 += x12 + x = x6 ^ x11 + x6 = (x << 12) | (x >> (32 - 12)) + x1 += x6 + x = x12 ^ x1 + x12 = (x << 8) | (x >> (32 - 8)) + x11 += x12 + x = x6 ^ x11 + x6 = (x << 7) | (x >> (32 - 7)) + x2 += x7 + x = x13 ^ x2 + x13 = (x << 16) | (x >> (32 - 16)) + x8 += x13 + x = x7 ^ x8 + x7 = (x << 12) | (x >> (32 - 12)) + x2 += x7 + x = x13 ^ x2 + x13 = (x << 8) | (x >> (32 - 8)) + x8 += x13 + x = x7 ^ x8 + x7 = (x << 7) | (x >> (32 - 7)) + x3 += x4 + x = x14 ^ x3 + x14 = (x << 16) | (x >> (32 - 16)) + x9 += x14 + x = x4 ^ x9 + x4 = (x << 12) | (x >> (32 - 12)) + x3 += x4 + x = x14 ^ x3 + x14 = (x << 8) | (x >> (32 - 8)) + x9 += x14 + x = x4 ^ x9 + x4 = (x << 7) | (x >> (32 - 7)) + } + binary.LittleEndian.PutUint32(out[0:4], s[0]+x0) + binary.LittleEndian.PutUint32(out[4:8], s[1]+x1) + binary.LittleEndian.PutUint32(out[8:12], s[2]+x2) + binary.LittleEndian.PutUint32(out[12:16], s[3]+x3) + binary.LittleEndian.PutUint32(out[16:20], s[4]+x4) + binary.LittleEndian.PutUint32(out[20:24], s[5]+x5) + binary.LittleEndian.PutUint32(out[24:28], s[6]+x6) + binary.LittleEndian.PutUint32(out[28:32], s[7]+x7) + binary.LittleEndian.PutUint32(out[32:36], s[8]+x8) + binary.LittleEndian.PutUint32(out[36:40], s[9]+x9) + binary.LittleEndian.PutUint32(out[40:44], s[10]+x10) + binary.LittleEndian.PutUint32(out[44:48], s[11]+x11) + binary.LittleEndian.PutUint32(out[48:52], s[12]+x12) + binary.LittleEndian.PutUint32(out[52:56], s[13]+x13) + binary.LittleEndian.PutUint32(out[56:60], s[14]+x14) + binary.LittleEndian.PutUint32(out[60:64], s[15]+x15) +} diff --git a/common/crypto/internal/chacha_core_gen.go b/common/crypto/internal/chacha_core_gen.go new file mode 100644 index 00000000..0058bd43 --- /dev/null +++ b/common/crypto/internal/chacha_core_gen.go @@ -0,0 +1,69 @@ +// +build generate + +package main + +import ( + "fmt" + "log" + "os" +) + +func writeQuarterRound(file *os.File, a, b, c, d int) { + add := "x%d+=x%d\n" + xor := "x=x%d^x%d\n" + rotate := "x%d=(x << %d) | (x >> (32 - %d))\n" + + fmt.Fprintf(file, add, a, b) + fmt.Fprintf(file, xor, d, a) + fmt.Fprintf(file, rotate, d, 16, 16) + + fmt.Fprintf(file, add, c, d) + fmt.Fprintf(file, xor, b, c) + fmt.Fprintf(file, rotate, b, 12, 12) + + fmt.Fprintf(file, add, a, b) + fmt.Fprintf(file, xor, d, a) + fmt.Fprintf(file, rotate, d, 8, 8) + + fmt.Fprintf(file, add, c, d) + fmt.Fprintf(file, xor, b, c) + fmt.Fprintf(file, rotate, b, 7, 7) +} + +func writeChacha20Block(file *os.File) { + fmt.Fprintln(file, ` +func ChaCha20Block(s *[16]uint32, out []byte, rounds int) { + var x0,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15 = s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9],s[10],s[11],s[12],s[13],s[14],s[15] + for i := 0; i < rounds; i+=2 { + var x uint32 + `) + + writeQuarterRound(file, 0, 4, 8, 12) + writeQuarterRound(file, 1, 5, 9, 13) + writeQuarterRound(file, 2, 6, 10, 14) + writeQuarterRound(file, 3, 7, 11, 15) + writeQuarterRound(file, 0, 5, 10, 15) + writeQuarterRound(file, 1, 6, 11, 12) + writeQuarterRound(file, 2, 7, 8, 13) + writeQuarterRound(file, 3, 4, 9, 14) + fmt.Fprintln(file, "}") + for i := 0; i < 16; i++ { + fmt.Fprintf(file, "binary.LittleEndian.PutUint32(out[%d:%d], s[%d]+x%d)\n", i*4, i*4+4, i, i) + } + fmt.Fprintln(file, "}") + fmt.Fprintln(file) +} + +func main() { + file, err := os.OpenFile("chacha_core.generated.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + log.Fatalf("Failed to generate chacha_core.go: %v", err) + } + defer file.Close() + + fmt.Fprintln(file, "package internal") + fmt.Fprintln(file) + fmt.Fprintln(file, "import \"encoding/binary\"") + fmt.Fprintln(file) + writeChacha20Block(file) +} diff --git a/common/crypto/io.go b/common/crypto/io.go new file mode 100644 index 00000000..f06ca7b5 --- /dev/null +++ b/common/crypto/io.go @@ -0,0 +1,66 @@ +package crypto + +import ( + "crypto/cipher" + "io" + + "github.com/xtls/xray-core/v1/common/buf" +) + +type CryptionReader struct { + stream cipher.Stream + reader io.Reader +} + +func NewCryptionReader(stream cipher.Stream, reader io.Reader) *CryptionReader { + return &CryptionReader{ + stream: stream, + reader: reader, + } +} + +func (r *CryptionReader) Read(data []byte) (int, error) { + nBytes, err := r.reader.Read(data) + if nBytes > 0 { + r.stream.XORKeyStream(data[:nBytes], data[:nBytes]) + } + return nBytes, err +} + +var ( + _ buf.Writer = (*CryptionWriter)(nil) +) + +type CryptionWriter struct { + stream cipher.Stream + writer io.Writer + bufWriter buf.Writer +} + +// NewCryptionWriter creates a new CryptionWriter. +func NewCryptionWriter(stream cipher.Stream, writer io.Writer) *CryptionWriter { + return &CryptionWriter{ + stream: stream, + writer: writer, + bufWriter: buf.NewWriter(writer), + } +} + +// Write implements io.Writer.Write(). +func (w *CryptionWriter) Write(data []byte) (int, error) { + w.stream.XORKeyStream(data, data) + + if err := buf.WriteAllBytes(w.writer, data); err != nil { + return 0, err + } + return len(data), nil +} + +// WriteMultiBuffer implements buf.Writer. +func (w *CryptionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for _, b := range mb { + w.stream.XORKeyStream(b.Bytes(), b.Bytes()) + } + + return w.bufWriter.WriteMultiBuffer(mb) +} diff --git a/common/dice/dice.go b/common/dice/dice.go new file mode 100644 index 00000000..69fcc240 --- /dev/null +++ b/common/dice/dice.go @@ -0,0 +1,52 @@ +// Package dice contains common functions to generate random number. +// It also initialize math/rand with the time in seconds at launch time. +package dice // import "github.com/xtls/xray-core/v1/common/dice" + +import ( + "math/rand" + "time" +) + +// Roll returns a non-negative number between 0 (inclusive) and n (exclusive). +func Roll(n int) int { + if n == 1 { + return 0 + } + return rand.Intn(n) +} + +// Roll returns a non-negative number between 0 (inclusive) and n (exclusive). +func RollDeterministic(n int, seed int64) int { + if n == 1 { + return 0 + } + return rand.New(rand.NewSource(seed)).Intn(n) +} + +// RollUint16 returns a random uint16 value. +func RollUint16() uint16 { + return uint16(rand.Int63() >> 47) +} + +func RollUint64() uint64 { + return rand.Uint64() +} + +func NewDeterministicDice(seed int64) *DeterministicDice { + return &DeterministicDice{rand.New(rand.NewSource(seed))} +} + +type DeterministicDice struct { + *rand.Rand +} + +func (dd *DeterministicDice) Roll(n int) int { + if n == 1 { + return 0 + } + return dd.Intn(n) +} + +func init() { + rand.Seed(time.Now().Unix()) +} diff --git a/common/dice/dice_test.go b/common/dice/dice_test.go new file mode 100644 index 00000000..9cf41ee3 --- /dev/null +++ b/common/dice/dice_test.go @@ -0,0 +1,50 @@ +package dice_test + +import ( + "math/rand" + "testing" + + . "github.com/xtls/xray-core/v1/common/dice" +) + +func BenchmarkRoll1(b *testing.B) { + for i := 0; i < b.N; i++ { + Roll(1) + } +} + +func BenchmarkRoll20(b *testing.B) { + for i := 0; i < b.N; i++ { + Roll(20) + } +} + +func BenchmarkIntn1(b *testing.B) { + for i := 0; i < b.N; i++ { + rand.Intn(1) + } +} + +func BenchmarkIntn20(b *testing.B) { + for i := 0; i < b.N; i++ { + rand.Intn(20) + } +} + +func BenchmarkInt63(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Int63() >> 47) + } +} + +func BenchmarkInt31(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Int31() >> 15) + } +} + +func BenchmarkIntn(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Intn(65536)) + } +} diff --git a/common/errors.generated.go b/common/errors.generated.go new file mode 100644 index 00000000..9d6c1cc3 --- /dev/null +++ b/common/errors.generated.go @@ -0,0 +1,9 @@ +package common + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/errors/errorgen/main.go b/common/errors/errorgen/main.go new file mode 100644 index 00000000..1fb8ef78 --- /dev/null +++ b/common/errors/errorgen/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/xtls/xray-core/v1/common" +) + +func main() { + pwd, err := os.Getwd() + if err != nil { + fmt.Println("can not get current working directory") + os.Exit(1) + } + pkg := filepath.Base(pwd) + if pkg == "xray-core" { + pkg = "core" + } + + moduleName, gmnErr := common.GetModuleName(pwd) + if gmnErr != nil { + fmt.Println("can not get module path", gmnErr) + os.Exit(1) + } + + file, err := os.OpenFile("errors.generated.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + log.Fatalf("Failed to generate errors.generated.go: %v", err) + os.Exit(1) + } + defer file.Close() + + fmt.Fprintln(file, "package", pkg) + fmt.Fprintln(file, "") + fmt.Fprintln(file, "import \""+moduleName+"/common/errors\"") + fmt.Fprintln(file, "") + fmt.Fprintln(file, "type errPathObjHolder struct{}") + fmt.Fprintln(file, "") + fmt.Fprintln(file, "func newError(values ...interface{}) *errors.Error {") + fmt.Fprintln(file, " return errors.New(values...).WithPathObj(errPathObjHolder{})") + fmt.Fprintln(file, "}") +} diff --git a/common/errors/errors.go b/common/errors/errors.go new file mode 100644 index 00000000..4b843be8 --- /dev/null +++ b/common/errors/errors.go @@ -0,0 +1,195 @@ +// Package errors is a drop-in replacement for Golang lib 'errors'. +package errors // import "github.com/xtls/xray-core/v1/common/errors" + +import ( + "os" + "reflect" + "strings" + + "github.com/xtls/xray-core/v1/common/log" + "github.com/xtls/xray-core/v1/common/serial" +) + +type hasInnerError interface { + // Inner returns the underlying error of this one. + Inner() error +} + +type hasSeverity interface { + Severity() log.Severity +} + +// Error is an error object with underlying error. +type Error struct { + pathObj interface{} + prefix []interface{} + message []interface{} + inner error + severity log.Severity +} + +func (err *Error) WithPathObj(obj interface{}) *Error { + err.pathObj = obj + return err +} + +func (err *Error) pkgPath() string { + if err.pathObj == nil { + return "" + } + return reflect.TypeOf(err.pathObj).PkgPath() +} + +// Error implements error.Error(). +func (err *Error) Error() string { + builder := strings.Builder{} + for _, prefix := range err.prefix { + builder.WriteByte('[') + builder.WriteString(serial.ToString(prefix)) + builder.WriteString("] ") + } + + path := err.pkgPath() + if len(path) > 0 { + builder.WriteString(path) + builder.WriteString(": ") + } + + msg := serial.Concat(err.message...) + builder.WriteString(msg) + + if err.inner != nil { + builder.WriteString(" > ") + builder.WriteString(err.inner.Error()) + } + + return builder.String() +} + +// Inner implements hasInnerError.Inner() +func (err *Error) Inner() error { + if err.inner == nil { + return nil + } + return err.inner +} + +func (err *Error) Base(e error) *Error { + err.inner = e + return err +} + +func (err *Error) atSeverity(s log.Severity) *Error { + err.severity = s + return err +} + +func (err *Error) Severity() log.Severity { + if err.inner == nil { + return err.severity + } + + if s, ok := err.inner.(hasSeverity); ok { + as := s.Severity() + if as < err.severity { + return as + } + } + + return err.severity +} + +// AtDebug sets the severity to debug. +func (err *Error) AtDebug() *Error { + return err.atSeverity(log.Severity_Debug) +} + +// AtInfo sets the severity to info. +func (err *Error) AtInfo() *Error { + return err.atSeverity(log.Severity_Info) +} + +// AtWarning sets the severity to warning. +func (err *Error) AtWarning() *Error { + return err.atSeverity(log.Severity_Warning) +} + +// AtError sets the severity to error. +func (err *Error) AtError() *Error { + return err.atSeverity(log.Severity_Error) +} + +// String returns the string representation of this error. +func (err *Error) String() string { + return err.Error() +} + +// WriteToLog writes current error into log. +func (err *Error) WriteToLog(opts ...ExportOption) { + var holder ExportOptionHolder + + for _, opt := range opts { + opt(&holder) + } + + if holder.SessionID > 0 { + err.prefix = append(err.prefix, holder.SessionID) + } + + log.Record(&log.GeneralMessage{ + Severity: GetSeverity(err), + Content: err, + }) +} + +type ExportOptionHolder struct { + SessionID uint32 +} + +type ExportOption func(*ExportOptionHolder) + +// New returns a new error object with message formed from given arguments. +func New(msg ...interface{}) *Error { + return &Error{ + message: msg, + severity: log.Severity_Info, + } +} + +// Cause returns the root cause of this error. +func Cause(err error) error { + if err == nil { + return nil + } +L: + for { + switch inner := err.(type) { + case hasInnerError: + if inner.Inner() == nil { + break L + } + err = inner.Inner() + case *os.PathError: + if inner.Err == nil { + break L + } + err = inner.Err + case *os.SyscallError: + if inner.Err == nil { + break L + } + err = inner.Err + default: + break L + } + } + return err +} + +// GetSeverity returns the actual severity of the error, including inner errors. +func GetSeverity(err error) log.Severity { + if s, ok := err.(hasSeverity); ok { + return s.Severity() + } + return log.Severity_Info +} diff --git a/common/errors/errors_test.go b/common/errors/errors_test.go new file mode 100644 index 00000000..5d87aca4 --- /dev/null +++ b/common/errors/errors_test.go @@ -0,0 +1,62 @@ +package errors_test + +import ( + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + . "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/log" +) + +func TestError(t *testing.T) { + err := New("TestError") + if v := GetSeverity(err); v != log.Severity_Info { + t.Error("severity: ", v) + } + + err = New("TestError2").Base(io.EOF) + if v := GetSeverity(err); v != log.Severity_Info { + t.Error("severity: ", v) + } + + err = New("TestError3").Base(io.EOF).AtWarning() + if v := GetSeverity(err); v != log.Severity_Warning { + t.Error("severity: ", v) + } + + err = New("TestError4").Base(io.EOF).AtWarning() + err = New("TestError5").Base(err) + if v := GetSeverity(err); v != log.Severity_Warning { + t.Error("severity: ", v) + } + if v := err.Error(); !strings.Contains(v, "EOF") { + t.Error("error: ", v) + } +} + +type e struct{} + +func TestErrorMessage(t *testing.T) { + data := []struct { + err error + msg string + }{ + { + err: New("a").Base(New("b")).WithPathObj(e{}), + msg: "github.com/xtls/xray-core/v1/common/errors_test: a > b", + }, + { + err: New("a").Base(New("b").WithPathObj(e{})), + msg: "a > github.com/xtls/xray-core/v1/common/errors_test: b", + }, + } + + for _, d := range data { + if diff := cmp.Diff(d.msg, d.err.Error()); diff != "" { + t.Error(diff) + } + } +} diff --git a/common/errors/multi_error.go b/common/errors/multi_error.go new file mode 100644 index 00000000..8f19c97a --- /dev/null +++ b/common/errors/multi_error.go @@ -0,0 +1,30 @@ +package errors + +import ( + "strings" +) + +type multiError []error + +func (e multiError) Error() string { + var r strings.Builder + r.WriteString("multierr: ") + for _, err := range e { + r.WriteString(err.Error()) + r.WriteString(" | ") + } + return r.String() +} + +func Combine(maybeError ...error) error { + var errs multiError + for _, err := range maybeError { + if err != nil { + errs = append(errs, err) + } + } + if len(errs) == 0 { + return nil + } + return errs +} diff --git a/common/interfaces.go b/common/interfaces.go new file mode 100644 index 00000000..8d881e7d --- /dev/null +++ b/common/interfaces.go @@ -0,0 +1,68 @@ +package common + +import "github.com/xtls/xray-core/v1/common/errors" + +// Closable is the interface for objects that can release its resources. +// +// xray:api:beta +type Closable interface { + // Close release all resources used by this object, including goroutines. + Close() error +} + +// Interruptible is an interface for objects that can be stopped before its completion. +// +// xray:api:beta +type Interruptible interface { + Interrupt() +} + +// Close closes the obj if it is a Closable. +// +// xray:api:beta +func Close(obj interface{}) error { + if c, ok := obj.(Closable); ok { + return c.Close() + } + return nil +} + +// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface. +// +// xray:api:beta +func Interrupt(obj interface{}) error { + if c, ok := obj.(Interruptible); ok { + c.Interrupt() + return nil + } + return Close(obj) +} + +// Runnable is the interface for objects that can start to work and stop on demand. +type Runnable interface { + // Start starts the runnable object. Upon the method returning nil, the object begins to function properly. + Start() error + + Closable +} + +// HasType is the interface for objects that knows its type. +type HasType interface { + // Type returns the type of the object. + // Usually it returns (*Type)(nil) of the object. + Type() interface{} +} + +// ChainedClosable is a Closable that consists of multiple Closable objects. +type ChainedClosable []Closable + +// Close implements Closable. +func (cc ChainedClosable) Close() error { + var errs []error + for _, c := range cc { + if err := c.Close(); err != nil { + errs = append(errs, err) + } + } + return errors.Combine(errs...) +} diff --git a/common/log/access.go b/common/log/access.go new file mode 100644 index 00000000..c46dfda1 --- /dev/null +++ b/common/log/access.go @@ -0,0 +1,64 @@ +package log + +import ( + "context" + "strings" + + "github.com/xtls/xray-core/v1/common/serial" +) + +type logKey int + +const ( + accessMessageKey logKey = iota +) + +type AccessStatus string + +const ( + AccessAccepted = AccessStatus("accepted") + AccessRejected = AccessStatus("rejected") +) + +type AccessMessage struct { + From interface{} + To interface{} + Status AccessStatus + Reason interface{} + Email string + Detour string +} + +func (m *AccessMessage) String() string { + builder := strings.Builder{} + builder.WriteString(serial.ToString(m.From)) + builder.WriteByte(' ') + builder.WriteString(string(m.Status)) + builder.WriteByte(' ') + builder.WriteString(serial.ToString(m.To)) + builder.WriteByte(' ') + if len(m.Detour) > 0 { + builder.WriteByte('[') + builder.WriteString(m.Detour) + builder.WriteString("] ") + } + builder.WriteString(serial.ToString(m.Reason)) + + if len(m.Email) > 0 { + builder.WriteString("email:") + builder.WriteString(m.Email) + builder.WriteByte(' ') + } + return builder.String() +} + +func ContextWithAccessMessage(ctx context.Context, accessMessage *AccessMessage) context.Context { + return context.WithValue(ctx, accessMessageKey, accessMessage) +} + +func AccessMessageFromContext(ctx context.Context) *AccessMessage { + if accessMessage, ok := ctx.Value(accessMessageKey).(*AccessMessage); ok { + return accessMessage + } + return nil +} diff --git a/common/log/log.go b/common/log/log.go new file mode 100644 index 00000000..49dad3c2 --- /dev/null +++ b/common/log/log.go @@ -0,0 +1,66 @@ +package log // import "github.com/xtls/xray-core/v1/common/log" + +import ( + "sync" + + "github.com/xtls/xray-core/v1/common/serial" +) + +// Message is the interface for all log messages. +type Message interface { + String() string +} + +// Handler is the interface for log handler. +type Handler interface { + Handle(msg Message) +} + +// GeneralMessage is a general log message that can contain all kind of content. +type GeneralMessage struct { + Severity Severity + Content interface{} +} + +// String implements Message. +func (m *GeneralMessage) String() string { + return serial.Concat("[", m.Severity, "] ", m.Content) +} + +// Record writes a message into log stream. +func Record(msg Message) { + logHandler.Handle(msg) +} + +var ( + logHandler syncHandler +) + +// RegisterHandler register a new handler as current log handler. Previous registered handler will be discarded. +func RegisterHandler(handler Handler) { + if handler == nil { + panic("Log handler is nil") + } + logHandler.Set(handler) +} + +type syncHandler struct { + sync.RWMutex + Handler +} + +func (h *syncHandler) Handle(msg Message) { + h.RLock() + defer h.RUnlock() + + if h.Handler != nil { + h.Handler.Handle(msg) + } +} + +func (h *syncHandler) Set(handler Handler) { + h.Lock() + defer h.Unlock() + + h.Handler = handler +} diff --git a/common/log/log.pb.go b/common/log/log.pb.go new file mode 100644 index 00000000..9c6f06c5 --- /dev/null +++ b/common/log/log.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/log/log.proto + +package log + +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 Severity int32 + +const ( + Severity_Unknown Severity = 0 + Severity_Error Severity = 1 + Severity_Warning Severity = 2 + Severity_Info Severity = 3 + Severity_Debug Severity = 4 +) + +// Enum value maps for Severity. +var ( + Severity_name = map[int32]string{ + 0: "Unknown", + 1: "Error", + 2: "Warning", + 3: "Info", + 4: "Debug", + } + Severity_value = map[string]int32{ + "Unknown": 0, + "Error": 1, + "Warning": 2, + "Info": 3, + "Debug": 4, + } +) + +func (x Severity) Enum() *Severity { + p := new(Severity) + *p = x + return p +} + +func (x Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Severity) Descriptor() protoreflect.EnumDescriptor { + return file_common_log_log_proto_enumTypes[0].Descriptor() +} + +func (Severity) Type() protoreflect.EnumType { + return &file_common_log_log_proto_enumTypes[0] +} + +func (x Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Severity.Descriptor instead. +func (Severity) EnumDescriptor() ([]byte, []int) { + return file_common_log_log_proto_rawDescGZIP(), []int{0} +} + +var File_common_log_log_proto protoreflect.FileDescriptor + +var file_common_log_log_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6c, 0x6f, 0x67, 0x2f, 0x6c, 0x6f, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x6c, 0x6f, 0x67, 0x2a, 0x44, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, + 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, + 0x12, 0x09, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, + 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, + 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x65, 0x62, 0x75, 0x67, 0x10, 0x04, 0x42, 0x52, 0x0a, + 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x6c, 0x6f, 0x67, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6c, 0x6f, 0x67, 0xaa, + 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, + 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_log_log_proto_rawDescOnce sync.Once + file_common_log_log_proto_rawDescData = file_common_log_log_proto_rawDesc +) + +func file_common_log_log_proto_rawDescGZIP() []byte { + file_common_log_log_proto_rawDescOnce.Do(func() { + file_common_log_log_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_log_log_proto_rawDescData) + }) + return file_common_log_log_proto_rawDescData +} + +var file_common_log_log_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_log_log_proto_goTypes = []interface{}{ + (Severity)(0), // 0: xray.common.log.Severity +} +var file_common_log_log_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_common_log_log_proto_init() } +func file_common_log_log_proto_init() { + if File_common_log_log_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_common_log_log_proto_rawDesc, + NumEnums: 1, + NumMessages: 0, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_log_log_proto_goTypes, + DependencyIndexes: file_common_log_log_proto_depIdxs, + EnumInfos: file_common_log_log_proto_enumTypes, + }.Build() + File_common_log_log_proto = out.File + file_common_log_log_proto_rawDesc = nil + file_common_log_log_proto_goTypes = nil + file_common_log_log_proto_depIdxs = nil +} diff --git a/common/log/log.proto b/common/log/log.proto new file mode 100644 index 00000000..c5e83e20 --- /dev/null +++ b/common/log/log.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.common.log; +option csharp_namespace = "Xray.Common.Log"; +option go_package = "github.com/xtls/xray-core/v1/common/log"; +option java_package = "com.xray.common.log"; +option java_multiple_files = true; + +enum Severity { + Unknown = 0; + Error = 1; + Warning = 2; + Info = 3; + Debug = 4; +} diff --git a/common/log/log_test.go b/common/log/log_test.go new file mode 100644 index 00000000..38b6b268 --- /dev/null +++ b/common/log/log_test.go @@ -0,0 +1,33 @@ +package log_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common/log" + "github.com/xtls/xray-core/v1/common/net" +) + +type testLogger struct { + value string +} + +func (l *testLogger) Handle(msg log.Message) { + l.value = msg.String() +} + +func TestLogRecord(t *testing.T) { + var logger testLogger + log.RegisterHandler(&logger) + + ip := "8.8.8.8" + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: net.ParseAddress(ip), + }) + + if diff := cmp.Diff("[Error] "+ip, logger.value); diff != "" { + t.Error(diff) + } +} diff --git a/common/log/logger.go b/common/log/logger.go new file mode 100644 index 00000000..e581be01 --- /dev/null +++ b/common/log/logger.go @@ -0,0 +1,152 @@ +package log + +import ( + "io" + "log" + "os" + "time" + + "github.com/xtls/xray-core/v1/common/platform" + "github.com/xtls/xray-core/v1/common/signal/done" + "github.com/xtls/xray-core/v1/common/signal/semaphore" +) + +// Writer is the interface for writing logs. +type Writer interface { + Write(string) error + io.Closer +} + +// WriterCreator is a function to create LogWriters. +type WriterCreator func() Writer + +type generalLogger struct { + creator WriterCreator + buffer chan Message + access *semaphore.Instance + done *done.Instance +} + +// NewLogger returns a generic log handler that can handle all type of messages. +func NewLogger(logWriterCreator WriterCreator) Handler { + return &generalLogger{ + creator: logWriterCreator, + buffer: make(chan Message, 16), + access: semaphore.New(1), + done: done.New(), + } +} + +func (l *generalLogger) run() { + defer l.access.Signal() + + dataWritten := false + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + logger := l.creator() + if logger == nil { + return + } + defer logger.Close() + + for { + select { + case <-l.done.Wait(): + return + case msg := <-l.buffer: + logger.Write(msg.String() + platform.LineSeparator()) + dataWritten = true + case <-ticker.C: + if !dataWritten { + return + } + dataWritten = false + } + } +} + +func (l *generalLogger) Handle(msg Message) { + select { + case l.buffer <- msg: + default: + } + + select { + case <-l.access.Wait(): + go l.run() + default: + } +} + +func (l *generalLogger) Close() error { + return l.done.Close() +} + +type consoleLogWriter struct { + logger *log.Logger +} + +func (w *consoleLogWriter) Write(s string) error { + w.logger.Print(s) + return nil +} + +func (w *consoleLogWriter) Close() error { + return nil +} + +type fileLogWriter struct { + file *os.File + logger *log.Logger +} + +func (w *fileLogWriter) Write(s string) error { + w.logger.Print(s) + return nil +} + +func (w *fileLogWriter) Close() error { + return w.file.Close() +} + +// CreateStdoutLogWriter returns a LogWriterCreator that creates LogWriter for stdout. +func CreateStdoutLogWriter() WriterCreator { + return func() Writer { + return &consoleLogWriter{ + logger: log.New(os.Stdout, "", log.Ldate|log.Ltime), + } + } +} + +// CreateStderrLogWriter returns a LogWriterCreator that creates LogWriter for stderr. +func CreateStderrLogWriter() WriterCreator { + return func() Writer { + return &consoleLogWriter{ + logger: log.New(os.Stderr, "", log.Ldate|log.Ltime), + } + } +} + +// CreateFileLogWriter returns a LogWriterCreator that creates LogWriter for the given file. +func CreateFileLogWriter(path string) (WriterCreator, error) { + file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return nil, err + } + file.Close() + return func() Writer { + file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return nil + } + return &fileLogWriter{ + file: file, + logger: log.New(file, "", log.Ldate|log.Ltime), + } + }, nil +} + +func init() { + RegisterHandler(NewLogger(CreateStdoutLogWriter())) +} diff --git a/common/log/logger_test.go b/common/log/logger_test.go new file mode 100644 index 00000000..6ab91bf7 --- /dev/null +++ b/common/log/logger_test.go @@ -0,0 +1,39 @@ +package log_test + +import ( + "io/ioutil" + "os" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/common/log" +) + +func TestFileLogger(t *testing.T) { + f, err := ioutil.TempFile("", "vtest") + common.Must(err) + path := f.Name() + common.Must(f.Close()) + + creator, err := CreateFileLogWriter(path) + common.Must(err) + + handler := NewLogger(creator) + handler.Handle(&GeneralMessage{Content: "Test Log"}) + time.Sleep(2 * time.Second) + + common.Must(common.Close(handler)) + + f, err = os.Open(path) + common.Must(err) + defer f.Close() + + b, err := buf.ReadAllToBytes(f) + common.Must(err) + if !strings.Contains(string(b), "Test Log") { + t.Fatal("Expect log text contains 'Test Log', but actually: ", string(b)) + } +} diff --git a/common/mux/client.go b/common/mux/client.go new file mode 100644 index 00000000..0395e36f --- /dev/null +++ b/common/mux/client.go @@ -0,0 +1,402 @@ +package mux + +import ( + "context" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "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/common/signal/done" + "github.com/xtls/xray-core/v1/common/task" + "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/pipe" +) + +type ClientManager struct { + Enabled bool // wheather mux is enabled from user config + Picker WorkerPicker +} + +func (m *ClientManager) Dispatch(ctx context.Context, link *transport.Link) error { + for i := 0; i < 16; i++ { + worker, err := m.Picker.PickAvailable() + if err != nil { + return err + } + if worker.Dispatch(ctx, link) { + return nil + } + } + + return newError("unable to find an available mux client").AtWarning() +} + +type WorkerPicker interface { + PickAvailable() (*ClientWorker, error) +} + +type IncrementalWorkerPicker struct { + Factory ClientWorkerFactory + + access sync.Mutex + workers []*ClientWorker + cleanupTask *task.Periodic +} + +func (p *IncrementalWorkerPicker) cleanupFunc() error { + p.access.Lock() + defer p.access.Unlock() + + if len(p.workers) == 0 { + return newError("no worker") + } + + p.cleanup() + return nil +} + +func (p *IncrementalWorkerPicker) cleanup() { + var activeWorkers []*ClientWorker + for _, w := range p.workers { + if !w.Closed() { + activeWorkers = append(activeWorkers, w) + } + } + p.workers = activeWorkers +} + +func (p *IncrementalWorkerPicker) findAvailable() int { + for idx, w := range p.workers { + if !w.IsFull() { + return idx + } + } + + return -1 +} + +func (p *IncrementalWorkerPicker) pickInternal() (*ClientWorker, bool, error) { + p.access.Lock() + defer p.access.Unlock() + + idx := p.findAvailable() + if idx >= 0 { + n := len(p.workers) + if n > 1 && idx != n-1 { + p.workers[n-1], p.workers[idx] = p.workers[idx], p.workers[n-1] + } + return p.workers[idx], false, nil + } + + p.cleanup() + + worker, err := p.Factory.Create() + if err != nil { + return nil, false, err + } + p.workers = append(p.workers, worker) + + if p.cleanupTask == nil { + p.cleanupTask = &task.Periodic{ + Interval: time.Second * 30, + Execute: p.cleanupFunc, + } + } + + return worker, true, nil +} + +func (p *IncrementalWorkerPicker) PickAvailable() (*ClientWorker, error) { + worker, start, err := p.pickInternal() + if start { + common.Must(p.cleanupTask.Start()) + } + + return worker, err +} + +type ClientWorkerFactory interface { + Create() (*ClientWorker, error) +} + +type DialingWorkerFactory struct { + Proxy proxy.Outbound + Dialer internet.Dialer + Strategy ClientStrategy +} + +func (f *DialingWorkerFactory) Create() (*ClientWorker, error) { + opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)} + uplinkReader, upLinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + + c, err := NewClientWorker(transport.Link{ + Reader: downlinkReader, + Writer: upLinkWriter, + }, f.Strategy) + + if err != nil { + return nil, err + } + + go func(p proxy.Outbound, d internet.Dialer, c common.Closable) { + ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{ + Target: net.TCPDestination(muxCoolAddress, muxCoolPort), + }) + ctx, cancel := context.WithCancel(ctx) + + if err := p.Process(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter}, d); err != nil { + errors.New("failed to handler mux client connection").Base(err).WriteToLog() + } + common.Must(c.Close()) + cancel() + }(f.Proxy, f.Dialer, c.done) + + return c, nil +} + +type ClientStrategy struct { + MaxConcurrency uint32 + MaxConnection uint32 +} + +type ClientWorker struct { + sessionManager *SessionManager + link transport.Link + done *done.Instance + strategy ClientStrategy +} + +var muxCoolAddress = net.DomainAddress("v1.mux.cool") +var muxCoolPort = net.Port(9527) + +// NewClientWorker creates a new mux.Client. +func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, error) { + c := &ClientWorker{ + sessionManager: NewSessionManager(), + link: stream, + done: done.New(), + strategy: s, + } + + go c.fetchOutput() + go c.monitor() + + return c, nil +} + +func (m *ClientWorker) TotalConnections() uint32 { + return uint32(m.sessionManager.Count()) +} + +func (m *ClientWorker) ActiveConnections() uint32 { + return uint32(m.sessionManager.Size()) +} + +// Closed returns true if this Client is closed. +func (m *ClientWorker) Closed() bool { + return m.done.Done() +} + +func (m *ClientWorker) monitor() { + timer := time.NewTicker(time.Second * 16) + defer timer.Stop() + + for { + select { + case <-m.done.Wait(): + m.sessionManager.Close() + common.Close(m.link.Writer) + common.Interrupt(m.link.Reader) + return + case <-timer.C: + size := m.sessionManager.Size() + if size == 0 && m.sessionManager.CloseIfNoSession() { + common.Must(m.done.Close()) + } + } + } +} + +func writeFirstPayload(reader buf.Reader, writer *Writer) error { + err := buf.CopyOnceTimeout(reader, writer, time.Millisecond*100) + if err == buf.ErrNotTimeoutReader || err == buf.ErrReadTimeout { + return writer.WriteMultiBuffer(buf.MultiBuffer{}) + } + + if err != nil { + return err + } + + return nil +} + +func fetchInput(ctx context.Context, s *Session, output buf.Writer) { + dest := session.OutboundFromContext(ctx).Target + transferType := protocol.TransferTypeStream + if dest.Network == net.Network_UDP { + transferType = protocol.TransferTypePacket + } + s.transferType = transferType + writer := NewWriter(s.ID, dest, output, transferType) + defer s.Close() + defer writer.Close() + + newError("dispatching request to ", dest).WriteToLog(session.ExportIDToError(ctx)) + if err := writeFirstPayload(s.input, writer); err != nil { + newError("failed to write first payload").Base(err).WriteToLog(session.ExportIDToError(ctx)) + writer.hasError = true + common.Interrupt(s.input) + return + } + + if err := buf.Copy(s.input, writer); err != nil { + newError("failed to fetch all input").Base(err).WriteToLog(session.ExportIDToError(ctx)) + writer.hasError = true + common.Interrupt(s.input) + return + } +} + +func (m *ClientWorker) IsClosing() bool { + sm := m.sessionManager + if m.strategy.MaxConnection > 0 && sm.Count() >= int(m.strategy.MaxConnection) { + return true + } + return false +} + +func (m *ClientWorker) IsFull() bool { + if m.IsClosing() || m.Closed() { + return true + } + + sm := m.sessionManager + if m.strategy.MaxConcurrency > 0 && sm.Size() >= int(m.strategy.MaxConcurrency) { + return true + } + return false +} + +func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool { + if m.IsFull() || m.Closed() { + return false + } + + sm := m.sessionManager + s := sm.Allocate() + if s == nil { + return false + } + s.input = link.Reader + s.output = link.Writer + go fetchInput(ctx, s, m.link.Writer) + return true +} + +func (m *ClientWorker) handleStatueKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) handleStatusNew(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.BufferedReader) error { + if !meta.Option.Has(OptionData) { + return nil + } + + s, found := m.sessionManager.Get(meta.SessionID) + if !found { + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, m.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + + rr := s.NewReader(reader) + err := buf.Copy(rr, s.output) + if err != nil && buf.IsWriteError(err) { + newError("failed to write to downstream. closing session ", s.ID).Base(err).WriteToLog() + + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, m.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + drainErr := buf.Copy(rr, buf.Discard) + common.Interrupt(s.input) + s.Close() + return drainErr + } + + return err +} + +func (m *ClientWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.BufferedReader) error { + if s, found := m.sessionManager.Get(meta.SessionID); found { + if meta.Option.Has(OptionError) { + common.Interrupt(s.input) + common.Interrupt(s.output) + } + s.Close() + } + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) fetchOutput() { + defer func() { + common.Must(m.done.Close()) + }() + + reader := &buf.BufferedReader{Reader: m.link.Reader} + + var meta FrameMetadata + for { + err := meta.Unmarshal(reader) + if err != nil { + if errors.Cause(err) != io.EOF { + newError("failed to read metadata").Base(err).WriteToLog() + } + break + } + + switch meta.SessionStatus { + case SessionStatusKeepAlive: + err = m.handleStatueKeepAlive(&meta, reader) + case SessionStatusEnd: + err = m.handleStatusEnd(&meta, reader) + case SessionStatusNew: + err = m.handleStatusNew(&meta, reader) + case SessionStatusKeep: + err = m.handleStatusKeep(&meta, reader) + default: + status := meta.SessionStatus + newError("unknown status: ", status).AtError().WriteToLog() + return + } + + if err != nil { + newError("failed to process data").Base(err).WriteToLog() + return + } + } +} diff --git a/common/mux/client_test.go b/common/mux/client_test.go new file mode 100644 index 00000000..e31b9c4b --- /dev/null +++ b/common/mux/client_test.go @@ -0,0 +1,116 @@ +package mux_test + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/xtls/xray-core/v1/common" + "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/common/session" + "github.com/xtls/xray-core/v1/testing/mocks" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +func TestIncrementalPickerFailure(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockWorkerFactory := mocks.NewMuxClientWorkerFactory(mockCtl) + mockWorkerFactory.EXPECT().Create().Return(nil, errors.New("test")) + + picker := mux.IncrementalWorkerPicker{ + Factory: mockWorkerFactory, + } + + _, err := picker.PickAvailable() + if err == nil { + t.Error("expected error, but nil") + } +} + +func TestClientWorkerEOF(t *testing.T) { + reader, writer := pipe.New(pipe.WithoutSizeLimit()) + common.Must(writer.Close()) + + worker, err := mux.NewClientWorker(transport.Link{Reader: reader, Writer: writer}, mux.ClientStrategy{}) + common.Must(err) + + time.Sleep(time.Millisecond * 500) + + f := worker.Dispatch(context.Background(), nil) + if f { + t.Error("expected failed dispatching, but actually not") + } +} + +func TestClientWorkerClose(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + r1, w1 := pipe.New(pipe.WithoutSizeLimit()) + worker1, err := mux.NewClientWorker(transport.Link{ + Reader: r1, + Writer: w1, + }, mux.ClientStrategy{ + MaxConcurrency: 4, + MaxConnection: 4, + }) + common.Must(err) + + r2, w2 := pipe.New(pipe.WithoutSizeLimit()) + worker2, err := mux.NewClientWorker(transport.Link{ + Reader: r2, + Writer: w2, + }, mux.ClientStrategy{ + MaxConcurrency: 4, + MaxConnection: 4, + }) + common.Must(err) + + factory := mocks.NewMuxClientWorkerFactory(mockCtl) + gomock.InOrder( + factory.EXPECT().Create().Return(worker1, nil), + factory.EXPECT().Create().Return(worker2, nil), + ) + + picker := &mux.IncrementalWorkerPicker{ + Factory: factory, + } + manager := &mux.ClientManager{ + Picker: picker, + } + + tr1, tw1 := pipe.New(pipe.WithoutSizeLimit()) + ctx1 := session.ContextWithOutbound(context.Background(), &session.Outbound{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80), + }) + common.Must(manager.Dispatch(ctx1, &transport.Link{ + Reader: tr1, + Writer: tw1, + })) + defer tw1.Close() + + common.Must(w1.Close()) + + time.Sleep(time.Millisecond * 500) + if !worker1.Closed() { + t.Error("worker1 is not finished") + } + + tr2, tw2 := pipe.New(pipe.WithoutSizeLimit()) + ctx2 := session.ContextWithOutbound(context.Background(), &session.Outbound{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80), + }) + common.Must(manager.Dispatch(ctx2, &transport.Link{ + Reader: tr2, + Writer: tw2, + })) + defer tw2.Close() + + common.Must(w2.Close()) +} diff --git a/common/mux/errors.generated.go b/common/mux/errors.generated.go new file mode 100644 index 00000000..39cdfd1a --- /dev/null +++ b/common/mux/errors.generated.go @@ -0,0 +1,9 @@ +package mux + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/mux/frame.go b/common/mux/frame.go new file mode 100644 index 00000000..398ac42b --- /dev/null +++ b/common/mux/frame.go @@ -0,0 +1,145 @@ +package mux + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/bitmask" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" +) + +type SessionStatus byte + +const ( + SessionStatusNew SessionStatus = 0x01 + SessionStatusKeep SessionStatus = 0x02 + SessionStatusEnd SessionStatus = 0x03 + SessionStatusKeepAlive SessionStatus = 0x04 +) + +const ( + OptionData bitmask.Byte = 0x01 + OptionError bitmask.Byte = 0x02 +) + +type TargetNetwork byte + +const ( + TargetNetworkTCP TargetNetwork = 0x01 + TargetNetworkUDP TargetNetwork = 0x02 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) + +/* +Frame format +2 bytes - length +2 bytes - session id +1 bytes - status +1 bytes - option + +1 byte - network +2 bytes - port +n bytes - address + +*/ + +type FrameMetadata struct { + Target net.Destination + SessionID uint16 + Option bitmask.Byte + SessionStatus SessionStatus +} + +func (f FrameMetadata) WriteTo(b *buf.Buffer) error { + lenBytes := b.Extend(2) + + len0 := b.Len() + sessionBytes := b.Extend(2) + binary.BigEndian.PutUint16(sessionBytes, f.SessionID) + + common.Must(b.WriteByte(byte(f.SessionStatus))) + common.Must(b.WriteByte(byte(f.Option))) + + if f.SessionStatus == SessionStatusNew { + switch f.Target.Network { + case net.Network_TCP: + common.Must(b.WriteByte(byte(TargetNetworkTCP))) + case net.Network_UDP: + common.Must(b.WriteByte(byte(TargetNetworkUDP))) + } + + if err := addrParser.WriteAddressPort(b, f.Target.Address, f.Target.Port); err != nil { + return err + } + } + + len1 := b.Len() + binary.BigEndian.PutUint16(lenBytes, uint16(len1-len0)) + return nil +} + +// Unmarshal reads FrameMetadata from the given reader. +func (f *FrameMetadata) Unmarshal(reader io.Reader) error { + metaLen, err := serial.ReadUint16(reader) + if err != nil { + return err + } + if metaLen > 512 { + return newError("invalid metalen ", metaLen).AtError() + } + + b := buf.New() + defer b.Release() + + if _, err := b.ReadFullFrom(reader, int32(metaLen)); err != nil { + return err + } + return f.UnmarshalFromBuffer(b) +} + +// UnmarshalFromBuffer reads a FrameMetadata from the given buffer. +// Visible for testing only. +func (f *FrameMetadata) UnmarshalFromBuffer(b *buf.Buffer) error { + if b.Len() < 4 { + return newError("insufficient buffer: ", b.Len()) + } + + f.SessionID = binary.BigEndian.Uint16(b.BytesTo(2)) + f.SessionStatus = SessionStatus(b.Byte(2)) + f.Option = bitmask.Byte(b.Byte(3)) + f.Target.Network = net.Network_Unknown + + if f.SessionStatus == SessionStatusNew { + if b.Len() < 8 { + return newError("insufficient buffer: ", b.Len()) + } + network := TargetNetwork(b.Byte(4)) + b.Advance(5) + + addr, port, err := addrParser.ReadAddressPort(nil, b) + if err != nil { + return newError("failed to parse address and port").Base(err) + } + + switch network { + case TargetNetworkTCP: + f.Target = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Target = net.UDPDestination(addr, port) + default: + return newError("unknown network type: ", network) + } + } + + return nil +} diff --git a/common/mux/frame_test.go b/common/mux/frame_test.go new file mode 100644 index 00000000..e81c4d6e --- /dev/null +++ b/common/mux/frame_test.go @@ -0,0 +1,25 @@ +package mux_test + +import ( + "testing" + + "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" +) + +func BenchmarkFrameWrite(b *testing.B) { + frame := mux.FrameMetadata{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), net.Port(80)), + SessionID: 1, + SessionStatus: mux.SessionStatusNew, + } + writer := buf.New() + defer writer.Release() + + for i := 0; i < b.N; i++ { + common.Must(frame.WriteTo(writer)) + writer.Clear() + } +} diff --git a/common/mux/mux.go b/common/mux/mux.go new file mode 100644 index 00000000..db41742a --- /dev/null +++ b/common/mux/mux.go @@ -0,0 +1,3 @@ +package mux + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/common/mux/mux_test.go b/common/mux/mux_test.go new file mode 100644 index 00000000..c5e9ab79 --- /dev/null +++ b/common/mux/mux_test.go @@ -0,0 +1,196 @@ +package mux_test + +import ( + "io" + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +func readAll(reader buf.Reader) (buf.MultiBuffer, error) { + var mb buf.MultiBuffer + for { + b, err := reader.ReadMultiBuffer() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + mb = append(mb, b...) + } + return mb, nil +} + +func TestReaderWriter(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + + dest := net.TCPDestination(net.DomainAddress("example.com"), 80) + writer := NewWriter(1, dest, pWriter, protocol.TransferTypeStream) + + dest2 := net.TCPDestination(net.LocalHostIP, 443) + writer2 := NewWriter(2, dest2, pWriter, protocol.TransferTypeStream) + + dest3 := net.TCPDestination(net.LocalHostIPv6, 18374) + writer3 := NewWriter(3, dest3, pWriter, protocol.TransferTypeStream) + + writePayload := func(writer *Writer, payload ...byte) error { + b := buf.New() + b.Write(payload) + return writer.WriteMultiBuffer(buf.MultiBuffer{b}) + } + + common.Must(writePayload(writer, 'a', 'b', 'c', 'd')) + common.Must(writePayload(writer2)) + + common.Must(writePayload(writer, 'e', 'f', 'g', 'h')) + common.Must(writePayload(writer3, 'x')) + + writer.Close() + writer3.Close() + + common.Must(writePayload(writer2, 'y')) + writer2.Close() + + bytesReader := &buf.BufferedReader{Reader: pReader} + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusNew, + Target: dest, + Option: OptionData, + }); r != "" { + t.Error("metadata: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "abcd" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionStatus: SessionStatusNew, + SessionID: 2, + Option: 0, + Target: dest2, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusKeep, + Option: 1, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "efgh" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 3, + SessionStatus: SessionStatusNew, + Option: 1, + Target: dest3, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "x" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 3, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 2, + SessionStatus: SessionStatusKeep, + Option: 1, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "y" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 2, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + pWriter.Close() + + { + var meta FrameMetadata + err := meta.Unmarshal(bytesReader) + if err == nil { + t.Error("nil error") + } + } +} diff --git a/common/mux/reader.go b/common/mux/reader.go new file mode 100644 index 00000000..31f39132 --- /dev/null +++ b/common/mux/reader.go @@ -0,0 +1,52 @@ +package mux + +import ( + "io" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "github.com/xtls/xray-core/v1/common/serial" +) + +// PacketReader is an io.Reader that reads whole chunk of Mux frames every time. +type PacketReader struct { + reader io.Reader + eof bool +} + +// NewPacketReader creates a new PacketReader. +func NewPacketReader(reader io.Reader) *PacketReader { + return &PacketReader{ + reader: reader, + eof: false, + } +} + +// ReadMultiBuffer implements buf.Reader. +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + if r.eof { + return nil, io.EOF + } + + size, err := serial.ReadUint16(r.reader) + if err != nil { + return nil, err + } + + if size > buf.Size { + return nil, newError("packet size too large: ", size) + } + + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, int32(size)); err != nil { + b.Release() + return nil, err + } + r.eof = true + return buf.MultiBuffer{b}, nil +} + +// NewStreamReader creates a new StreamReader. +func NewStreamReader(reader *buf.BufferedReader) buf.Reader { + return crypto.NewChunkStreamReaderWithChunkCount(crypto.PlainChunkSizeParser{}, reader, 1) +} diff --git a/common/mux/server.go b/common/mux/server.go new file mode 100644 index 00000000..1bf96dd2 --- /dev/null +++ b/common/mux/server.go @@ -0,0 +1,252 @@ +package mux + +import ( + "context" + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "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/routing" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +type Server struct { + dispatcher routing.Dispatcher +} + +// NewServer creates a new mux.Server. +func NewServer(ctx context.Context) *Server { + s := &Server{} + core.RequireFeatures(ctx, func(d routing.Dispatcher) { + s.dispatcher = d + }) + return s +} + +// Type implements common.HasType. +func (s *Server) Type() interface{} { + return s.dispatcher.Type() +} + +// Dispatch implements routing.Dispatcher +func (s *Server) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + if dest.Address != muxCoolAddress { + return s.dispatcher.Dispatch(ctx, dest) + } + + opts := pipe.OptionsFromContext(ctx) + uplinkReader, uplinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + + _, err := NewServerWorker(ctx, s.dispatcher, &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + }) + if err != nil { + return nil, err + } + + return &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, nil +} + +// Start implements common.Runnable. +func (s *Server) Start() error { + return nil +} + +// Close implements common.Closable. +func (s *Server) Close() error { + return nil +} + +type ServerWorker struct { + dispatcher routing.Dispatcher + link *transport.Link + sessionManager *SessionManager +} + +func NewServerWorker(ctx context.Context, d routing.Dispatcher, link *transport.Link) (*ServerWorker, error) { + worker := &ServerWorker{ + dispatcher: d, + link: link, + sessionManager: NewSessionManager(), + } + go worker.run(ctx) + return worker, nil +} + +func handle(ctx context.Context, s *Session, output buf.Writer) { + writer := NewResponseWriter(s.ID, output, s.transferType) + if err := buf.Copy(s.input, writer); err != nil { + newError("session ", s.ID, " ends.").Base(err).WriteToLog(session.ExportIDToError(ctx)) + writer.hasError = true + } + + writer.Close() + s.Close() +} + +func (w *ServerWorker) ActiveConnections() uint32 { + return uint32(w.sessionManager.Size()) +} + +func (w *ServerWorker) Closed() bool { + return w.sessionManager.Closed() +} + +func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error { + newError("received request for ", meta.Target).WriteToLog(session.ExportIDToError(ctx)) + { + msg := &log.AccessMessage{ + To: meta.Target, + Status: log.AccessAccepted, + Reason: "", + } + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + msg.From = inbound.Source + msg.Email = inbound.User.Email + } + ctx = log.ContextWithAccessMessage(ctx, msg) + } + link, err := w.dispatcher.Dispatch(ctx, meta.Target) + if err != nil { + if meta.Option.Has(OptionData) { + buf.Copy(NewStreamReader(reader), buf.Discard) + } + return newError("failed to dispatch request.").Base(err) + } + s := &Session{ + input: link.Reader, + output: link.Writer, + parent: w.sessionManager, + ID: meta.SessionID, + transferType: protocol.TransferTypeStream, + } + if meta.Target.Network == net.Network_UDP { + s.transferType = protocol.TransferTypePacket + } + w.sessionManager.Add(s) + go handle(ctx, s, w.link.Writer) + if !meta.Option.Has(OptionData) { + return nil + } + + rr := s.NewReader(reader) + if err := buf.Copy(rr, s.output); err != nil { + buf.Copy(rr, buf.Discard) + common.Interrupt(s.input) + return s.Close() + } + return nil +} + +func (w *ServerWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.BufferedReader) error { + if !meta.Option.Has(OptionData) { + return nil + } + + s, found := w.sessionManager.Get(meta.SessionID) + if !found { + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, w.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + + rr := s.NewReader(reader) + err := buf.Copy(rr, s.output) + + if err != nil && buf.IsWriteError(err) { + newError("failed to write to downstream writer. closing session ", s.ID).Base(err).WriteToLog() + + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, w.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + drainErr := buf.Copy(rr, buf.Discard) + common.Interrupt(s.input) + s.Close() + return drainErr + } + + return err +} + +func (w *ServerWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.BufferedReader) error { + if s, found := w.sessionManager.Get(meta.SessionID); found { + if meta.Option.Has(OptionError) { + common.Interrupt(s.input) + common.Interrupt(s.output) + } + s.Close() + } + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedReader) error { + var meta FrameMetadata + err := meta.Unmarshal(reader) + if err != nil { + return newError("failed to read metadata").Base(err) + } + + switch meta.SessionStatus { + case SessionStatusKeepAlive: + err = w.handleStatusKeepAlive(&meta, reader) + case SessionStatusEnd: + err = w.handleStatusEnd(&meta, reader) + case SessionStatusNew: + err = w.handleStatusNew(ctx, &meta, reader) + case SessionStatusKeep: + err = w.handleStatusKeep(&meta, reader) + default: + status := meta.SessionStatus + return newError("unknown status: ", status).AtError() + } + + if err != nil { + return newError("failed to process data").Base(err) + } + return nil +} + +func (w *ServerWorker) run(ctx context.Context) { + input := w.link.Reader + reader := &buf.BufferedReader{Reader: input} + + defer w.sessionManager.Close() + + for { + select { + case <-ctx.Done(): + return + default: + err := w.handleFrame(ctx, reader) + if err != nil { + if errors.Cause(err) != io.EOF { + newError("unexpected EOF").Base(err).WriteToLog(session.ExportIDToError(ctx)) + common.Interrupt(input) + } + return + } + } + } +} diff --git a/common/mux/session.go b/common/mux/session.go new file mode 100644 index 00000000..2d009a4c --- /dev/null +++ b/common/mux/session.go @@ -0,0 +1,160 @@ +package mux + +import ( + "sync" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/protocol" +) + +type SessionManager struct { + sync.RWMutex + sessions map[uint16]*Session + count uint16 + closed bool +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + count: 0, + sessions: make(map[uint16]*Session, 16), + } +} + +func (m *SessionManager) Closed() bool { + m.RLock() + defer m.RUnlock() + + return m.closed +} + +func (m *SessionManager) Size() int { + m.RLock() + defer m.RUnlock() + + return len(m.sessions) +} + +func (m *SessionManager) Count() int { + m.RLock() + defer m.RUnlock() + + return int(m.count) +} + +func (m *SessionManager) Allocate() *Session { + m.Lock() + defer m.Unlock() + + if m.closed { + return nil + } + + m.count++ + s := &Session{ + ID: m.count, + parent: m, + } + m.sessions[s.ID] = s + return s +} + +func (m *SessionManager) Add(s *Session) { + m.Lock() + defer m.Unlock() + + if m.closed { + return + } + + m.count++ + m.sessions[s.ID] = s +} + +func (m *SessionManager) Remove(id uint16) { + m.Lock() + defer m.Unlock() + + if m.closed { + return + } + + delete(m.sessions, id) + + if len(m.sessions) == 0 { + m.sessions = make(map[uint16]*Session, 16) + } +} + +func (m *SessionManager) Get(id uint16) (*Session, bool) { + m.RLock() + defer m.RUnlock() + + if m.closed { + return nil, false + } + + s, found := m.sessions[id] + return s, found +} + +func (m *SessionManager) CloseIfNoSession() bool { + m.Lock() + defer m.Unlock() + + if m.closed { + return true + } + + if len(m.sessions) != 0 { + return false + } + + m.closed = true + return true +} + +func (m *SessionManager) Close() error { + m.Lock() + defer m.Unlock() + + if m.closed { + return nil + } + + m.closed = true + + for _, s := range m.sessions { + common.Close(s.input) + common.Close(s.output) + } + + m.sessions = nil + return nil +} + +// Session represents a client connection in a Mux connection. +type Session struct { + input buf.Reader + output buf.Writer + parent *SessionManager + ID uint16 + transferType protocol.TransferType +} + +// Close closes all resources associated with this session. +func (s *Session) Close() error { + common.Close(s.output) + common.Close(s.input) + s.parent.Remove(s.ID) + return nil +} + +// NewReader creates a buf.Reader based on the transfer type of this Session. +func (s *Session) NewReader(reader *buf.BufferedReader) buf.Reader { + if s.transferType == protocol.TransferTypeStream { + return NewStreamReader(reader) + } + return NewPacketReader(reader) +} diff --git a/common/mux/session_test.go b/common/mux/session_test.go new file mode 100644 index 00000000..2719fe28 --- /dev/null +++ b/common/mux/session_test.go @@ -0,0 +1,51 @@ +package mux_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/mux" +) + +func TestSessionManagerAdd(t *testing.T) { + m := NewSessionManager() + + s := m.Allocate() + if s.ID != 1 { + t.Error("id: ", s.ID) + } + if m.Size() != 1 { + t.Error("size: ", m.Size()) + } + + s = m.Allocate() + if s.ID != 2 { + t.Error("id: ", s.ID) + } + if m.Size() != 2 { + t.Error("size: ", m.Size()) + } + + s = &Session{ + ID: 4, + } + m.Add(s) + if s.ID != 4 { + t.Error("id: ", s.ID) + } + if m.Size() != 3 { + t.Error("size: ", m.Size()) + } +} + +func TestSessionManagerClose(t *testing.T) { + m := NewSessionManager() + s := m.Allocate() + + if m.CloseIfNoSession() { + t.Error("able to close") + } + m.Remove(s.ID) + if !m.CloseIfNoSession() { + t.Error("not able to close") + } +} diff --git a/common/mux/writer.go b/common/mux/writer.go new file mode 100644 index 00000000..48455bc4 --- /dev/null +++ b/common/mux/writer.go @@ -0,0 +1,126 @@ +package mux + +import ( + "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/protocol" + "github.com/xtls/xray-core/v1/common/serial" +) + +type Writer struct { + dest net.Destination + writer buf.Writer + id uint16 + followup bool + hasError bool + transferType protocol.TransferType +} + +func NewWriter(id uint16, dest net.Destination, writer buf.Writer, transferType protocol.TransferType) *Writer { + return &Writer{ + id: id, + dest: dest, + writer: writer, + followup: false, + transferType: transferType, + } +} + +func NewResponseWriter(id uint16, writer buf.Writer, transferType protocol.TransferType) *Writer { + return &Writer{ + id: id, + writer: writer, + followup: true, + transferType: transferType, + } +} + +func (w *Writer) getNextFrameMeta() FrameMetadata { + meta := FrameMetadata{ + SessionID: w.id, + Target: w.dest, + } + + if w.followup { + meta.SessionStatus = SessionStatusKeep + } else { + w.followup = true + meta.SessionStatus = SessionStatusNew + } + + return meta +} + +func (w *Writer) writeMetaOnly() error { + meta := w.getNextFrameMeta() + b := buf.New() + if err := meta.WriteTo(b); err != nil { + return err + } + return w.writer.WriteMultiBuffer(buf.MultiBuffer{b}) +} + +func writeMetaWithFrame(writer buf.Writer, meta FrameMetadata, data buf.MultiBuffer) error { + frame := buf.New() + if err := meta.WriteTo(frame); err != nil { + return err + } + if _, err := serial.WriteUint16(frame, uint16(data.Len())); err != nil { + return err + } + + mb2 := make(buf.MultiBuffer, 0, len(data)+1) + mb2 = append(mb2, frame) + mb2 = append(mb2, data...) + return writer.WriteMultiBuffer(mb2) +} + +func (w *Writer) writeData(mb buf.MultiBuffer) error { + meta := w.getNextFrameMeta() + meta.Option.Set(OptionData) + + return writeMetaWithFrame(w.writer, meta, mb) +} + +// WriteMultiBuffer implements buf.Writer. +func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + if mb.IsEmpty() { + return w.writeMetaOnly() + } + + for !mb.IsEmpty() { + var chunk buf.MultiBuffer + if w.transferType == protocol.TransferTypeStream { + mb, chunk = buf.SplitSize(mb, 8*1024) + } else { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + chunk = buf.MultiBuffer{b} + } + if err := w.writeData(chunk); err != nil { + return err + } + } + + return nil +} + +// Close implements common.Closable. +func (w *Writer) Close() error { + meta := FrameMetadata{ + SessionID: w.id, + SessionStatus: SessionStatusEnd, + } + if w.hasError { + meta.Option.Set(OptionError) + } + + frame := buf.New() + common.Must(meta.WriteTo(frame)) + + w.writer.WriteMultiBuffer(buf.MultiBuffer{frame}) + return nil +} diff --git a/common/net/address.go b/common/net/address.go new file mode 100644 index 00000000..9ba8c473 --- /dev/null +++ b/common/net/address.go @@ -0,0 +1,211 @@ +package net + +import ( + "bytes" + "net" + "strings" +) + +var ( + // LocalHostIP is a constant value for localhost IP in IPv4. + LocalHostIP = IPAddress([]byte{127, 0, 0, 1}) + + // AnyIP is a constant value for any IP in IPv4. + AnyIP = IPAddress([]byte{0, 0, 0, 0}) + + // LocalHostDomain is a constant value for localhost domain. + LocalHostDomain = DomainAddress("localhost") + + // LocalHostIPv6 is a constant value for localhost IP in IPv6. + LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) + + // AnyIPv6 is a constant value for any IP in IPv6. + AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) +) + +// AddressFamily is the type of address. +type AddressFamily byte + +const ( + // AddressFamilyIPv4 represents address as IPv4 + AddressFamilyIPv4 = AddressFamily(0) + + // AddressFamilyIPv6 represents address as IPv6 + AddressFamilyIPv6 = AddressFamily(1) + + // AddressFamilyDomain represents address as Domain + AddressFamilyDomain = AddressFamily(2) +) + +// IsIPv4 returns true if current AddressFamily is IPv4. +func (af AddressFamily) IsIPv4() bool { + return af == AddressFamilyIPv4 +} + +// IsIPv6 returns true if current AddressFamily is IPv6. +func (af AddressFamily) IsIPv6() bool { + return af == AddressFamilyIPv6 +} + +// IsIP returns true if current AddressFamily is IPv6 or IPv4. +func (af AddressFamily) IsIP() bool { + return af == AddressFamilyIPv4 || af == AddressFamilyIPv6 +} + +// IsDomain returns true if current AddressFamily is Domain. +func (af AddressFamily) IsDomain() bool { + return af == AddressFamilyDomain +} + +// Address represents a network address to be communicated with. It may be an IP address or domain +// address, not both. This interface doesn't resolve IP address for a given domain. +type Address interface { + IP() net.IP // IP of this Address + Domain() string // Domain of this Address + Family() AddressFamily + + String() string // String representation of this Address +} + +func isAlphaNum(c byte) bool { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// ParseAddress parses a string into an Address. The return value will be an IPAddress when +// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise. +func ParseAddress(addr string) Address { + // Handle IPv6 address in form as "[2001:4860:0:2001::68]" + lenAddr := len(addr) + if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' { + addr = addr[1 : lenAddr-1] + lenAddr -= 2 + } + + if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) { + addr = strings.TrimSpace(addr) + } + + ip := net.ParseIP(addr) + if ip != nil { + return IPAddress(ip) + } + return DomainAddress(addr) +} + +var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +// IPAddress creates an Address with given IP. +func IPAddress(ip []byte) Address { + switch len(ip) { + case net.IPv4len: + var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]} + return addr + case net.IPv6len: + if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff { + return IPAddress(ip[12:16]) + } + var addr ipv6Address = [16]byte{ + ip[0], ip[1], ip[2], ip[3], + ip[4], ip[5], ip[6], ip[7], + ip[8], ip[9], ip[10], ip[11], + ip[12], ip[13], ip[14], ip[15], + } + return addr + default: + newError("invalid IP format: ", ip).AtError().WriteToLog() + return nil + } +} + +// DomainAddress creates an Address with given domain. +func DomainAddress(domain string) Address { + return domainAddress(domain) +} + +type ipv4Address [4]byte + +func (a ipv4Address) IP() net.IP { + return net.IP(a[:]) +} + +func (ipv4Address) Domain() string { + panic("Calling Domain() on an IPv4Address.") +} + +func (ipv4Address) Family() AddressFamily { + return AddressFamilyIPv4 +} + +func (a ipv4Address) String() string { + return a.IP().String() +} + +type ipv6Address [16]byte + +func (a ipv6Address) IP() net.IP { + return net.IP(a[:]) +} + +func (ipv6Address) Domain() string { + panic("Calling Domain() on an IPv6Address.") +} + +func (ipv6Address) Family() AddressFamily { + return AddressFamilyIPv6 +} + +func (a ipv6Address) String() string { + return "[" + a.IP().String() + "]" +} + +type domainAddress string + +func (domainAddress) IP() net.IP { + panic("Calling IP() on a DomainAddress.") +} + +func (a domainAddress) Domain() string { + return string(a) +} + +func (domainAddress) Family() AddressFamily { + return AddressFamilyDomain +} + +func (a domainAddress) String() string { + return a.Domain() +} + +// AsAddress translates IPOrDomain to Address. +func (d *IPOrDomain) AsAddress() Address { + if d == nil { + return nil + } + switch addr := d.Address.(type) { + case *IPOrDomain_Ip: + return IPAddress(addr.Ip) + case *IPOrDomain_Domain: + return DomainAddress(addr.Domain) + } + panic("Common|Net: Invalid address.") +} + +// NewIPOrDomain translates Address to IPOrDomain +func NewIPOrDomain(addr Address) *IPOrDomain { + switch addr.Family() { + case AddressFamilyDomain: + return &IPOrDomain{ + Address: &IPOrDomain_Domain{ + Domain: addr.Domain(), + }, + } + case AddressFamilyIPv4, AddressFamilyIPv6: + return &IPOrDomain{ + Address: &IPOrDomain_Ip{ + Ip: addr.IP(), + }, + } + default: + panic("Unknown Address type.") + } +} diff --git a/common/net/address.pb.go b/common/net/address.pb.go new file mode 100644 index 00000000..890d2ff3 --- /dev/null +++ b/common/net/address.pb.go @@ -0,0 +1,195 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/net/address.proto + +package net + +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 + +// Address of a network host. It may be either an IP address or a domain +// address. +type IPOrDomain struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Address: + // *IPOrDomain_Ip + // *IPOrDomain_Domain + Address isIPOrDomain_Address `protobuf_oneof:"address"` +} + +func (x *IPOrDomain) Reset() { + *x = IPOrDomain{} + if protoimpl.UnsafeEnabled { + mi := &file_common_net_address_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IPOrDomain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IPOrDomain) ProtoMessage() {} + +func (x *IPOrDomain) ProtoReflect() protoreflect.Message { + mi := &file_common_net_address_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 IPOrDomain.ProtoReflect.Descriptor instead. +func (*IPOrDomain) Descriptor() ([]byte, []int) { + return file_common_net_address_proto_rawDescGZIP(), []int{0} +} + +func (m *IPOrDomain) GetAddress() isIPOrDomain_Address { + if m != nil { + return m.Address + } + return nil +} + +func (x *IPOrDomain) GetIp() []byte { + if x, ok := x.GetAddress().(*IPOrDomain_Ip); ok { + return x.Ip + } + return nil +} + +func (x *IPOrDomain) GetDomain() string { + if x, ok := x.GetAddress().(*IPOrDomain_Domain); ok { + return x.Domain + } + return "" +} + +type isIPOrDomain_Address interface { + isIPOrDomain_Address() +} + +type IPOrDomain_Ip struct { + // IP address. Must by either 4 or 16 bytes. + Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3,oneof"` +} + +type IPOrDomain_Domain struct { + // Domain address. + Domain string `protobuf:"bytes,2,opt,name=domain,proto3,oneof"` +} + +func (*IPOrDomain_Ip) isIPOrDomain_Address() {} + +func (*IPOrDomain_Domain) isIPOrDomain_Address() {} + +var File_common_net_address_proto protoreflect.FileDescriptor + +var file_common_net_address_proto_rawDesc = []byte{ + 0x0a, 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, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x22, 0x43, 0x0a, 0x0a, 0x49, + 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x02, 0x69, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x02, 0x69, 0x70, 0x12, 0x18, 0x0a, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x42, 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, + 0x65, 0x74, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_net_address_proto_rawDescOnce sync.Once + file_common_net_address_proto_rawDescData = file_common_net_address_proto_rawDesc +) + +func file_common_net_address_proto_rawDescGZIP() []byte { + file_common_net_address_proto_rawDescOnce.Do(func() { + file_common_net_address_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_net_address_proto_rawDescData) + }) + return file_common_net_address_proto_rawDescData +} + +var file_common_net_address_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_address_proto_goTypes = []interface{}{ + (*IPOrDomain)(nil), // 0: xray.common.net.IPOrDomain +} +var file_common_net_address_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_common_net_address_proto_init() } +func file_common_net_address_proto_init() { + if File_common_net_address_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_net_address_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IPOrDomain); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_common_net_address_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*IPOrDomain_Ip)(nil), + (*IPOrDomain_Domain)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_common_net_address_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_address_proto_goTypes, + DependencyIndexes: file_common_net_address_proto_depIdxs, + MessageInfos: file_common_net_address_proto_msgTypes, + }.Build() + File_common_net_address_proto = out.File + file_common_net_address_proto_rawDesc = nil + file_common_net_address_proto_goTypes = nil + file_common_net_address_proto_depIdxs = nil +} diff --git a/common/net/address.proto b/common/net/address.proto new file mode 100644 index 00000000..0498a1de --- /dev/null +++ b/common/net/address.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/v1/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +// Address of a network host. It may be either an IP address or a domain +// address. +message IPOrDomain { + oneof address { + // IP address. Must by either 4 or 16 bytes. + bytes ip = 1; + + // Domain address. + string domain = 2; + } +} diff --git a/common/net/address_test.go b/common/net/address_test.go new file mode 100644 index 00000000..0fe4adb1 --- /dev/null +++ b/common/net/address_test.go @@ -0,0 +1,194 @@ +package net_test + +import ( + "net" + "testing" + + "github.com/google/go-cmp/cmp" + + . "github.com/xtls/xray-core/v1/common/net" +) + +func TestAddressProperty(t *testing.T) { + type addrProprty struct { + IP []byte + Domain string + Family AddressFamily + String string + } + + testCases := []struct { + Input Address + Output addrProprty + }{ + { + Input: IPAddress([]byte{byte(1), byte(2), byte(3), byte(4)}), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: IPAddress([]byte{ + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + }), + Output: addrProprty{ + IP: []byte{ + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + }, + Family: AddressFamilyIPv6, + String: "[102:304:102:304:102:304:102:304]", + }, + }, + { + Input: IPAddress([]byte{ + byte(0), byte(0), byte(0), byte(0), + byte(0), byte(0), byte(0), byte(0), + byte(0), byte(0), byte(255), byte(255), + byte(1), byte(2), byte(3), byte(4), + }), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: DomainAddress("example.com"), + Output: addrProprty{ + Domain: "example.com", + Family: AddressFamilyDomain, + String: "example.com", + }, + }, + { + Input: IPAddress(net.IPv4(1, 2, 3, 4)), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: ParseAddress("[2001:4860:0:2001::68]"), + Output: addrProprty{ + IP: []byte{0x20, 0x01, 0x48, 0x60, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68}, + Family: AddressFamilyIPv6, + String: "[2001:4860:0:2001::68]", + }, + }, + { + Input: ParseAddress("::0"), + Output: addrProprty{ + IP: AnyIPv6.IP(), + Family: AddressFamilyIPv6, + String: "[::]", + }, + }, + { + Input: ParseAddress("[::ffff:123.151.71.143]"), + Output: addrProprty{ + IP: []byte{123, 151, 71, 143}, + Family: AddressFamilyIPv4, + String: "123.151.71.143", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("example.com")).AsAddress(), + Output: addrProprty{ + Domain: "example.com", + Family: AddressFamilyDomain, + String: "example.com", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("8.8.8.8")).AsAddress(), + Output: addrProprty{ + IP: []byte{8, 8, 8, 8}, + Family: AddressFamilyIPv4, + String: "8.8.8.8", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("[2001:4860:0:2001::68]")).AsAddress(), + Output: addrProprty{ + IP: []byte{0x20, 0x01, 0x48, 0x60, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68}, + Family: AddressFamilyIPv6, + String: "[2001:4860:0:2001::68]", + }, + }, + } + + for _, testCase := range testCases { + actual := addrProprty{ + Family: testCase.Input.Family(), + String: testCase.Input.String(), + } + if testCase.Input.Family().IsIP() { + actual.IP = testCase.Input.IP() + } else { + actual.Domain = testCase.Input.Domain() + } + + if r := cmp.Diff(actual, testCase.Output); r != "" { + t.Error("for input: ", testCase.Input, ":", r) + } + } +} + +func TestInvalidAddressConvertion(t *testing.T) { + panics := func(f func()) (ret bool) { + defer func() { + if r := recover(); r != nil { + ret = true + } + }() + f() + return false + } + + testCases := []func(){ + func() { ParseAddress("8.8.8.8").Domain() }, + func() { ParseAddress("2001:4860:0:2001::68").Domain() }, + func() { ParseAddress("example.com").IP() }, + } + for idx, testCase := range testCases { + if !panics(testCase) { + t.Error("case ", idx, " failed") + } + } +} + +func BenchmarkParseAddressIPv4(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("8.8.8.8") + if addr.Family() != AddressFamilyIPv4 { + panic("not ipv4") + } + } +} + +func BenchmarkParseAddressIPv6(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("2001:4860:0:2001::68") + if addr.Family() != AddressFamilyIPv6 { + panic("not ipv6") + } + } +} + +func BenchmarkParseAddressDomain(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("example.com") + if addr.Family() != AddressFamilyDomain { + panic("not domain") + } + } +} diff --git a/common/net/connection.go b/common/net/connection.go new file mode 100644 index 00000000..bdf4dde4 --- /dev/null +++ b/common/net/connection.go @@ -0,0 +1,162 @@ +// +build !confonly + +package net + +import ( + "io" + "net" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/signal/done" +) + +type ConnectionOption func(*connection) + +func ConnectionLocalAddr(a net.Addr) ConnectionOption { + return func(c *connection) { + c.local = a + } +} + +func ConnectionRemoteAddr(a net.Addr) ConnectionOption { + return func(c *connection) { + c.remote = a + } +} + +func ConnectionInput(writer io.Writer) ConnectionOption { + return func(c *connection) { + c.writer = buf.NewWriter(writer) + } +} + +func ConnectionInputMulti(writer buf.Writer) ConnectionOption { + return func(c *connection) { + c.writer = writer + } +} + +func ConnectionOutput(reader io.Reader) ConnectionOption { + return func(c *connection) { + c.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } +} + +func ConnectionOutputMulti(reader buf.Reader) ConnectionOption { + return func(c *connection) { + c.reader = &buf.BufferedReader{Reader: reader} + } +} + +func ConnectionOutputMultiUDP(reader buf.Reader) ConnectionOption { + return func(c *connection) { + c.reader = &buf.BufferedReader{ + Reader: reader, + Spliter: buf.SplitFirstBytes, + } + } +} + +func ConnectionOnClose(n io.Closer) ConnectionOption { + return func(c *connection) { + c.onClose = n + } +} + +func NewConnection(opts ...ConnectionOption) net.Conn { + c := &connection{ + done: done.New(), + local: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + remote: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +type connection struct { + reader *buf.BufferedReader + writer buf.Writer + done *done.Instance + onClose io.Closer + local Addr + remote Addr +} + +func (c *connection) Read(b []byte) (int, error) { + return c.reader.Read(b) +} + +// ReadMultiBuffer implements buf.Reader. +func (c *connection) ReadMultiBuffer() (buf.MultiBuffer, error) { + return c.reader.ReadMultiBuffer() +} + +// Write implements net.Conn.Write(). +func (c *connection) Write(b []byte) (int, error) { + if c.done.Done() { + return 0, io.ErrClosedPipe + } + + l := len(b) + mb := make(buf.MultiBuffer, 0, l/buf.Size+1) + mb = buf.MergeBytes(mb, b) + return l, c.writer.WriteMultiBuffer(mb) +} + +func (c *connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + if c.done.Done() { + buf.ReleaseMulti(mb) + return io.ErrClosedPipe + } + + return c.writer.WriteMultiBuffer(mb) +} + +// Close implements net.Conn.Close(). +func (c *connection) Close() error { + common.Must(c.done.Close()) + common.Interrupt(c.reader) + common.Close(c.writer) + if c.onClose != nil { + return c.onClose.Close() + } + + return nil +} + +// LocalAddr implements net.Conn.LocalAddr(). +func (c *connection) LocalAddr() net.Addr { + return c.local +} + +// RemoteAddr implements net.Conn.RemoteAddr(). +func (c *connection) RemoteAddr() net.Addr { + return c.remote +} + +// SetDeadline implements net.Conn.SetDeadline(). +func (c *connection) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline implements net.Conn.SetReadDeadline(). +func (c *connection) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline implements net.Conn.SetWriteDeadline(). +func (c *connection) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/common/net/destination.go b/common/net/destination.go new file mode 100644 index 00000000..055395e9 --- /dev/null +++ b/common/net/destination.go @@ -0,0 +1,126 @@ +package net + +import ( + "net" + "strings" +) + +// Destination represents a network destination including address and protocol (tcp / udp). +type Destination struct { + Address Address + Port Port + Network Network +} + +// DestinationFromAddr generates a Destination from a net address. +func DestinationFromAddr(addr net.Addr) Destination { + switch addr := addr.(type) { + case *net.TCPAddr: + return TCPDestination(IPAddress(addr.IP), Port(addr.Port)) + case *net.UDPAddr: + return UDPDestination(IPAddress(addr.IP), Port(addr.Port)) + case *net.UnixAddr: + return UnixDestination(DomainAddress(addr.Name)) + default: + panic("Net: Unknown address type.") + } +} + +// ParseDestination converts a destination from its string presentation. +func ParseDestination(dest string) (Destination, error) { + d := Destination{ + Address: AnyIP, + Port: Port(0), + } + if strings.HasPrefix(dest, "tcp:") { + d.Network = Network_TCP + dest = dest[4:] + } else if strings.HasPrefix(dest, "udp:") { + d.Network = Network_UDP + dest = dest[4:] + } else if strings.HasPrefix(dest, "unix:") { + d = UnixDestination(DomainAddress(dest[5:])) + return d, nil + } + + hstr, pstr, err := SplitHostPort(dest) + if err != nil { + return d, err + } + if len(hstr) > 0 { + d.Address = ParseAddress(hstr) + } + if len(pstr) > 0 { + port, err := PortFromString(pstr) + if err != nil { + return d, err + } + d.Port = port + } + return d, nil +} + +// TCPDestination creates a TCP destination with given address +func TCPDestination(address Address, port Port) Destination { + return Destination{ + Network: Network_TCP, + Address: address, + Port: port, + } +} + +// UDPDestination creates a UDP destination with given address +func UDPDestination(address Address, port Port) Destination { + return Destination{ + Network: Network_UDP, + Address: address, + Port: port, + } +} + +// UnixDestination creates a Unix destination with given address +func UnixDestination(address Address) Destination { + return Destination{ + Network: Network_UNIX, + Address: address, + } +} + +// NetAddr returns the network address in this Destination in string form. +func (d Destination) NetAddr() string { + addr := "" + if d.Network == Network_TCP || d.Network == Network_UDP { + addr = d.Address.String() + ":" + d.Port.String() + } else if d.Network == Network_UNIX { + addr = d.Address.String() + } + return addr +} + +// String returns the strings form of this Destination. +func (d Destination) String() string { + prefix := "unknown:" + switch d.Network { + case Network_TCP: + prefix = "tcp:" + case Network_UDP: + prefix = "udp:" + case Network_UNIX: + prefix = "unix:" + } + return prefix + d.NetAddr() +} + +// IsValid returns true if this Destination is valid. +func (d Destination) IsValid() bool { + return d.Network != Network_Unknown +} + +// AsDestination converts current Endpoint into Destination. +func (p *Endpoint) AsDestination() Destination { + return Destination{ + Network: p.Network, + Address: p.Address.AsAddress(), + Port: Port(p.Port), + } +} diff --git a/common/net/destination.pb.go b/common/net/destination.pb.go new file mode 100644 index 00000000..3f323d78 --- /dev/null +++ b/common/net/destination.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/net/destination.proto + +package net + +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 + +// Endpoint of a network connection. +type Endpoint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Network Network `protobuf:"varint,1,opt,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + Address *IPOrDomain `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` +} + +func (x *Endpoint) Reset() { + *x = Endpoint{} + if protoimpl.UnsafeEnabled { + mi := &file_common_net_destination_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Endpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Endpoint) ProtoMessage() {} + +func (x *Endpoint) ProtoReflect() protoreflect.Message { + mi := &file_common_net_destination_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 Endpoint.ProtoReflect.Descriptor instead. +func (*Endpoint) Descriptor() ([]byte, []int) { + return file_common_net_destination_proto_rawDescGZIP(), []int{0} +} + +func (x *Endpoint) GetNetwork() Network { + if x != nil { + return x.Network + } + return Network_Unknown +} + +func (x *Endpoint) GetAddress() *IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *Endpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +var File_common_net_destination_proto protoreflect.FileDescriptor + +var file_common_net_destination_proto_rawDesc = []byte{ + 0x0a, 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, 0x12, 0x0f, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 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, 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, 0x22, 0x89, 0x01, 0x0a, 0x08, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x12, 0x32, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 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, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 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, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x42, + 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, + 0x74, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4e, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_net_destination_proto_rawDescOnce sync.Once + file_common_net_destination_proto_rawDescData = file_common_net_destination_proto_rawDesc +) + +func file_common_net_destination_proto_rawDescGZIP() []byte { + file_common_net_destination_proto_rawDescOnce.Do(func() { + file_common_net_destination_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_net_destination_proto_rawDescData) + }) + return file_common_net_destination_proto_rawDescData +} + +var file_common_net_destination_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_destination_proto_goTypes = []interface{}{ + (*Endpoint)(nil), // 0: xray.common.net.Endpoint + (Network)(0), // 1: xray.common.net.Network + (*IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain +} +var file_common_net_destination_proto_depIdxs = []int32{ + 1, // 0: xray.common.net.Endpoint.network:type_name -> xray.common.net.Network + 2, // 1: xray.common.net.Endpoint.address:type_name -> xray.common.net.IPOrDomain + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_common_net_destination_proto_init() } +func file_common_net_destination_proto_init() { + if File_common_net_destination_proto != nil { + return + } + file_common_net_network_proto_init() + file_common_net_address_proto_init() + if !protoimpl.UnsafeEnabled { + file_common_net_destination_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Endpoint); 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_common_net_destination_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_destination_proto_goTypes, + DependencyIndexes: file_common_net_destination_proto_depIdxs, + MessageInfos: file_common_net_destination_proto_msgTypes, + }.Build() + File_common_net_destination_proto = out.File + file_common_net_destination_proto_rawDesc = nil + file_common_net_destination_proto_goTypes = nil + file_common_net_destination_proto_depIdxs = nil +} diff --git a/common/net/destination.proto b/common/net/destination.proto new file mode 100644 index 00000000..e8c1f638 --- /dev/null +++ b/common/net/destination.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/v1/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/net/address.proto"; + +// Endpoint of a network connection. +message Endpoint { + Network network = 1; + IPOrDomain address = 2; + uint32 port = 3; +} diff --git a/common/net/destination_test.go b/common/net/destination_test.go new file mode 100644 index 00000000..a4a008ec --- /dev/null +++ b/common/net/destination_test.go @@ -0,0 +1,111 @@ +package net_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + . "github.com/xtls/xray-core/v1/common/net" +) + +func TestDestinationProperty(t *testing.T) { + testCases := []struct { + Input Destination + Network Network + String string + NetString string + }{ + { + Input: TCPDestination(IPAddress([]byte{1, 2, 3, 4}), 80), + Network: Network_TCP, + String: "tcp:1.2.3.4:80", + NetString: "1.2.3.4:80", + }, + { + Input: UDPDestination(IPAddress([]byte{0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88}), 53), + Network: Network_UDP, + String: "udp:[2001:4860:4860::8888]:53", + NetString: "[2001:4860:4860::8888]:53", + }, + { + Input: UnixDestination(DomainAddress("/tmp/test.sock")), + Network: Network_UNIX, + String: "unix:/tmp/test.sock", + NetString: "/tmp/test.sock", + }, + } + + for _, testCase := range testCases { + dest := testCase.Input + if r := cmp.Diff(dest.Network, testCase.Network); r != "" { + t.Error("unexpected Network in ", dest.String(), ": ", r) + } + if r := cmp.Diff(dest.String(), testCase.String); r != "" { + t.Error(r) + } + if r := cmp.Diff(dest.NetAddr(), testCase.NetString); r != "" { + t.Error(r) + } + } +} + +func TestDestinationParse(t *testing.T) { + cases := []struct { + Input string + Output Destination + Error bool + }{ + { + Input: "tcp:127.0.0.1:80", + Output: TCPDestination(LocalHostIP, Port(80)), + }, + { + Input: "udp:8.8.8.8:53", + Output: UDPDestination(IPAddress([]byte{8, 8, 8, 8}), Port(53)), + }, + { + Input: "unix:/tmp/test.sock", + Output: UnixDestination(DomainAddress("/tmp/test.sock")), + }, + { + Input: "8.8.8.8:53", + Output: Destination{ + Address: IPAddress([]byte{8, 8, 8, 8}), + Port: Port(53), + }, + }, + { + Input: ":53", + Output: Destination{ + Address: AnyIP, + Port: Port(53), + }, + }, + { + Input: "8.8.8.8", + Error: true, + }, + { + Input: "8.8.8.8:http", + Error: true, + }, + { + Input: "/tmp/test.sock", + Error: true, + }, + } + + for _, testcase := range cases { + d, err := ParseDestination(testcase.Input) + if !testcase.Error { + if err != nil { + t.Error("for test case: ", testcase.Input, " expected no error, but got ", err) + } + if d != testcase.Output { + t.Error("for test case: ", testcase.Input, " expected output: ", testcase.Output.String(), " but got ", d.String()) + } + } else if err == nil { + t.Error("for test case: ", testcase.Input, " expected error, but got nil") + } + } +} diff --git a/common/net/errors.generated.go b/common/net/errors.generated.go new file mode 100644 index 00000000..cb133cbe --- /dev/null +++ b/common/net/errors.generated.go @@ -0,0 +1,9 @@ +package net + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/net/net.go b/common/net/net.go new file mode 100644 index 00000000..96105820 --- /dev/null +++ b/common/net/net.go @@ -0,0 +1,4 @@ +// Package net is a drop-in replacement to Golang's net package, with some more functionalities. +package net // import "github.com/xtls/xray-core/v1/common/net" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/common/net/network.go b/common/net/network.go new file mode 100644 index 00000000..f2e303b0 --- /dev/null +++ b/common/net/network.go @@ -0,0 +1,24 @@ +package net + +func (n Network) SystemString() string { + switch n { + case Network_TCP: + return "tcp" + case Network_UDP: + return "udp" + case Network_UNIX: + return "unix" + default: + return "unknown" + } +} + +// HasNetwork returns true if the network list has a certain network. +func HasNetwork(list []Network, network Network) bool { + for _, value := range list { + if value == network { + return true + } + } + return false +} diff --git a/common/net/network.pb.go b/common/net/network.pb.go new file mode 100644 index 00000000..b59d4300 --- /dev/null +++ b/common/net/network.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/net/network.proto + +package net + +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 Network int32 + +const ( + Network_Unknown Network = 0 + // Deprecated: Do not use. + Network_RawTCP Network = 1 + Network_TCP Network = 2 + Network_UDP Network = 3 + Network_UNIX Network = 4 +) + +// Enum value maps for Network. +var ( + Network_name = map[int32]string{ + 0: "Unknown", + 1: "RawTCP", + 2: "TCP", + 3: "UDP", + 4: "UNIX", + } + Network_value = map[string]int32{ + "Unknown": 0, + "RawTCP": 1, + "TCP": 2, + "UDP": 3, + "UNIX": 4, + } +) + +func (x Network) Enum() *Network { + p := new(Network) + *p = x + return p +} + +func (x Network) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Network) Descriptor() protoreflect.EnumDescriptor { + return file_common_net_network_proto_enumTypes[0].Descriptor() +} + +func (Network) Type() protoreflect.EnumType { + return &file_common_net_network_proto_enumTypes[0] +} + +func (x Network) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Network.Descriptor instead. +func (Network) EnumDescriptor() ([]byte, []int) { + return file_common_net_network_proto_rawDescGZIP(), []int{0} +} + +// NetworkList is a list of Networks. +type NetworkList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Network []Network `protobuf:"varint,1,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` +} + +func (x *NetworkList) Reset() { + *x = NetworkList{} + if protoimpl.UnsafeEnabled { + mi := &file_common_net_network_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NetworkList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkList) ProtoMessage() {} + +func (x *NetworkList) ProtoReflect() protoreflect.Message { + mi := &file_common_net_network_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 NetworkList.ProtoReflect.Descriptor instead. +func (*NetworkList) Descriptor() ([]byte, []int) { + return file_common_net_network_proto_rawDescGZIP(), []int{0} +} + +func (x *NetworkList) GetNetwork() []Network { + if x != nil { + return x.Network + } + return nil +} + +var File_common_net_network_proto protoreflect.FileDescriptor + +var file_common_net_network_proto_rawDesc = []byte{ + 0x0a, 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, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x22, 0x41, 0x0a, 0x0b, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 0x20, 0x03, 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, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2a, 0x42, + 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x06, 0x52, 0x61, 0x77, 0x54, 0x43, 0x50, + 0x10, 0x01, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, + 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x4e, 0x49, 0x58, + 0x10, 0x04, 0x42, 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2f, 0x6e, 0x65, 0x74, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_net_network_proto_rawDescOnce sync.Once + file_common_net_network_proto_rawDescData = file_common_net_network_proto_rawDesc +) + +func file_common_net_network_proto_rawDescGZIP() []byte { + file_common_net_network_proto_rawDescOnce.Do(func() { + file_common_net_network_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_net_network_proto_rawDescData) + }) + return file_common_net_network_proto_rawDescData +} + +var file_common_net_network_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_net_network_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_network_proto_goTypes = []interface{}{ + (Network)(0), // 0: xray.common.net.Network + (*NetworkList)(nil), // 1: xray.common.net.NetworkList +} +var file_common_net_network_proto_depIdxs = []int32{ + 0, // 0: xray.common.net.NetworkList.network:type_name -> xray.common.net.Network + 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_common_net_network_proto_init() } +func file_common_net_network_proto_init() { + if File_common_net_network_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_net_network_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworkList); 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_common_net_network_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_network_proto_goTypes, + DependencyIndexes: file_common_net_network_proto_depIdxs, + EnumInfos: file_common_net_network_proto_enumTypes, + MessageInfos: file_common_net_network_proto_msgTypes, + }.Build() + File_common_net_network_proto = out.File + file_common_net_network_proto_rawDesc = nil + file_common_net_network_proto_goTypes = nil + file_common_net_network_proto_depIdxs = nil +} diff --git a/common/net/network.proto b/common/net/network.proto new file mode 100644 index 00000000..205149e3 --- /dev/null +++ b/common/net/network.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/v1/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +enum Network { + Unknown = 0; + + RawTCP = 1 [deprecated = true]; + TCP = 2; + UDP = 3; + UNIX = 4; +} + +// NetworkList is a list of Networks. +message NetworkList { repeated Network network = 1; } diff --git a/common/net/port.go b/common/net/port.go new file mode 100644 index 00000000..2a0bf637 --- /dev/null +++ b/common/net/port.go @@ -0,0 +1,95 @@ +package net + +import ( + "encoding/binary" + "strconv" +) + +// Port represents a network port in TCP and UDP protocol. +type Port uint16 + +// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order. +// @unsafe Caller must ensure that the byte array has at least 2 elements. +func PortFromBytes(port []byte) Port { + return Port(binary.BigEndian.Uint16(port)) +} + +// PortFromInt converts an integer to a Port. +// @error when the integer is not positive or larger then 65535 +func PortFromInt(val uint32) (Port, error) { + if val > 65535 { + return Port(0), newError("invalid port range: ", val) + } + return Port(val), nil +} + +// PortFromString converts a string to a Port. +// @error when the string is not an integer or the integral value is a not a valid Port. +func PortFromString(s string) (Port, error) { + val, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return Port(0), newError("invalid port range: ", s) + } + return PortFromInt(uint32(val)) +} + +// Value return the corresponding uint16 value of a Port. +func (p Port) Value() uint16 { + return uint16(p) +} + +// String returns the string presentation of a Port. +func (p Port) String() string { + return strconv.Itoa(int(p)) +} + +// FromPort returns the beginning port of this PortRange. +func (p *PortRange) FromPort() Port { + return Port(p.From) +} + +// ToPort returns the end port of this PortRange. +func (p *PortRange) ToPort() Port { + return Port(p.To) +} + +// Contains returns true if the given port is within the range of a PortRange. +func (p *PortRange) Contains(port Port) bool { + return p.FromPort() <= port && port <= p.ToPort() +} + +// SinglePortRange returns a PortRange contains a single port. +func SinglePortRange(p Port) *PortRange { + return &PortRange{ + From: uint32(p), + To: uint32(p), + } +} + +type MemoryPortRange struct { + From Port + To Port +} + +func (r MemoryPortRange) Contains(port Port) bool { + return r.From <= port && port <= r.To +} + +type MemoryPortList []MemoryPortRange + +func PortListFromProto(l *PortList) MemoryPortList { + mpl := make(MemoryPortList, 0, len(l.Range)) + for _, r := range l.Range { + mpl = append(mpl, MemoryPortRange{From: Port(r.From), To: Port(r.To)}) + } + return mpl +} + +func (mpl MemoryPortList) Contains(port Port) bool { + for _, pr := range mpl { + if pr.Contains(port) { + return true + } + } + return false +} diff --git a/common/net/port.pb.go b/common/net/port.pb.go new file mode 100644 index 00000000..0ed67140 --- /dev/null +++ b/common/net/port.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/net/port.proto + +package net + +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 + +// PortRange represents a range of ports. +type PortRange struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The port that this range starts from. + From uint32 `protobuf:"varint,1,opt,name=From,proto3" json:"From,omitempty"` + // The port that this range ends with (inclusive). + To uint32 `protobuf:"varint,2,opt,name=To,proto3" json:"To,omitempty"` +} + +func (x *PortRange) Reset() { + *x = PortRange{} + if protoimpl.UnsafeEnabled { + mi := &file_common_net_port_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PortRange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortRange) ProtoMessage() {} + +func (x *PortRange) ProtoReflect() protoreflect.Message { + mi := &file_common_net_port_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 PortRange.ProtoReflect.Descriptor instead. +func (*PortRange) Descriptor() ([]byte, []int) { + return file_common_net_port_proto_rawDescGZIP(), []int{0} +} + +func (x *PortRange) GetFrom() uint32 { + if x != nil { + return x.From + } + return 0 +} + +func (x *PortRange) GetTo() uint32 { + if x != nil { + return x.To + } + return 0 +} + +// PortList is a list of ports. +type PortList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Range []*PortRange `protobuf:"bytes,1,rep,name=range,proto3" json:"range,omitempty"` +} + +func (x *PortList) Reset() { + *x = PortList{} + if protoimpl.UnsafeEnabled { + mi := &file_common_net_port_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PortList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortList) ProtoMessage() {} + +func (x *PortList) ProtoReflect() protoreflect.Message { + mi := &file_common_net_port_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 PortList.ProtoReflect.Descriptor instead. +func (*PortList) Descriptor() ([]byte, []int) { + return file_common_net_port_proto_rawDescGZIP(), []int{1} +} + +func (x *PortList) GetRange() []*PortRange { + if x != nil { + return x.Range + } + return nil +} + +var File_common_net_port_proto protoreflect.FileDescriptor + +var file_common_net_port_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x22, 0x2f, 0x0a, 0x09, 0x50, 0x6f, 0x72, 0x74, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x54, 0x6f, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x54, 0x6f, 0x22, 0x3c, 0x0a, 0x08, 0x50, 0x6f, 0x72, + 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 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, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_common_net_port_proto_rawDescOnce sync.Once + file_common_net_port_proto_rawDescData = file_common_net_port_proto_rawDesc +) + +func file_common_net_port_proto_rawDescGZIP() []byte { + file_common_net_port_proto_rawDescOnce.Do(func() { + file_common_net_port_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_net_port_proto_rawDescData) + }) + return file_common_net_port_proto_rawDescData +} + +var file_common_net_port_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_common_net_port_proto_goTypes = []interface{}{ + (*PortRange)(nil), // 0: xray.common.net.PortRange + (*PortList)(nil), // 1: xray.common.net.PortList +} +var file_common_net_port_proto_depIdxs = []int32{ + 0, // 0: xray.common.net.PortList.range:type_name -> xray.common.net.PortRange + 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_common_net_port_proto_init() } +func file_common_net_port_proto_init() { + if File_common_net_port_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_net_port_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PortRange); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_common_net_port_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PortList); 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_common_net_port_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_port_proto_goTypes, + DependencyIndexes: file_common_net_port_proto_depIdxs, + MessageInfos: file_common_net_port_proto_msgTypes, + }.Build() + File_common_net_port_proto = out.File + file_common_net_port_proto_rawDesc = nil + file_common_net_port_proto_goTypes = nil + file_common_net_port_proto_depIdxs = nil +} diff --git a/common/net/port.proto b/common/net/port.proto new file mode 100644 index 00000000..b18df0c4 --- /dev/null +++ b/common/net/port.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/v1/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +// PortRange represents a range of ports. +message PortRange { + // The port that this range starts from. + uint32 From = 1; + // The port that this range ends with (inclusive). + uint32 To = 2; +} + +// PortList is a list of ports. +message PortList { + repeated PortRange range = 1; +} diff --git a/common/net/port_test.go b/common/net/port_test.go new file mode 100644 index 00000000..cd66ac9b --- /dev/null +++ b/common/net/port_test.go @@ -0,0 +1,18 @@ +package net_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/net" +) + +func TestPortRangeContains(t *testing.T) { + portRange := &PortRange{ + From: 53, + To: 53, + } + + if !portRange.Contains(Port(53)) { + t.Error("expected port range containing 53, but actually not") + } +} diff --git a/common/net/system.go b/common/net/system.go new file mode 100644 index 00000000..19468794 --- /dev/null +++ b/common/net/system.go @@ -0,0 +1,61 @@ +package net + +import "net" + +// DialTCP is an alias of net.DialTCP. +var DialTCP = net.DialTCP +var DialUDP = net.DialUDP +var DialUnix = net.DialUnix +var Dial = net.Dial + +type ListenConfig = net.ListenConfig + +var Listen = net.Listen +var ListenTCP = net.ListenTCP +var ListenUDP = net.ListenUDP +var ListenUnix = net.ListenUnix + +var LookupIP = net.LookupIP + +var FileConn = net.FileConn + +// ParseIP is an alias of net.ParseIP +var ParseIP = net.ParseIP + +var SplitHostPort = net.SplitHostPort + +var CIDRMask = net.CIDRMask + +type Addr = net.Addr +type Conn = net.Conn +type PacketConn = net.PacketConn + +type TCPAddr = net.TCPAddr +type TCPConn = net.TCPConn + +type UDPAddr = net.UDPAddr +type UDPConn = net.UDPConn + +type UnixAddr = net.UnixAddr +type UnixConn = net.UnixConn + +// IP is an alias for net.IP. +type IP = net.IP +type IPMask = net.IPMask +type IPNet = net.IPNet + +const IPv4len = net.IPv4len +const IPv6len = net.IPv6len + +type Error = net.Error +type AddrError = net.AddrError + +type Dialer = net.Dialer +type Listener = net.Listener +type TCPListener = net.TCPListener +type UnixListener = net.UnixListener + +var ResolveUnixAddr = net.ResolveUnixAddr +var ResolveUDPAddr = net.ResolveUDPAddr + +type Resolver = net.Resolver diff --git a/common/peer/latency.go b/common/peer/latency.go new file mode 100644 index 00000000..aae292ed --- /dev/null +++ b/common/peer/latency.go @@ -0,0 +1,30 @@ +package peer + +import ( + "sync" +) + +type Latency interface { + Value() uint64 +} + +type HasLatency interface { + ConnectionLatency() Latency + HandshakeLatency() Latency +} + +type AverageLatency struct { + access sync.Mutex + value uint64 +} + +func (al *AverageLatency) Update(newValue uint64) { + al.access.Lock() + defer al.access.Unlock() + + al.value = (al.value + newValue*2) / 3 +} + +func (al *AverageLatency) Value() uint64 { + return al.value +} diff --git a/common/peer/peer.go b/common/peer/peer.go new file mode 100644 index 00000000..333defff --- /dev/null +++ b/common/peer/peer.go @@ -0,0 +1 @@ +package peer diff --git a/common/platform/ctlcmd/attr_other.go b/common/platform/ctlcmd/attr_other.go new file mode 100644 index 00000000..8dfe963d --- /dev/null +++ b/common/platform/ctlcmd/attr_other.go @@ -0,0 +1,9 @@ +// +build !windows + +package ctlcmd + +import "syscall" + +func getSysProcAttr() *syscall.SysProcAttr { + return nil +} diff --git a/common/platform/ctlcmd/attr_windows.go b/common/platform/ctlcmd/attr_windows.go new file mode 100644 index 00000000..b4c1c5f8 --- /dev/null +++ b/common/platform/ctlcmd/attr_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package ctlcmd + +import "syscall" + +func getSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + HideWindow: true, + } +} diff --git a/common/platform/ctlcmd/ctlcmd.go b/common/platform/ctlcmd/ctlcmd.go new file mode 100644 index 00000000..845a10be --- /dev/null +++ b/common/platform/ctlcmd/ctlcmd.go @@ -0,0 +1,50 @@ +package ctlcmd + +import ( + "io" + "os" + "os/exec" + "strings" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/platform" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +func Run(args []string, input io.Reader) (buf.MultiBuffer, error) { + xctl := platform.GetToolLocation("xctl") + if _, err := os.Stat(xctl); err != nil { + return nil, newError("xctl doesn't exist").Base(err) + } + + var errBuffer buf.MultiBufferContainer + var outBuffer buf.MultiBufferContainer + + cmd := exec.Command(xctl, args...) + cmd.Stderr = &errBuffer + cmd.Stdout = &outBuffer + cmd.SysProcAttr = getSysProcAttr() + if input != nil { + cmd.Stdin = input + } + + if err := cmd.Start(); err != nil { + return nil, newError("failed to start xctl").Base(err) + } + + if err := cmd.Wait(); err != nil { + msg := "failed to execute xctl" + if errBuffer.Len() > 0 { + msg += ": \n" + strings.TrimSpace(errBuffer.MultiBuffer.String()) + } + return nil, newError(msg).Base(err) + } + + // log stderr, info message + if !errBuffer.IsEmpty() { + newError(" \n", strings.TrimSpace(errBuffer.MultiBuffer.String())).AtInfo().WriteToLog() + } + + return outBuffer.MultiBuffer, nil +} diff --git a/common/platform/ctlcmd/errors.generated.go b/common/platform/ctlcmd/errors.generated.go new file mode 100644 index 00000000..618c7f23 --- /dev/null +++ b/common/platform/ctlcmd/errors.generated.go @@ -0,0 +1,9 @@ +package ctlcmd + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go new file mode 100644 index 00000000..bfaf6f7d --- /dev/null +++ b/common/platform/filesystem/file.go @@ -0,0 +1,44 @@ +package filesystem + +import ( + "io" + "os" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/platform" +) + +type FileReaderFunc func(path string) (io.ReadCloser, error) + +var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +func ReadFile(path string) ([]byte, error) { + reader, err := NewFileReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + + return buf.ReadAllToBytes(reader) +} + +func ReadAsset(file string) ([]byte, error) { + return ReadFile(platform.GetAssetLocation(file)) +} + +func CopyFile(dst string, src string) error { + bytes, err := ReadFile(src) + if err != nil { + return err + } + f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(bytes) + return err +} diff --git a/common/platform/others.go b/common/platform/others.go new file mode 100644 index 00000000..efa8e0c8 --- /dev/null +++ b/common/platform/others.go @@ -0,0 +1,44 @@ +// +build !windows + +package platform + +import ( + "os" + "path/filepath" +) + +func ExpandEnv(s string) string { + return os.ExpandEnv(s) +} + +func LineSeparator() string { + return "\n" +} + +func GetToolLocation(file string) string { + const name = "xray.location.tool" + toolPath := EnvFlag{Name: name, AltName: NormalizeEnvName(name)}.GetValue(getExecutableDir) + return filepath.Join(toolPath, file) +} + +// GetAssetLocation search for `file` in certain locations +func GetAssetLocation(file string) string { + const name = "xray.location.asset" + assetPath := NewEnvFlag(name).GetValue(getExecutableDir) + defPath := filepath.Join(assetPath, file) + for _, p := range []string{ + defPath, + filepath.Join("/usr/local/share/xray/", file), + filepath.Join("/usr/share/xray/", file), + } { + if _, err := os.Stat(p); os.IsNotExist(err) { + continue + } + + // asset found + return p + } + + // asset not found, let the caller throw out the error + return defPath +} diff --git a/common/platform/platform.go b/common/platform/platform.go new file mode 100644 index 00000000..2370a445 --- /dev/null +++ b/common/platform/platform.go @@ -0,0 +1,86 @@ +package platform // import "github.com/xtls/xray-core/v1/common/platform" + +import ( + "os" + "path/filepath" + "strconv" + "strings" +) + +type EnvFlag struct { + Name string + AltName string +} + +func NewEnvFlag(name string) EnvFlag { + return EnvFlag{ + Name: name, + AltName: NormalizeEnvName(name), + } +} + +func (f EnvFlag) GetValue(defaultValue func() string) string { + if v, found := os.LookupEnv(f.Name); found { + return v + } + if len(f.AltName) > 0 { + if v, found := os.LookupEnv(f.AltName); found { + return v + } + } + + return defaultValue() +} + +func (f EnvFlag) GetValueAsInt(defaultValue int) int { + useDefaultValue := false + s := f.GetValue(func() string { + useDefaultValue = true + return "" + }) + if useDefaultValue { + return defaultValue + } + v, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return defaultValue + } + return int(v) +} + +func NormalizeEnvName(name string) string { + return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") +} + +func getExecutableDir() string { + exec, err := os.Executable() + if err != nil { + return "" + } + return filepath.Dir(exec) +} + +func getExecutableSubDir(dir string) func() string { + return func() string { + return filepath.Join(getExecutableDir(), dir) + } +} + +func GetPluginDirectory() string { + const name = "xray.location.plugin" + pluginDir := NewEnvFlag(name).GetValue(getExecutableSubDir("plugins")) + return pluginDir +} + +func GetConfigurationPath() string { + const name = "xray.location.config" + configPath := NewEnvFlag(name).GetValue(getExecutableDir) + return filepath.Join(configPath, "config.json") +} + +// GetConfDirPath reads "xray.location.confdir" +func GetConfDirPath() string { + const name = "xray.location.confdir" + configPath := NewEnvFlag(name).GetValue(func() string { return "" }) + return configPath +} diff --git a/common/platform/platform_test.go b/common/platform/platform_test.go new file mode 100644 index 00000000..b3196f15 --- /dev/null +++ b/common/platform/platform_test.go @@ -0,0 +1,65 @@ +package platform_test + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/platform" +) + +func TestNormalizeEnvName(t *testing.T) { + cases := []struct { + input string + output string + }{ + { + input: "a", + output: "A", + }, + { + input: "a.a", + output: "A_A", + }, + { + input: "A.A.B", + output: "A_A_B", + }, + } + for _, test := range cases { + if v := NormalizeEnvName(test.input); v != test.output { + t.Error("unexpected output: ", v, " want ", test.output) + } + } +} + +func TestEnvFlag(t *testing.T) { + if v := (EnvFlag{ + Name: "xxxxx.y", + }.GetValueAsInt(10)); v != 10 { + t.Error("env value: ", v) + } +} + +func TestGetAssetLocation(t *testing.T) { + exec, err := os.Executable() + common.Must(err) + + loc := GetAssetLocation("t") + if filepath.Dir(loc) != filepath.Dir(exec) { + t.Error("asset dir: ", loc, " not in ", exec) + } + + os.Setenv("xray.location.asset", "/xray") + if runtime.GOOS == "windows" { + if v := GetAssetLocation("t"); v != "\\xray\\t" { + t.Error("asset loc: ", v) + } + } else { + if v := GetAssetLocation("t"); v != "/xray/t" { + t.Error("asset loc: ", v) + } + } +} diff --git a/common/platform/windows.go b/common/platform/windows.go new file mode 100644 index 00000000..17ee35e7 --- /dev/null +++ b/common/platform/windows.go @@ -0,0 +1,27 @@ +// +build windows + +package platform + +import "path/filepath" + +func ExpandEnv(s string) string { + // TODO + return s +} + +func LineSeparator() string { + return "\r\n" +} + +func GetToolLocation(file string) string { + const name = "xray.location.tool" + toolPath := EnvFlag{Name: name, AltName: NormalizeEnvName(name)}.GetValue(getExecutableDir) + return filepath.Join(toolPath, file+".exe") +} + +// GetAssetLocation search for `file` in the excutable dir +func GetAssetLocation(file string) string { + const name = "xray.location.asset" + assetPath := NewEnvFlag(name).GetValue(getExecutableDir) + return filepath.Join(assetPath, file) +} diff --git a/common/protocol/account.go b/common/protocol/account.go new file mode 100644 index 00000000..7793974a --- /dev/null +++ b/common/protocol/account.go @@ -0,0 +1,11 @@ +package protocol + +// Account is a user identity used for authentication. +type Account interface { + Equals(Account) bool +} + +// AsAccount is an object can be converted into account. +type AsAccount interface { + AsAccount() (Account, error) +} diff --git a/common/protocol/address.go b/common/protocol/address.go new file mode 100644 index 00000000..74b04602 --- /dev/null +++ b/common/protocol/address.go @@ -0,0 +1,258 @@ +package protocol + +import ( + "io" + + "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" +) + +type AddressOption func(*option) + +func PortThenAddress() AddressOption { + return func(p *option) { + p.portFirst = true + } +} + +func AddressFamilyByte(b byte, f net.AddressFamily) AddressOption { + if b >= 16 { + panic("address family byte too big") + } + return func(p *option) { + p.addrTypeMap[b] = f + p.addrByteMap[f] = b + } +} + +type AddressTypeParser func(byte) byte + +func WithAddressTypeParser(atp AddressTypeParser) AddressOption { + return func(p *option) { + p.typeParser = atp + } +} + +type AddressSerializer interface { + ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) + + WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error +} + +const afInvalid = 255 + +type option struct { + addrTypeMap [16]net.AddressFamily + addrByteMap [16]byte + portFirst bool + typeParser AddressTypeParser +} + +// NewAddressParser creates a new AddressParser +func NewAddressParser(options ...AddressOption) AddressSerializer { + var o option + for i := range o.addrByteMap { + o.addrByteMap[i] = afInvalid + } + for i := range o.addrTypeMap { + o.addrTypeMap[i] = net.AddressFamily(afInvalid) + } + for _, opt := range options { + opt(&o) + } + + ap := &addressParser{ + addrByteMap: o.addrByteMap, + addrTypeMap: o.addrTypeMap, + } + + if o.typeParser != nil { + ap.typeParser = o.typeParser + } + + if o.portFirst { + return portFirstAddressParser{ap: ap} + } + + return portLastAddressParser{ap: ap} +} + +type portFirstAddressParser struct { + ap *addressParser +} + +func (p portFirstAddressParser) ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) { + if buffer == nil { + buffer = buf.New() + defer buffer.Release() + } + + port, err := readPort(buffer, input) + if err != nil { + return nil, 0, err + } + + addr, err := p.ap.readAddress(buffer, input) + if err != nil { + return nil, 0, err + } + return addr, port, nil +} + +func (p portFirstAddressParser) WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error { + if err := writePort(writer, port); err != nil { + return err + } + + return p.ap.writeAddress(writer, addr) +} + +type portLastAddressParser struct { + ap *addressParser +} + +func (p portLastAddressParser) ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) { + if buffer == nil { + buffer = buf.New() + defer buffer.Release() + } + + addr, err := p.ap.readAddress(buffer, input) + if err != nil { + return nil, 0, err + } + + port, err := readPort(buffer, input) + if err != nil { + return nil, 0, err + } + + return addr, port, nil +} + +func (p portLastAddressParser) WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error { + if err := p.ap.writeAddress(writer, addr); err != nil { + return err + } + + return writePort(writer, port) +} + +func readPort(b *buf.Buffer, reader io.Reader) (net.Port, error) { + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return 0, err + } + return net.PortFromBytes(b.BytesFrom(-2)), nil +} + +func writePort(writer io.Writer, port net.Port) error { + return common.Error2(serial.WriteUint16(writer, port.Value())) +} + +func maybeIPPrefix(b byte) bool { + return b == '[' || (b >= '0' && b <= '9') +} + +func isValidDomain(d string) bool { + for _, c := range d { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '.' || c == '_') { + return false + } + } + return true +} + +type addressParser struct { + addrTypeMap [16]net.AddressFamily + addrByteMap [16]byte + typeParser AddressTypeParser +} + +func (p *addressParser) readAddress(b *buf.Buffer, reader io.Reader) (net.Address, error) { + if _, err := b.ReadFullFrom(reader, 1); err != nil { + return nil, err + } + + addrType := b.Byte(b.Len() - 1) + if p.typeParser != nil { + addrType = p.typeParser(addrType) + } + + if addrType >= 16 { + return nil, newError("unknown address type: ", addrType) + } + + addrFamily := p.addrTypeMap[addrType] + if addrFamily == net.AddressFamily(afInvalid) { + return nil, newError("unknown address type: ", addrType) + } + + switch addrFamily { + case net.AddressFamilyIPv4: + if _, err := b.ReadFullFrom(reader, 4); err != nil { + return nil, err + } + return net.IPAddress(b.BytesFrom(-4)), nil + case net.AddressFamilyIPv6: + if _, err := b.ReadFullFrom(reader, 16); err != nil { + return nil, err + } + return net.IPAddress(b.BytesFrom(-16)), nil + case net.AddressFamilyDomain: + if _, err := b.ReadFullFrom(reader, 1); err != nil { + return nil, err + } + domainLength := int32(b.Byte(b.Len() - 1)) + if _, err := b.ReadFullFrom(reader, domainLength); err != nil { + return nil, err + } + domain := string(b.BytesFrom(-domainLength)) + if maybeIPPrefix(domain[0]) { + addr := net.ParseAddress(domain) + if addr.Family().IsIP() { + return addr, nil + } + } + if !isValidDomain(domain) { + return nil, newError("invalid domain name: ", domain) + } + return net.DomainAddress(domain), nil + default: + panic("impossible case") + } +} + +func (p *addressParser) writeAddress(writer io.Writer, address net.Address) error { + tb := p.addrByteMap[address.Family()] + if tb == afInvalid { + return newError("unknown address family", address.Family()) + } + + switch address.Family() { + case net.AddressFamilyIPv4, net.AddressFamilyIPv6: + if _, err := writer.Write([]byte{tb}); err != nil { + return err + } + if _, err := writer.Write(address.IP()); err != nil { + return err + } + case net.AddressFamilyDomain: + domain := address.Domain() + if isDomainTooLong(domain) { + return newError("Super long domain is not supported: ", domain) + } + + if _, err := writer.Write([]byte{tb, byte(len(domain))}); err != nil { + return err + } + if _, err := writer.Write([]byte(domain)); err != nil { + return err + } + default: + panic("Unknown family type.") + } + + return nil +} diff --git a/common/protocol/address_test.go b/common/protocol/address_test.go new file mode 100644 index 00000000..508db809 --- /dev/null +++ b/common/protocol/address_test.go @@ -0,0 +1,242 @@ +package protocol_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" +) + +func TestAddressReading(t *testing.T) { + data := []struct { + Options []AddressOption + Input []byte + Address net.Address + Port net.Port + Error bool + }{ + { + Options: []AddressOption{}, + Input: []byte{}, + Error: true, + }, + { + Options: []AddressOption{}, + Input: []byte{0, 0, 0, 0, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Input: []byte{1, 0, 0, 0, 0, 0, 53}, + Address: net.IPAddress([]byte{0, 0, 0, 0}), + Port: net.Port(53), + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4), PortThenAddress()}, + Input: []byte{0, 53, 1, 0, 0, 0, 0}, + Address: net.IPAddress([]byte{0, 0, 0, 0}), + Port: net.Port(53), + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Input: []byte{1, 0, 0, 0, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x04, net.AddressFamilyIPv6)}, + Input: []byte{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 80}, + Address: net.IPAddress([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 9, 118, 50, 114, 97, 121, 46, 99, 111, 109, 0, 80}, + Address: net.DomainAddress("example.com"), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 9, 118, 50, 114, 97, 121, 46, 99, 111, 109, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 7, 56, 46, 56, 46, 56, 46, 56, 0, 80}, + Address: net.ParseAddress("8.8.8.8"), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 7, 10, 46, 56, 46, 56, 46, 56, 0, 80}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: append(append([]byte{3, 24}, []byte("2a00:1450:4007:816::200e")...), 0, 80), + Address: net.ParseAddress("2a00:1450:4007:816::200e"), + Port: net.Port(80), + }, + } + + for _, tc := range data { + b := buf.New() + parser := NewAddressParser(tc.Options...) + addr, port, err := parser.ReadAddressPort(b, bytes.NewReader(tc.Input)) + b.Release() + if tc.Error { + if err == nil { + t.Errorf("Expect error but not: %v", tc) + } + } else { + if err != nil { + t.Errorf("Expect no error but: %s %v", err.Error(), tc) + } + + if addr != tc.Address { + t.Error("Got address ", addr.String(), " want ", tc.Address.String()) + } + + if tc.Port != port { + t.Error("Got port ", port, " want ", tc.Port) + } + } + } +} + +func TestAddressWriting(t *testing.T) { + data := []struct { + Options []AddressOption + Address net.Address + Port net.Port + Bytes []byte + Error bool + }{ + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Address: net.LocalHostIP, + Port: net.Port(80), + Bytes: []byte{1, 127, 0, 0, 1, 0, 80}, + }, + } + + for _, tc := range data { + parser := NewAddressParser(tc.Options...) + + b := buf.New() + err := parser.WriteAddressPort(b, tc.Address, tc.Port) + if tc.Error { + if err == nil { + t.Error("Expect error but nil") + } + } else { + common.Must(err) + if diff := cmp.Diff(tc.Bytes, b.Bytes()); diff != "" { + t.Error(err) + } + } + } +} + +func BenchmarkAddressReadingIPv4(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x01, net.AddressFamilyIPv4)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{1, 0, 0, 0, 0, 0, 53} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressReadingIPv6(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x04, net.AddressFamilyIPv6)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 80} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressReadingDomain(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x03, net.AddressFamilyDomain)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{3, 9, 118, 50, 114, 97, 121, 46, 99, 111, 109, 0, 80} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressWritingIPv4(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x01, net.AddressFamilyIPv4)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.LocalHostIP, net.Port(80))) + writer.Clear() + } +} + +func BenchmarkAddressWritingIPv6(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x04, net.AddressFamilyIPv6)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.LocalHostIPv6, net.Port(80))) + writer.Clear() + } +} + +func BenchmarkAddressWritingDomain(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x02, net.AddressFamilyDomain)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.DomainAddress("www.example.com"), net.Port(80))) + writer.Clear() + } +} diff --git a/common/protocol/bittorrent/bittorrent.go b/common/protocol/bittorrent/bittorrent.go new file mode 100644 index 00000000..dee5816f --- /dev/null +++ b/common/protocol/bittorrent/bittorrent.go @@ -0,0 +1,32 @@ +package bittorrent + +import ( + "errors" + + "github.com/xtls/xray-core/v1/common" +) + +type SniffHeader struct { +} + +func (h *SniffHeader) Protocol() string { + return "bittorrent" +} + +func (h *SniffHeader) Domain() string { + return "" +} + +var errNotBittorrent = errors.New("not bittorrent header") + +func SniffBittorrent(b []byte) (*SniffHeader, error) { + if len(b) < 20 { + return nil, common.ErrNoClue + } + + if b[0] == 19 && string(b[1:20]) == "BitTorrent protocol" { + return &SniffHeader{}, nil + } + + return nil, errNotBittorrent +} diff --git a/common/protocol/context.go b/common/protocol/context.go new file mode 100644 index 00000000..6bb51042 --- /dev/null +++ b/common/protocol/context.go @@ -0,0 +1,23 @@ +package protocol + +import ( + "context" +) + +type key int + +const ( + requestKey key = iota +) + +func ContextWithRequestHeader(ctx context.Context, request *RequestHeader) context.Context { + return context.WithValue(ctx, requestKey, request) +} + +func RequestHeaderFromContext(ctx context.Context) *RequestHeader { + request := ctx.Value(requestKey) + if request == nil { + return nil + } + return request.(*RequestHeader) +} diff --git a/common/protocol/dns/errors.generated.go b/common/protocol/dns/errors.generated.go new file mode 100644 index 00000000..ce4c0605 --- /dev/null +++ b/common/protocol/dns/errors.generated.go @@ -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{}) +} diff --git a/common/protocol/dns/io.go b/common/protocol/dns/io.go new file mode 100644 index 00000000..deeb8bb4 --- /dev/null +++ b/common/protocol/dns/io.go @@ -0,0 +1,143 @@ +package dns + +import ( + "encoding/binary" + "sync" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/serial" + "golang.org/x/net/dns/dnsmessage" +) + +func PackMessage(msg *dnsmessage.Message) (*buf.Buffer, error) { + buffer := buf.New() + rawBytes := buffer.Extend(buf.Size) + packed, err := msg.AppendPack(rawBytes[:0]) + if err != nil { + buffer.Release() + return nil, err + } + buffer.Resize(0, int32(len(packed))) + return buffer, nil +} + +type MessageReader interface { + ReadMessage() (*buf.Buffer, error) +} + +type UDPReader struct { + buf.Reader + + access sync.Mutex + cache buf.MultiBuffer +} + +func (r *UDPReader) readCache() *buf.Buffer { + r.access.Lock() + defer r.access.Unlock() + + mb, b := buf.SplitFirst(r.cache) + r.cache = mb + return b +} + +func (r *UDPReader) refill() error { + mb, err := r.Reader.ReadMultiBuffer() + if err != nil { + return err + } + r.access.Lock() + r.cache = mb + r.access.Unlock() + return nil +} + +// ReadMessage implements MessageReader. +func (r *UDPReader) ReadMessage() (*buf.Buffer, error) { + for { + b := r.readCache() + if b != nil { + return b, nil + } + if err := r.refill(); err != nil { + return nil, err + } + } +} + +// Close implements common.Closable. +func (r *UDPReader) Close() error { + defer func() { + r.access.Lock() + buf.ReleaseMulti(r.cache) + r.cache = nil + r.access.Unlock() + }() + + return common.Close(r.Reader) +} + +type TCPReader struct { + reader *buf.BufferedReader +} + +func NewTCPReader(reader buf.Reader) *TCPReader { + return &TCPReader{ + reader: &buf.BufferedReader{ + Reader: reader, + }, + } +} + +func (r *TCPReader) ReadMessage() (*buf.Buffer, error) { + size, err := serial.ReadUint16(r.reader) + if err != nil { + return nil, err + } + if size > buf.Size { + return nil, newError("message size too large: ", size) + } + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, int32(size)); err != nil { + return nil, err + } + return b, nil +} + +func (r *TCPReader) Interrupt() { + common.Interrupt(r.reader) +} + +func (r *TCPReader) Close() error { + return common.Close(r.reader) +} + +type MessageWriter interface { + WriteMessage(msg *buf.Buffer) error +} + +type UDPWriter struct { + buf.Writer +} + +func (w *UDPWriter) WriteMessage(b *buf.Buffer) error { + return w.WriteMultiBuffer(buf.MultiBuffer{b}) +} + +type TCPWriter struct { + buf.Writer +} + +func (w *TCPWriter) WriteMessage(b *buf.Buffer) error { + if b.IsEmpty() { + return nil + } + + mb := make(buf.MultiBuffer, 0, 2) + + size := buf.New() + binary.BigEndian.PutUint16(size.Extend(2), uint16(b.Len())) + mb = append(mb, size, b) + return w.WriteMultiBuffer(mb) +} diff --git a/common/protocol/errors.generated.go b/common/protocol/errors.generated.go new file mode 100644 index 00000000..d09ca6a4 --- /dev/null +++ b/common/protocol/errors.generated.go @@ -0,0 +1,9 @@ +package protocol + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/protocol/headers.go b/common/protocol/headers.go new file mode 100644 index 00000000..60e5da13 --- /dev/null +++ b/common/protocol/headers.go @@ -0,0 +1,92 @@ +package protocol + +import ( + "runtime" + + "github.com/xtls/xray-core/v1/common/bitmask" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/uuid" +) + +// RequestCommand is a custom command in a proxy request. +type RequestCommand byte + +const ( + RequestCommandTCP = RequestCommand(0x01) + RequestCommandUDP = RequestCommand(0x02) + RequestCommandMux = RequestCommand(0x03) +) + +func (c RequestCommand) TransferType() TransferType { + switch c { + case RequestCommandTCP, RequestCommandMux: + return TransferTypeStream + case RequestCommandUDP: + return TransferTypePacket + default: + return TransferTypeStream + } +} + +const ( + // RequestOptionChunkStream indicates request payload is chunked. Each chunk consists of length, authentication and payload. + RequestOptionChunkStream bitmask.Byte = 0x01 + + // RequestOptionConnectionReuse indicates client side expects to reuse the connection. + RequestOptionConnectionReuse bitmask.Byte = 0x02 + + RequestOptionChunkMasking bitmask.Byte = 0x04 + + RequestOptionGlobalPadding bitmask.Byte = 0x08 +) + +type RequestHeader struct { + Version byte + Command RequestCommand + Option bitmask.Byte + Security SecurityType + Port net.Port + Address net.Address + User *MemoryUser +} + +func (h *RequestHeader) Destination() net.Destination { + if h.Command == RequestCommandUDP { + return net.UDPDestination(h.Address, h.Port) + } + return net.TCPDestination(h.Address, h.Port) +} + +const ( + ResponseOptionConnectionReuse bitmask.Byte = 0x01 +) + +type ResponseCommand interface{} + +type ResponseHeader struct { + Option bitmask.Byte + Command ResponseCommand +} + +type CommandSwitchAccount struct { + Host net.Address + Port net.Port + ID uuid.UUID + Level uint32 + AlterIds uint16 + ValidMin byte +} + +func (sc *SecurityConfig) GetSecurityType() SecurityType { + if sc == nil || sc.Type == SecurityType_AUTO { + if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" { + return SecurityType_AES128_GCM + } + return SecurityType_CHACHA20_POLY1305 + } + return sc.Type +} + +func isDomainTooLong(domain string) bool { + return len(domain) > 256 +} diff --git a/common/protocol/headers.pb.go b/common/protocol/headers.pb.go new file mode 100644 index 00000000..11a2e6cb --- /dev/null +++ b/common/protocol/headers.pb.go @@ -0,0 +1,224 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/protocol/headers.proto + +package protocol + +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 SecurityType int32 + +const ( + SecurityType_UNKNOWN SecurityType = 0 + SecurityType_LEGACY SecurityType = 1 + SecurityType_AUTO SecurityType = 2 + SecurityType_AES128_GCM SecurityType = 3 + SecurityType_CHACHA20_POLY1305 SecurityType = 4 + SecurityType_NONE SecurityType = 5 +) + +// Enum value maps for SecurityType. +var ( + SecurityType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "LEGACY", + 2: "AUTO", + 3: "AES128_GCM", + 4: "CHACHA20_POLY1305", + 5: "NONE", + } + SecurityType_value = map[string]int32{ + "UNKNOWN": 0, + "LEGACY": 1, + "AUTO": 2, + "AES128_GCM": 3, + "CHACHA20_POLY1305": 4, + "NONE": 5, + } +) + +func (x SecurityType) Enum() *SecurityType { + p := new(SecurityType) + *p = x + return p +} + +func (x SecurityType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SecurityType) Descriptor() protoreflect.EnumDescriptor { + return file_common_protocol_headers_proto_enumTypes[0].Descriptor() +} + +func (SecurityType) Type() protoreflect.EnumType { + return &file_common_protocol_headers_proto_enumTypes[0] +} + +func (x SecurityType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SecurityType.Descriptor instead. +func (SecurityType) EnumDescriptor() ([]byte, []int) { + return file_common_protocol_headers_proto_rawDescGZIP(), []int{0} +} + +type SecurityConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type SecurityType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.common.protocol.SecurityType" json:"type,omitempty"` +} + +func (x *SecurityConfig) Reset() { + *x = SecurityConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_common_protocol_headers_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecurityConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecurityConfig) ProtoMessage() {} + +func (x *SecurityConfig) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_headers_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 SecurityConfig.ProtoReflect.Descriptor instead. +func (*SecurityConfig) Descriptor() ([]byte, []int) { + return file_common_protocol_headers_proto_rawDescGZIP(), []int{0} +} + +func (x *SecurityConfig) GetType() SecurityType { + if x != nil { + return x.Type + } + return SecurityType_UNKNOWN +} + +var File_common_protocol_headers_proto protoreflect.FileDescriptor + +var file_common_protocol_headers_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x14, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x48, 0x0a, 0x0e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, + 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, + 0x62, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, + 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, 0x4f, + 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x41, 0x45, 0x53, 0x31, 0x32, 0x38, 0x5f, 0x47, 0x43, 0x4d, + 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x5f, 0x50, + 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, + 0x45, 0x10, 0x05, 0x42, 0x61, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 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, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0xaa, + 0x02, 0x14, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_protocol_headers_proto_rawDescOnce sync.Once + file_common_protocol_headers_proto_rawDescData = file_common_protocol_headers_proto_rawDesc +) + +func file_common_protocol_headers_proto_rawDescGZIP() []byte { + file_common_protocol_headers_proto_rawDescOnce.Do(func() { + file_common_protocol_headers_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_protocol_headers_proto_rawDescData) + }) + return file_common_protocol_headers_proto_rawDescData +} + +var file_common_protocol_headers_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_protocol_headers_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_headers_proto_goTypes = []interface{}{ + (SecurityType)(0), // 0: xray.common.protocol.SecurityType + (*SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig +} +var file_common_protocol_headers_proto_depIdxs = []int32{ + 0, // 0: xray.common.protocol.SecurityConfig.type:type_name -> xray.common.protocol.SecurityType + 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_common_protocol_headers_proto_init() } +func file_common_protocol_headers_proto_init() { + if File_common_protocol_headers_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_protocol_headers_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecurityConfig); 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_common_protocol_headers_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_headers_proto_goTypes, + DependencyIndexes: file_common_protocol_headers_proto_depIdxs, + EnumInfos: file_common_protocol_headers_proto_enumTypes, + MessageInfos: file_common_protocol_headers_proto_msgTypes, + }.Build() + File_common_protocol_headers_proto = out.File + file_common_protocol_headers_proto_rawDesc = nil + file_common_protocol_headers_proto_goTypes = nil + file_common_protocol_headers_proto_depIdxs = nil +} diff --git a/common/protocol/headers.proto b/common/protocol/headers.proto new file mode 100644 index 00000000..78150e64 --- /dev/null +++ b/common/protocol/headers.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/v1/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +enum SecurityType { + UNKNOWN = 0; + LEGACY = 1; + AUTO = 2; + AES128_GCM = 3; + CHACHA20_POLY1305 = 4; + NONE = 5; +} + +message SecurityConfig { + SecurityType type = 1; +} diff --git a/common/protocol/http/headers.go b/common/protocol/http/headers.go new file mode 100644 index 00000000..ee16467a --- /dev/null +++ b/common/protocol/http/headers.go @@ -0,0 +1,68 @@ +package http + +import ( + "net/http" + "strconv" + "strings" + + "github.com/xtls/xray-core/v1/common/net" +) + +// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it. +func ParseXForwardedFor(header http.Header) []net.Address { + xff := header.Get("X-Forwarded-For") + if xff == "" { + return nil + } + list := strings.Split(xff, ",") + addrs := make([]net.Address, 0, len(list)) + for _, proxy := range list { + addrs = append(addrs, net.ParseAddress(proxy)) + } + return addrs +} + +// RemoveHopByHopHeaders remove hop by hop headers in http header list. +func RemoveHopByHopHeaders(header http.Header) { + // Strip hop-by-hop header based on RFC: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 + // https://www.mnot.net/blog/2011/07/11/what_proxies_must_do + + header.Del("Proxy-Connection") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") + + connections := header.Get("Connection") + header.Del("Connection") + if connections == "" { + return + } + for _, h := range strings.Split(connections, ",") { + header.Del(strings.TrimSpace(h)) + } +} + +// ParseHost splits host and port from a raw string. Default port is used when raw string doesn't contain port. +func ParseHost(rawHost string, defaultPort net.Port) (net.Destination, error) { + port := defaultPort + host, rawPort, err := net.SplitHostPort(rawHost) + if err != nil { + if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { + host = rawHost + } else { + return net.Destination{}, err + } + } else if len(rawPort) > 0 { + intPort, err := strconv.Atoi(rawPort) + if err != nil { + return net.Destination{}, err + } + port = net.Port(intPort) + } + + return net.TCPDestination(net.ParseAddress(host), port), nil +} diff --git a/common/protocol/http/headers_test.go b/common/protocol/http/headers_test.go new file mode 100644 index 00000000..ee9d5ef3 --- /dev/null +++ b/common/protocol/http/headers_test.go @@ -0,0 +1,118 @@ +package http_test + +import ( + "bufio" + "net/http" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/common/protocol/http" +) + +func TestParseXForwardedFor(t *testing.T) { + header := http.Header{} + header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") + addrs := ParseXForwardedFor(header) + if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" { + t.Error(r) + } +} + +func TestHopByHopHeadersRemoving(t *testing.T) { + rawRequest := `GET /pkg/net/http/ HTTP/1.1 +Host: golang.org +Connection: keep-alive,Foo, Bar +Foo: foo +Bar: bar +Proxy-Connection: keep-alive +Proxy-Authenticate: abc +Accept-Encoding: gzip +Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7 +Cache-Control: no-cache +Accept-Language: de,en;q=0.7,en-us;q=0.3 + +` + b := bufio.NewReader(strings.NewReader(rawRequest)) + req, err := http.ReadRequest(b) + common.Must(err) + headers := []struct { + Key string + Value string + }{ + { + Key: "Foo", + Value: "foo", + }, + { + Key: "Bar", + Value: "bar", + }, + { + Key: "Connection", + Value: "keep-alive,Foo, Bar", + }, + { + Key: "Proxy-Connection", + Value: "keep-alive", + }, + { + Key: "Proxy-Authenticate", + Value: "abc", + }, + } + for _, header := range headers { + if v := req.Header.Get(header.Key); v != header.Value { + t.Error("header ", header.Key, " = ", v, " want ", header.Value) + } + } + + RemoveHopByHopHeaders(req.Header) + + for _, header := range []string{"Connection", "Foo", "Bar", "Proxy-Connection", "Proxy-Authenticate"} { + if v := req.Header.Get(header); v != "" { + t.Error("header ", header, " = ", v) + } + } +} + +func TestParseHost(t *testing.T) { + testCases := []struct { + RawHost string + DefaultPort net.Port + Destination net.Destination + Error bool + }{ + { + RawHost: "example.com:80", + DefaultPort: 443, + Destination: net.TCPDestination(net.DomainAddress("example.com"), 80), + }, + { + RawHost: "tls.example.com", + DefaultPort: 443, + Destination: net.TCPDestination(net.DomainAddress("tls.example.com"), 443), + }, + { + RawHost: "[2401:1bc0:51f0:ec08::1]:80", + DefaultPort: 443, + Destination: net.TCPDestination(net.ParseAddress("[2401:1bc0:51f0:ec08::1]"), 80), + }, + } + + for _, testCase := range testCases { + dest, err := ParseHost(testCase.RawHost, testCase.DefaultPort) + if testCase.Error { + if err == nil { + t.Error("for test case: ", testCase.RawHost, " expected error, but actually nil") + } + } else { + if dest != testCase.Destination { + t.Error("for test case: ", testCase.RawHost, " expected host: ", testCase.Destination.String(), " but got ", dest.String()) + } + } + } +} diff --git a/common/protocol/http/sniff.go b/common/protocol/http/sniff.go new file mode 100644 index 00000000..4cb42d82 --- /dev/null +++ b/common/protocol/http/sniff.go @@ -0,0 +1,94 @@ +package http + +import ( + "bytes" + "errors" + "strings" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" +) + +type version byte + +const ( + HTTP1 version = iota + HTTP2 +) + +type SniffHeader struct { + version version + host string +} + +func (h *SniffHeader) Protocol() string { + switch h.version { + case HTTP1: + return "http1" + case HTTP2: + return "http2" + default: + return "unknown" + } +} + +func (h *SniffHeader) Domain() string { + return h.host +} + +var ( + methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} + + errNotHTTPMethod = errors.New("not an HTTP method") +) + +func beginWithHTTPMethod(b []byte) error { + for _, m := range &methods { + if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { + return nil + } + + if len(b) < len(m) { + return common.ErrNoClue + } + } + + return errNotHTTPMethod +} + +func SniffHTTP(b []byte) (*SniffHeader, error) { + if err := beginWithHTTPMethod(b); err != nil { + return nil, err + } + + sh := &SniffHeader{ + version: HTTP1, + } + + headers := bytes.Split(b, []byte{'\n'}) + for i := 1; i < len(headers); i++ { + header := headers[i] + if len(header) == 0 { + break + } + parts := bytes.SplitN(header, []byte{':'}, 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(string(parts[0])) + if key == "host" { + rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1]))) + dest, err := ParseHost(rawHost, net.Port(80)) + if err != nil { + return nil, err + } + sh.host = dest.Address.String() + } + } + + if len(sh.host) > 0 { + return sh, nil + } + + return nil, common.ErrNoClue +} diff --git a/common/protocol/http/sniff_test.go b/common/protocol/http/sniff_test.go new file mode 100644 index 00000000..dca249a8 --- /dev/null +++ b/common/protocol/http/sniff_test.go @@ -0,0 +1,105 @@ +package http_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/protocol/http" +) + +func TestHTTPHeaders(t *testing.T) { + cases := []struct { + input string + domain string + err bool + }{ + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1 +Host: net.tutsplus.com +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 +Pragma: no-cache +Cache-Control: no-cache`, + domain: "net.tutsplus.com", + }, + { + input: `POST /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "localhost", + }, + { + input: `X /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /foo.php HTTP/1.1 +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +Host: localhost +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`, + domain: "", + err: true, + }, + } + + for _, test := range cases { + header, err := SniffHTTP([]byte(test.input)) + if test.err { + if err == nil { + t.Errorf("Expect error but nil, in test: %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if header.Domain() != test.domain { + t.Error("expected domain ", test.domain, " but got ", header.Domain()) + } + } + } +} diff --git a/common/protocol/id.go b/common/protocol/id.go new file mode 100644 index 00000000..a46f9128 --- /dev/null +++ b/common/protocol/id.go @@ -0,0 +1,82 @@ +package protocol + +import ( + "crypto/hmac" + "crypto/md5" + "hash" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/uuid" +) + +const ( + IDBytesLen = 16 +) + +type IDHash func(key []byte) hash.Hash + +func DefaultIDHash(key []byte) hash.Hash { + return hmac.New(md5.New, key) +} + +// The ID of en entity, in the form of a UUID. +type ID struct { + uuid uuid.UUID + cmdKey [IDBytesLen]byte +} + +// Equals returns true if this ID equals to the other one. +func (id *ID) Equals(another *ID) bool { + return id.uuid.Equals(&(another.uuid)) +} + +func (id *ID) Bytes() []byte { + return id.uuid.Bytes() +} + +func (id *ID) String() string { + return id.uuid.String() +} + +func (id *ID) UUID() uuid.UUID { + return id.uuid +} + +func (id ID) CmdKey() []byte { + return id.cmdKey[:] +} + +// NewID returns an ID with given UUID. +func NewID(uuid uuid.UUID) *ID { + id := &ID{uuid: uuid} + md5hash := md5.New() + common.Must2(md5hash.Write(uuid.Bytes())) + common.Must2(md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))) + md5hash.Sum(id.cmdKey[:0]) + return id +} + +func nextID(u *uuid.UUID) uuid.UUID { + md5hash := md5.New() + common.Must2(md5hash.Write(u.Bytes())) + common.Must2(md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81"))) + var newid uuid.UUID + for { + md5hash.Sum(newid[:0]) + if !newid.Equals(u) { + return newid + } + common.Must2(md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2"))) + } +} + +func NewAlterIDs(primary *ID, alterIDCount uint16) []*ID { + alterIDs := make([]*ID, alterIDCount) + prevID := primary.UUID() + for idx := range alterIDs { + newid := nextID(&prevID) + alterIDs[idx] = NewID(newid) + prevID = newid + } + return alterIDs +} diff --git a/common/protocol/id_test.go b/common/protocol/id_test.go new file mode 100644 index 00000000..1d6cb7d2 --- /dev/null +++ b/common/protocol/id_test.go @@ -0,0 +1,21 @@ +package protocol_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" +) + +func TestIdEquals(t *testing.T) { + id1 := NewID(uuid.New()) + id2 := NewID(id1.UUID()) + + if !id1.Equals(id2) { + t.Error("expected id1 to equal id2, but actually not") + } + + if id1.String() != id2.String() { + t.Error(id1.String(), " != ", id2.String()) + } +} diff --git a/common/protocol/payload.go b/common/protocol/payload.go new file mode 100644 index 00000000..2f3c7290 --- /dev/null +++ b/common/protocol/payload.go @@ -0,0 +1,16 @@ +package protocol + +type TransferType byte + +const ( + TransferTypeStream TransferType = 0 + TransferTypePacket TransferType = 1 +) + +type AddressType byte + +const ( + AddressTypeIPv4 AddressType = 1 + AddressTypeDomain AddressType = 2 + AddressTypeIPv6 AddressType = 3 +) diff --git a/common/protocol/protocol.go b/common/protocol/protocol.go new file mode 100644 index 00000000..dc52c2a7 --- /dev/null +++ b/common/protocol/protocol.go @@ -0,0 +1,3 @@ +package protocol // import "github.com/xtls/xray-core/v1/common/protocol" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/common/protocol/server_picker.go b/common/protocol/server_picker.go new file mode 100644 index 00000000..62aa404e --- /dev/null +++ b/common/protocol/server_picker.go @@ -0,0 +1,89 @@ +package protocol + +import ( + "sync" +) + +type ServerList struct { + sync.RWMutex + servers []*ServerSpec +} + +func NewServerList() *ServerList { + return &ServerList{} +} + +func (sl *ServerList) AddServer(server *ServerSpec) { + sl.Lock() + defer sl.Unlock() + + sl.servers = append(sl.servers, server) +} + +func (sl *ServerList) Size() uint32 { + sl.RLock() + defer sl.RUnlock() + + return uint32(len(sl.servers)) +} + +func (sl *ServerList) GetServer(idx uint32) *ServerSpec { + sl.Lock() + defer sl.Unlock() + + for { + if idx >= uint32(len(sl.servers)) { + return nil + } + + server := sl.servers[idx] + if !server.IsValid() { + sl.removeServer(idx) + continue + } + + return server + } +} + +func (sl *ServerList) removeServer(idx uint32) { + n := len(sl.servers) + sl.servers[idx] = sl.servers[n-1] + sl.servers = sl.servers[:n-1] +} + +type ServerPicker interface { + PickServer() *ServerSpec +} + +type RoundRobinServerPicker struct { + sync.Mutex + serverlist *ServerList + nextIndex uint32 +} + +func NewRoundRobinServerPicker(serverlist *ServerList) *RoundRobinServerPicker { + return &RoundRobinServerPicker{ + serverlist: serverlist, + nextIndex: 0, + } +} + +func (p *RoundRobinServerPicker) PickServer() *ServerSpec { + p.Lock() + defer p.Unlock() + + next := p.nextIndex + server := p.serverlist.GetServer(next) + if server == nil { + next = 0 + server = p.serverlist.GetServer(0) + } + next++ + if next >= p.serverlist.Size() { + next = 0 + } + p.nextIndex = next + + return server +} diff --git a/common/protocol/server_picker_test.go b/common/protocol/server_picker_test.go new file mode 100644 index 00000000..674293a3 --- /dev/null +++ b/common/protocol/server_picker_test.go @@ -0,0 +1,71 @@ +package protocol_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/common/protocol" +) + +func TestServerList(t *testing.T) { + list := NewServerList() + list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(1)), AlwaysValid())) + if list.Size() != 1 { + t.Error("list size: ", list.Size()) + } + list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(2)), BeforeTime(time.Now().Add(time.Second)))) + if list.Size() != 2 { + t.Error("list.size: ", list.Size()) + } + + server := list.GetServer(1) + if server.Destination().Port != 2 { + t.Error("server: ", server.Destination()) + } + time.Sleep(2 * time.Second) + server = list.GetServer(1) + if server != nil { + t.Error("server: ", server) + } + + server = list.GetServer(0) + if server.Destination().Port != 1 { + t.Error("server: ", server.Destination()) + } +} + +func TestServerPicker(t *testing.T) { + list := NewServerList() + list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(1)), AlwaysValid())) + list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(2)), BeforeTime(time.Now().Add(time.Second)))) + list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(3)), BeforeTime(time.Now().Add(time.Second)))) + + picker := NewRoundRobinServerPicker(list) + server := picker.PickServer() + if server.Destination().Port != 1 { + t.Error("server: ", server.Destination()) + } + server = picker.PickServer() + if server.Destination().Port != 2 { + t.Error("server: ", server.Destination()) + } + server = picker.PickServer() + if server.Destination().Port != 3 { + t.Error("server: ", server.Destination()) + } + server = picker.PickServer() + if server.Destination().Port != 1 { + t.Error("server: ", server.Destination()) + } + + time.Sleep(2 * time.Second) + server = picker.PickServer() + if server.Destination().Port != 1 { + t.Error("server: ", server.Destination()) + } + server = picker.PickServer() + if server.Destination().Port != 1 { + t.Error("server: ", server.Destination()) + } +} diff --git a/common/protocol/server_spec.go b/common/protocol/server_spec.go new file mode 100644 index 00000000..f950b1cc --- /dev/null +++ b/common/protocol/server_spec.go @@ -0,0 +1,122 @@ +package protocol + +import ( + "sync" + "time" + + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/net" +) + +type ValidationStrategy interface { + IsValid() bool + Invalidate() +} + +type alwaysValidStrategy struct{} + +func AlwaysValid() ValidationStrategy { + return alwaysValidStrategy{} +} + +func (alwaysValidStrategy) IsValid() bool { + return true +} + +func (alwaysValidStrategy) Invalidate() {} + +type timeoutValidStrategy struct { + until time.Time +} + +func BeforeTime(t time.Time) ValidationStrategy { + return &timeoutValidStrategy{ + until: t, + } +} + +func (s *timeoutValidStrategy) IsValid() bool { + return s.until.After(time.Now()) +} + +func (s *timeoutValidStrategy) Invalidate() { + s.until = time.Time{} +} + +type ServerSpec struct { + sync.RWMutex + dest net.Destination + users []*MemoryUser + valid ValidationStrategy +} + +func NewServerSpec(dest net.Destination, valid ValidationStrategy, users ...*MemoryUser) *ServerSpec { + return &ServerSpec{ + dest: dest, + users: users, + valid: valid, + } +} + +func NewServerSpecFromPB(spec *ServerEndpoint) (*ServerSpec, error) { + dest := net.TCPDestination(spec.Address.AsAddress(), net.Port(spec.Port)) + mUsers := make([]*MemoryUser, len(spec.User)) + for idx, u := range spec.User { + mUser, err := u.ToMemoryUser() + if err != nil { + return nil, err + } + mUsers[idx] = mUser + } + return NewServerSpec(dest, AlwaysValid(), mUsers...), nil +} + +func (s *ServerSpec) Destination() net.Destination { + return s.dest +} + +func (s *ServerSpec) HasUser(user *MemoryUser) bool { + s.RLock() + defer s.RUnlock() + + for _, u := range s.users { + if u.Account.Equals(user.Account) { + return true + } + } + return false +} + +func (s *ServerSpec) AddUser(user *MemoryUser) { + if s.HasUser(user) { + return + } + + s.Lock() + defer s.Unlock() + + s.users = append(s.users, user) +} + +func (s *ServerSpec) PickUser() *MemoryUser { + s.RLock() + defer s.RUnlock() + + userCount := len(s.users) + switch userCount { + case 0: + return nil + case 1: + return s.users[0] + default: + return s.users[dice.Roll(userCount)] + } +} + +func (s *ServerSpec) IsValid() bool { + return s.valid.IsValid() +} + +func (s *ServerSpec) Invalidate() { + s.valid.Invalidate() +} diff --git a/common/protocol/server_spec.pb.go b/common/protocol/server_spec.pb.go new file mode 100644 index 00000000..5bc2d39c --- /dev/null +++ b/common/protocol/server_spec.pb.go @@ -0,0 +1,186 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/protocol/server_spec.proto + +package protocol + +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 + +type ServerEndpoint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + User []*User `protobuf:"bytes,3,rep,name=user,proto3" json:"user,omitempty"` +} + +func (x *ServerEndpoint) Reset() { + *x = ServerEndpoint{} + if protoimpl.UnsafeEnabled { + mi := &file_common_protocol_server_spec_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEndpoint) ProtoMessage() {} + +func (x *ServerEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_server_spec_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 ServerEndpoint.ProtoReflect.Descriptor instead. +func (*ServerEndpoint) Descriptor() ([]byte, []int) { + return file_common_protocol_server_spec_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerEndpoint) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ServerEndpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ServerEndpoint) GetUser() []*User { + if x != nil { + return x.User + } + return nil +} + +var File_common_protocol_server_spec_proto protoreflect.FileDescriptor + +var file_common_protocol_server_spec_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 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, 0x1a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x8b, 0x01, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 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, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x42, 0x61, 0x0a, + 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0xaa, 0x02, 0x14, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_protocol_server_spec_proto_rawDescOnce sync.Once + file_common_protocol_server_spec_proto_rawDescData = file_common_protocol_server_spec_proto_rawDesc +) + +func file_common_protocol_server_spec_proto_rawDescGZIP() []byte { + file_common_protocol_server_spec_proto_rawDescOnce.Do(func() { + file_common_protocol_server_spec_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_protocol_server_spec_proto_rawDescData) + }) + return file_common_protocol_server_spec_proto_rawDescData +} + +var file_common_protocol_server_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_server_spec_proto_goTypes = []interface{}{ + (*ServerEndpoint)(nil), // 0: xray.common.protocol.ServerEndpoint + (*net.IPOrDomain)(nil), // 1: xray.common.net.IPOrDomain + (*User)(nil), // 2: xray.common.protocol.User +} +var file_common_protocol_server_spec_proto_depIdxs = []int32{ + 1, // 0: xray.common.protocol.ServerEndpoint.address:type_name -> xray.common.net.IPOrDomain + 2, // 1: xray.common.protocol.ServerEndpoint.user:type_name -> xray.common.protocol.User + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_common_protocol_server_spec_proto_init() } +func file_common_protocol_server_spec_proto_init() { + if File_common_protocol_server_spec_proto != nil { + return + } + file_common_protocol_user_proto_init() + if !protoimpl.UnsafeEnabled { + file_common_protocol_server_spec_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerEndpoint); 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_common_protocol_server_spec_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_server_spec_proto_goTypes, + DependencyIndexes: file_common_protocol_server_spec_proto_depIdxs, + MessageInfos: file_common_protocol_server_spec_proto_msgTypes, + }.Build() + File_common_protocol_server_spec_proto = out.File + file_common_protocol_server_spec_proto_rawDesc = nil + file_common_protocol_server_spec_proto_goTypes = nil + file_common_protocol_server_spec_proto_depIdxs = nil +} diff --git a/common/protocol/server_spec.proto b/common/protocol/server_spec.proto new file mode 100644 index 00000000..7753b943 --- /dev/null +++ b/common/protocol/server_spec.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/v1/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/protocol/user.proto"; + +message ServerEndpoint { + xray.common.net.IPOrDomain address = 1; + uint32 port = 2; + repeated xray.common.protocol.User user = 3; +} diff --git a/common/protocol/server_spec_test.go b/common/protocol/server_spec_test.go new file mode 100644 index 00000000..7b42b65e --- /dev/null +++ b/common/protocol/server_spec_test.go @@ -0,0 +1,79 @@ +package protocol_test + +import ( + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/proxy/vmess" +) + +func TestAlwaysValidStrategy(t *testing.T) { + strategy := AlwaysValid() + if !strategy.IsValid() { + t.Error("strategy not valid") + } + strategy.Invalidate() + if !strategy.IsValid() { + t.Error("strategy not valid") + } +} + +func TestTimeoutValidStrategy(t *testing.T) { + strategy := BeforeTime(time.Now().Add(2 * time.Second)) + if !strategy.IsValid() { + t.Error("strategy not valid") + } + time.Sleep(3 * time.Second) + if strategy.IsValid() { + t.Error("strategy is valid") + } + + strategy = BeforeTime(time.Now().Add(2 * time.Second)) + strategy.Invalidate() + if strategy.IsValid() { + t.Error("strategy is valid") + } +} + +func TestUserInServerSpec(t *testing.T) { + uuid1 := uuid.New() + uuid2 := uuid.New() + + toAccount := func(a *vmess.Account) Account { + account, err := a.AsAccount() + common.Must(err) + return account + } + + spec := NewServerSpec(net.Destination{}, AlwaysValid(), &MemoryUser{ + Email: "test1@example.com", + Account: toAccount(&vmess.Account{Id: uuid1.String()}), + }) + if spec.HasUser(&MemoryUser{ + Email: "test1@example.com", + Account: toAccount(&vmess.Account{Id: uuid2.String()}), + }) { + t.Error("has user: ", uuid2) + } + + spec.AddUser(&MemoryUser{Email: "test2@example.com"}) + if !spec.HasUser(&MemoryUser{ + Email: "test1@example.com", + Account: toAccount(&vmess.Account{Id: uuid1.String()}), + }) { + t.Error("not having user: ", uuid1) + } +} + +func TestPickUser(t *testing.T) { + spec := NewServerSpec(net.Destination{}, AlwaysValid(), &MemoryUser{Email: "test1@example.com"}, &MemoryUser{Email: "test2@example.com"}, &MemoryUser{Email: "test3@example.com"}) + user := spec.PickUser() + if !strings.HasSuffix(user.Email, "@example.com") { + t.Error("user: ", user.Email) + } +} diff --git a/common/protocol/time.go b/common/protocol/time.go new file mode 100644 index 00000000..1cef8fb4 --- /dev/null +++ b/common/protocol/time.go @@ -0,0 +1,22 @@ +package protocol + +import ( + "time" + + "github.com/xtls/xray-core/v1/common/dice" +) + +type Timestamp int64 + +type TimestampGenerator func() Timestamp + +func NowTime() Timestamp { + return Timestamp(time.Now().Unix()) +} + +func NewTimestampGenerator(base Timestamp, delta int) TimestampGenerator { + return func() Timestamp { + rangeInDelta := dice.Roll(delta*2) - delta + return base + Timestamp(rangeInDelta) + } +} diff --git a/common/protocol/time_test.go b/common/protocol/time_test.go new file mode 100644 index 00000000..45a32d2a --- /dev/null +++ b/common/protocol/time_test.go @@ -0,0 +1,21 @@ +package protocol_test + +import ( + "testing" + "time" + + . "github.com/xtls/xray-core/v1/common/protocol" +) + +func TestGenerateRandomInt64InRange(t *testing.T) { + base := time.Now().Unix() + delta := 100 + generator := NewTimestampGenerator(Timestamp(base), delta) + + for i := 0; i < 100; i++ { + val := int64(generator()) + if val > base+int64(delta) || val < base-int64(delta) { + t.Error(val, " not between ", base-int64(delta), " and ", base+int64(delta)) + } + } +} diff --git a/common/protocol/tls/cert/.gitignore b/common/protocol/tls/cert/.gitignore new file mode 100644 index 00000000..612424a3 --- /dev/null +++ b/common/protocol/tls/cert/.gitignore @@ -0,0 +1 @@ +*.pem \ No newline at end of file diff --git a/common/protocol/tls/cert/cert.go b/common/protocol/tls/cert/cert.go new file mode 100644 index 00000000..cf19ca67 --- /dev/null +++ b/common/protocol/tls/cert/cert.go @@ -0,0 +1,178 @@ +package cert + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "math/big" + "time" + + "github.com/xtls/xray-core/v1/common" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +type Certificate struct { + // Cerificate in ASN.1 DER format + Certificate []byte + // Private key in ASN.1 DER format + PrivateKey []byte +} + +func ParseCertificate(certPEM []byte, keyPEM []byte) (*Certificate, error) { + certBlock, _ := pem.Decode(certPEM) + if certBlock == nil { + return nil, newError("failed to decode certificate") + } + keyBlock, _ := pem.Decode(keyPEM) + if keyBlock == nil { + return nil, newError("failed to decode key") + } + return &Certificate{ + Certificate: certBlock.Bytes, + PrivateKey: keyBlock.Bytes, + }, nil +} + +func (c *Certificate) ToPEM() ([]byte, []byte) { + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Certificate}), + pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: c.PrivateKey}) +} + +type Option func(*x509.Certificate) + +func Authority(isCA bool) Option { + return func(cert *x509.Certificate) { + cert.IsCA = isCA + } +} + +func NotBefore(t time.Time) Option { + return func(c *x509.Certificate) { + c.NotBefore = t + } +} + +func NotAfter(t time.Time) Option { + return func(c *x509.Certificate) { + c.NotAfter = t + } +} + +func DNSNames(names ...string) Option { + return func(c *x509.Certificate) { + c.DNSNames = names + } +} + +func CommonName(name string) Option { + return func(c *x509.Certificate) { + c.Subject.CommonName = name + } +} + +func KeyUsage(usage x509.KeyUsage) Option { + return func(c *x509.Certificate) { + c.KeyUsage = usage + } +} + +func Organization(org string) Option { + return func(c *x509.Certificate) { + c.Subject.Organization = []string{org} + } +} + +func MustGenerate(parent *Certificate, opts ...Option) *Certificate { + cert, err := Generate(parent, opts...) + common.Must(err) + return cert +} + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} + +func Generate(parent *Certificate, opts ...Option) (*Certificate, error) { + var ( + pKey interface{} + parentKey interface{} + err error + ) + // higher signing performance than RSA2048 + selfKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, newError("failed to generate self private key").Base(err) + } + parentKey = selfKey + if parent != nil { + if _, e := asn1.Unmarshal(parent.PrivateKey, &ecPrivateKey{}); e == nil { + pKey, err = x509.ParseECPrivateKey(parent.PrivateKey) + } else if _, e := asn1.Unmarshal(parent.PrivateKey, &pkcs8{}); e == nil { + pKey, err = x509.ParsePKCS8PrivateKey(parent.PrivateKey) + } else if _, e := asn1.Unmarshal(parent.PrivateKey, &pkcs1PrivateKey{}); e == nil { + pKey, err = x509.ParsePKCS1PrivateKey(parent.PrivateKey) + } + if err != nil { + return nil, newError("failed to parse parent private key").Base(err) + } + parentKey = pKey + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, newError("failed to generate serial number").Base(err) + } + + template := &x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: time.Now().Add(time.Hour * -1), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + for _, opt := range opts { + opt(template) + } + + parentCert := template + if parent != nil { + pCert, err := x509.ParseCertificate(parent.Certificate) + if err != nil { + return nil, newError("failed to parse parent certificate").Base(err) + } + parentCert = pCert + } + + derBytes, err := x509.CreateCertificate(rand.Reader, template, parentCert, publicKey(selfKey), parentKey) + if err != nil { + return nil, newError("failed to create certificate").Base(err) + } + + privateKey, err := x509.MarshalPKCS8PrivateKey(selfKey) + if err != nil { + return nil, newError("Unable to marshal private key").Base(err) + } + + return &Certificate{ + Certificate: derBytes, + PrivateKey: privateKey, + }, nil +} diff --git a/common/protocol/tls/cert/cert_test.go b/common/protocol/tls/cert/cert_test.go new file mode 100644 index 00000000..7f92488a --- /dev/null +++ b/common/protocol/tls/cert/cert_test.go @@ -0,0 +1,92 @@ +package cert + +import ( + "context" + "crypto/x509" + "encoding/json" + "os" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/task" +) + +func TestGenerate(t *testing.T) { + err := generate(nil, true, true, "ca") + if err != nil { + t.Fatal(err) + } +} + +func generate(domainNames []string, isCA bool, jsonOutput bool, fileOutput string) error { + commonName := "Xray Root CA" + organization := "Xray Inc" + + expire := time.Hour * 3 + + var opts []Option + if isCA { + opts = append(opts, Authority(isCA)) + opts = append(opts, KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature)) + } + + opts = append(opts, NotAfter(time.Now().Add(expire))) + opts = append(opts, CommonName(commonName)) + if len(domainNames) > 0 { + opts = append(opts, DNSNames(domainNames...)) + } + opts = append(opts, Organization(organization)) + + cert, err := Generate(nil, opts...) + if err != nil { + return newError("failed to generate TLS certificate").Base(err) + } + + if jsonOutput { + printJSON(cert) + } + + if len(fileOutput) > 0 { + if err := printFile(cert, fileOutput); err != nil { + return err + } + } + + return nil +} + +type jsonCert struct { + Certificate []string `json:"certificate"` + Key []string `json:"key"` +} + +func printJSON(certificate *Certificate) { + certPEM, keyPEM := certificate.ToPEM() + jCert := &jsonCert{ + Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"), + Key: strings.Split(strings.TrimSpace(string(keyPEM)), "\n"), + } + content, err := json.MarshalIndent(jCert, "", " ") + common.Must(err) + os.Stdout.Write(content) + os.Stdout.WriteString("\n") +} +func printFile(certificate *Certificate, name string) error { + certPEM, keyPEM := certificate.ToPEM() + return task.Run(context.Background(), func() error { + return writeFile(certPEM, name+"_cert.pem") + }, func() error { + return writeFile(keyPEM, name+"_key.pem") + }) +} +func writeFile(content []byte, name string) error { + f, err := os.Create(name) + if err != nil { + return err + } + defer f.Close() + + return common.Error2(f.Write(content)) +} diff --git a/common/protocol/tls/cert/errors.generated.go b/common/protocol/tls/cert/errors.generated.go new file mode 100644 index 00000000..20e2effd --- /dev/null +++ b/common/protocol/tls/cert/errors.generated.go @@ -0,0 +1,9 @@ +package cert + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/protocol/tls/cert/privateKey.go b/common/protocol/tls/cert/privateKey.go new file mode 100644 index 00000000..52a8e68c --- /dev/null +++ b/common/protocol/tls/cert/privateKey.go @@ -0,0 +1,44 @@ +package cert + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "math/big" +) + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + // optional attributes omitted. +} + +type pkcs1AdditionalRSAPrime struct { + Prime *big.Int + + // We ignore these values because rsa will calculate them. + Exp *big.Int + Coeff *big.Int +} + +type pkcs1PrivateKey struct { + Version int + N *big.Int + E int + D *big.Int + P *big.Int + Q *big.Int + // We ignore these values, if present, because rsa will calculate them. + Dp *big.Int `asn1:"optional"` + Dq *big.Int `asn1:"optional"` + Qinv *big.Int `asn1:"optional"` + + AdditionalPrimes []pkcs1AdditionalRSAPrime `asn1:"optional,omitempty"` +} diff --git a/common/protocol/tls/sniff.go b/common/protocol/tls/sniff.go new file mode 100644 index 00000000..2d90268b --- /dev/null +++ b/common/protocol/tls/sniff.go @@ -0,0 +1,146 @@ +package tls + +import ( + "encoding/binary" + "errors" + "strings" + + "github.com/xtls/xray-core/v1/common" +) + +type SniffHeader struct { + domain string +} + +func (h *SniffHeader) Protocol() string { + return "tls" +} + +func (h *SniffHeader) Domain() string { + return h.domain +} + +var errNotTLS = errors.New("not TLS header") +var errNotClientHello = errors.New("not client hello") + +func IsValidTLSVersion(major, minor byte) bool { + return major == 3 +} + +// ReadClientHello returns server name (if any) from TLS client hello message. +// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300 +func ReadClientHello(data []byte, h *SniffHeader) error { + if len(data) < 42 { + return common.ErrNoClue + } + sessionIDLen := int(data[38]) + if sessionIDLen > 32 || len(data) < 39+sessionIDLen { + return common.ErrNoClue + } + data = data[39+sessionIDLen:] + if len(data) < 2 { + return common.ErrNoClue + } + // cipherSuiteLen is the number of bytes of cipher suite numbers. Since + // they are uint16s, the number must be even. + cipherSuiteLen := int(data[0])<<8 | int(data[1]) + if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { + return errNotClientHello + } + data = data[2+cipherSuiteLen:] + if len(data) < 1 { + return common.ErrNoClue + } + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return common.ErrNoClue + } + data = data[1+compressionMethodsLen:] + + if len(data) == 0 { + return errNotClientHello + } + if len(data) < 2 { + return errNotClientHello + } + + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + if extensionsLength != len(data) { + return errNotClientHello + } + + for len(data) != 0 { + if len(data) < 4 { + return errNotClientHello + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + if len(data) < length { + return errNotClientHello + } + + if extension == 0x00 { /* extensionServerName */ + d := data[:length] + if len(d) < 2 { + return errNotClientHello + } + namesLen := int(d[0])<<8 | int(d[1]) + d = d[2:] + if len(d) != namesLen { + return errNotClientHello + } + for len(d) > 0 { + if len(d) < 3 { + return errNotClientHello + } + nameType := d[0] + nameLen := int(d[1])<<8 | int(d[2]) + d = d[3:] + if len(d) < nameLen { + return errNotClientHello + } + if nameType == 0 { + serverName := string(d[:nameLen]) + // An SNI value may not include a + // trailing dot. See + // https://tools.ietf.org/html/rfc6066#section-3. + if strings.HasSuffix(serverName, ".") { + return errNotClientHello + } + h.domain = serverName + return nil + } + d = d[nameLen:] + } + } + data = data[length:] + } + + return errNotTLS +} + +func SniffTLS(b []byte) (*SniffHeader, error) { + if len(b) < 5 { + return nil, common.ErrNoClue + } + + if b[0] != 0x16 /* TLS Handshake */ { + return nil, errNotTLS + } + if !IsValidTLSVersion(b[1], b[2]) { + return nil, errNotTLS + } + headerLen := int(binary.BigEndian.Uint16(b[3:5])) + if 5+headerLen > len(b) { + return nil, common.ErrNoClue + } + + h := &SniffHeader{} + err := ReadClientHello(b[5:5+headerLen], h) + if err == nil { + return h, nil + } + return nil, err +} diff --git a/common/protocol/tls/sniff_test.go b/common/protocol/tls/sniff_test.go new file mode 100644 index 00000000..f5ae1eb4 --- /dev/null +++ b/common/protocol/tls/sniff_test.go @@ -0,0 +1,161 @@ +package tls_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/protocol/tls" +) + +func TestTLSHeaders(t *testing.T) { + cases := []struct { + input []byte + domain string + err bool + }{ + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe, + 0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4, + 0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36, + 0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43, + 0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, + 0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d, + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, + 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, + 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, + 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, + 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, + 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, + 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, + 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, + 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, + 0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, + 0xaa, 0xaa, 0x00, 0x01, 0x00, + }, + domain: "c.s-microsoft.com", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00, + 0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca, + 0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5, + 0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e, + 0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca, + 0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00, + 0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74, + 0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85, + 0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea, + 0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30, + 0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74, + 0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, + 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, + 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, + 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, + 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, + 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, + 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a, + 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a, + 0x00, 0x01, 0x00, + }, + domain: "www07.clicktale.net", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1, + 0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6, + 0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d, + 0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84, + 0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08, + 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, + 0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, + 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, + 0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, + 0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00, + 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c, + 0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01, + 0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72, + }, + domain: "dogfish", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, + 0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee, + 0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14, + 0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62, + 0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45, + 0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c, + 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, + 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e, + 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, + 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, + 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, + 0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03, + 0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, + 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, + 0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30, + 0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a, + 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, + 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, + 0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03, + 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, + 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, + 0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02, + 0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f, + 0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, + 0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28, + 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, + 0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e, + 0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f, + 0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d, + 0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36, + }, + domain: "10.42.0.243", + err: false, + }, + } + + for _, test := range cases { + header, err := SniffTLS(test.input) + if test.err { + if err == nil { + t.Errorf("Exepct error but nil in test %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if header.Domain() != test.domain { + t.Error("expect domain ", test.domain, " but got ", header.Domain()) + } + } + } +} diff --git a/common/protocol/udp/packet.go b/common/protocol/udp/packet.go new file mode 100644 index 00000000..28134559 --- /dev/null +++ b/common/protocol/udp/packet.go @@ -0,0 +1,13 @@ +package udp + +import ( + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" +) + +// Packet is a UDP packet together with its source and destination address. +type Packet struct { + Payload *buf.Buffer + Source net.Destination + Target net.Destination +} diff --git a/common/protocol/udp/udp.go b/common/protocol/udp/udp.go new file mode 100644 index 00000000..0213f41f --- /dev/null +++ b/common/protocol/udp/udp.go @@ -0,0 +1 @@ +package udp diff --git a/common/protocol/user.go b/common/protocol/user.go new file mode 100644 index 00000000..8325f555 --- /dev/null +++ b/common/protocol/user.go @@ -0,0 +1,39 @@ +package protocol + +func (u *User) GetTypedAccount() (Account, error) { + if u.GetAccount() == nil { + return nil, newError("Account missing").AtWarning() + } + + rawAccount, err := u.Account.GetInstance() + if err != nil { + return nil, err + } + if asAccount, ok := rawAccount.(AsAccount); ok { + return asAccount.AsAccount() + } + if account, ok := rawAccount.(Account); ok { + return account, nil + } + return nil, newError("Unknown account type: ", u.Account.Type) +} + +func (u *User) ToMemoryUser() (*MemoryUser, error) { + account, err := u.GetTypedAccount() + if err != nil { + return nil, err + } + return &MemoryUser{ + Account: account, + Email: u.Email, + Level: u.Level, + }, nil +} + +// MemoryUser is a parsed form of User, to reduce number of parsing of Account proto. +type MemoryUser struct { + // Account is the parsed account of the protocol. + Account Account + Email string + Level uint32 +} diff --git a/common/protocol/user.pb.go b/common/protocol/user.pb.go new file mode 100644 index 00000000..0aee3a61 --- /dev/null +++ b/common/protocol/user.pb.go @@ -0,0 +1,182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/protocol/user.proto + +package protocol + +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 + +// User is a generic user for all procotols. +type User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Level uint32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + // Protocol specific account information. Must be the account proto in one of + // the proxies. + Account *serial.TypedMessage `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` +} + +func (x *User) Reset() { + *x = User{} + if protoimpl.UnsafeEnabled { + mi := &file_common_protocol_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_user_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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_common_protocol_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetLevel() uint32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetAccount() *serial.TypedMessage { + if x != nil { + return x.Account + } + return nil +} + +var File_common_protocol_user_proto protoreflect.FileDescriptor + +var file_common_protocol_user_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 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, 0x6e, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x07, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 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, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x61, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 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, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0xaa, 0x02, 0x14, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_protocol_user_proto_rawDescOnce sync.Once + file_common_protocol_user_proto_rawDescData = file_common_protocol_user_proto_rawDesc +) + +func file_common_protocol_user_proto_rawDescGZIP() []byte { + file_common_protocol_user_proto_rawDescOnce.Do(func() { + file_common_protocol_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_protocol_user_proto_rawDescData) + }) + return file_common_protocol_user_proto_rawDescData +} + +var file_common_protocol_user_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_user_proto_goTypes = []interface{}{ + (*User)(nil), // 0: xray.common.protocol.User + (*serial.TypedMessage)(nil), // 1: xray.common.serial.TypedMessage +} +var file_common_protocol_user_proto_depIdxs = []int32{ + 1, // 0: xray.common.protocol.User.account: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_common_protocol_user_proto_init() } +func file_common_protocol_user_proto_init() { + if File_common_protocol_user_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_protocol_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*User); 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_common_protocol_user_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_user_proto_goTypes, + DependencyIndexes: file_common_protocol_user_proto_depIdxs, + MessageInfos: file_common_protocol_user_proto_msgTypes, + }.Build() + File_common_protocol_user_proto = out.File + file_common_protocol_user_proto_rawDesc = nil + file_common_protocol_user_proto_goTypes = nil + file_common_protocol_user_proto_depIdxs = nil +} diff --git a/common/protocol/user.proto b/common/protocol/user.proto new file mode 100644 index 00000000..26543062 --- /dev/null +++ b/common/protocol/user.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/v1/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// User is a generic user for all procotols. +message User { + uint32 level = 1; + string email = 2; + + // Protocol specific account information. Must be the account proto in one of + // the proxies. + xray.common.serial.TypedMessage account = 3; +} diff --git a/common/retry/errors.generated.go b/common/retry/errors.generated.go new file mode 100644 index 00000000..68df9f0f --- /dev/null +++ b/common/retry/errors.generated.go @@ -0,0 +1,9 @@ +package retry + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/common/retry/retry.go b/common/retry/retry.go new file mode 100644 index 00000000..17a9a054 --- /dev/null +++ b/common/retry/retry.go @@ -0,0 +1,64 @@ +package retry // import "github.com/xtls/xray-core/v1/common/retry" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "time" +) + +var ( + ErrRetryFailed = newError("all retry attempts failed") +) + +// Strategy is a way to retry on a specific function. +type Strategy interface { + // On performs a retry on a specific function, until it doesn't return any error. + On(func() error) error +} + +type retryer struct { + totalAttempt int + nextDelay func() uint32 +} + +// On implements Strategy.On. +func (r *retryer) On(method func() error) error { + attempt := 0 + accumulatedError := make([]error, 0, r.totalAttempt) + for attempt < r.totalAttempt { + err := method() + if err == nil { + return nil + } + numErrors := len(accumulatedError) + if numErrors == 0 || err.Error() != accumulatedError[numErrors-1].Error() { + accumulatedError = append(accumulatedError, err) + } + delay := r.nextDelay() + time.Sleep(time.Duration(delay) * time.Millisecond) + attempt++ + } + return newError(accumulatedError).Base(ErrRetryFailed) +} + +// Timed returns a retry strategy with fixed interval. +func Timed(attempts int, delay uint32) Strategy { + return &retryer{ + totalAttempt: attempts, + nextDelay: func() uint32 { + return delay + }, + } +} + +func ExponentialBackoff(attempts int, delay uint32) Strategy { + nextDelay := uint32(0) + return &retryer{ + totalAttempt: attempts, + nextDelay: func() uint32 { + r := nextDelay + nextDelay += delay + return r + }, + } +} diff --git a/common/retry/retry_test.go b/common/retry/retry_test.go new file mode 100644 index 00000000..6f701798 --- /dev/null +++ b/common/retry/retry_test.go @@ -0,0 +1,98 @@ +package retry_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" + . "github.com/xtls/xray-core/v1/common/retry" +) + +var ( + errorTestOnly = errors.New("this is a fake error") +) + +func TestNoRetry(t *testing.T) { + startTime := time.Now().Unix() + err := Timed(10, 100000).On(func() error { + return nil + }) + endTime := time.Now().Unix() + + common.Must(err) + if endTime < startTime { + t.Error("endTime < startTime: ", startTime, " -> ", endTime) + } +} + +func TestRetryOnce(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(10, 1000).On(func() error { + if called == 0 { + called++ + return errorTestOnly + } + return nil + }) + duration := time.Since(startTime) + + common.Must(err) + if v := int64(duration / time.Millisecond); v < 900 { + t.Error("duration: ", v) + } +} + +func TestRetryMultiple(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(10, 1000).On(func() error { + if called < 5 { + called++ + return errorTestOnly + } + return nil + }) + duration := time.Since(startTime) + + common.Must(err) + if v := int64(duration / time.Millisecond); v < 4900 { + t.Error("duration: ", v) + } +} + +func TestRetryExhausted(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(2, 1000).On(func() error { + called++ + return errorTestOnly + }) + duration := time.Since(startTime) + + if errors.Cause(err) != ErrRetryFailed { + t.Error("cause: ", err) + } + + if v := int64(duration / time.Millisecond); v < 1900 { + t.Error("duration: ", v) + } +} + +func TestExponentialBackoff(t *testing.T) { + startTime := time.Now() + called := 0 + err := ExponentialBackoff(10, 100).On(func() error { + called++ + return errorTestOnly + }) + duration := time.Since(startTime) + + if errors.Cause(err) != ErrRetryFailed { + t.Error("cause: ", err) + } + if v := int64(duration / time.Millisecond); v < 4000 { + t.Error("duration: ", v) + } +} diff --git a/common/serial/serial.go b/common/serial/serial.go new file mode 100644 index 00000000..46197dc3 --- /dev/null +++ b/common/serial/serial.go @@ -0,0 +1,29 @@ +package serial + +import ( + "encoding/binary" + "io" +) + +// ReadUint16 reads first two bytes from the reader, and then coverts them to an uint16 value. +func ReadUint16(reader io.Reader) (uint16, error) { + var b [2]byte + if _, err := io.ReadFull(reader, b[:]); err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b[:]), nil +} + +// WriteUint16 writes an uint16 value into writer. +func WriteUint16(writer io.Writer, value uint16) (int, error) { + var b [2]byte + binary.BigEndian.PutUint16(b[:], value) + return writer.Write(b[:]) +} + +// WriteUint64 writes an uint64 value into writer. +func WriteUint64(writer io.Writer, value uint64) (int, error) { + var b [8]byte + binary.BigEndian.PutUint64(b[:], value) + return writer.Write(b[:]) +} diff --git a/common/serial/serial_test.go b/common/serial/serial_test.go new file mode 100644 index 00000000..e47a5370 --- /dev/null +++ b/common/serial/serial_test.go @@ -0,0 +1,88 @@ +package serial_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/serial" +) + +func TestUint16Serial(t *testing.T) { + b := buf.New() + defer b.Release() + + n, err := serial.WriteUint16(b, 10) + common.Must(err) + if n != 2 { + t.Error("expect 2 bytes writtng, but actually ", n) + } + if diff := cmp.Diff(b.Bytes(), []byte{0, 10}); diff != "" { + t.Error(diff) + } +} + +func TestUint64Serial(t *testing.T) { + b := buf.New() + defer b.Release() + + n, err := serial.WriteUint64(b, 10) + common.Must(err) + if n != 8 { + t.Error("expect 8 bytes writtng, but actually ", n) + } + if diff := cmp.Diff(b.Bytes(), []byte{0, 0, 0, 0, 0, 0, 0, 10}); diff != "" { + t.Error(diff) + } +} + +func TestReadUint16(t *testing.T) { + testCases := []struct { + Input []byte + Output uint16 + }{ + { + Input: []byte{0, 1}, + Output: 1, + }, + } + + for _, testCase := range testCases { + v, err := serial.ReadUint16(bytes.NewReader(testCase.Input)) + common.Must(err) + if v != testCase.Output { + t.Error("for input ", testCase.Input, " expect output ", testCase.Output, " but got ", v) + } + } +} + +func BenchmarkReadUint16(b *testing.B) { + reader := buf.New() + defer reader.Release() + + common.Must2(reader.Write([]byte{0, 1})) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := serial.ReadUint16(reader) + common.Must(err) + reader.Clear() + reader.Extend(2) + } +} + +func BenchmarkWriteUint64(b *testing.B) { + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := serial.WriteUint64(writer, 8) + common.Must(err) + writer.Clear() + } +} diff --git a/common/serial/string.go b/common/serial/string.go new file mode 100644 index 00000000..70de87a7 --- /dev/null +++ b/common/serial/string.go @@ -0,0 +1,35 @@ +package serial + +import ( + "fmt" + "strings" +) + +// ToString serialize an arbitrary value into string. +func ToString(v interface{}) string { + if v == nil { + return " " + } + + switch value := v.(type) { + case string: + return value + case *string: + return *value + case fmt.Stringer: + return value.String() + case error: + return value.Error() + default: + return fmt.Sprintf("%+v", value) + } +} + +// Concat concatenates all input into a single string. +func Concat(v ...interface{}) string { + builder := strings.Builder{} + for _, value := range v { + builder.WriteString(ToString(value)) + } + return builder.String() +} diff --git a/common/serial/string_test.go b/common/serial/string_test.go new file mode 100644 index 00000000..6f122b54 --- /dev/null +++ b/common/serial/string_test.go @@ -0,0 +1,59 @@ +package serial_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + + . "github.com/xtls/xray-core/v1/common/serial" +) + +func TestToString(t *testing.T) { + s := "a" + data := []struct { + Value interface{} + String string + }{ + {Value: s, String: s}, + {Value: &s, String: s}, + {Value: errors.New("t"), String: "t"}, + {Value: []byte{'b', 'c'}, String: "[98 99]"}, + } + + for _, c := range data { + if r := cmp.Diff(ToString(c.Value), c.String); r != "" { + t.Error(r) + } + } +} + +func TestConcat(t *testing.T) { + testCases := []struct { + Input []interface{} + Output string + }{ + { + Input: []interface{}{ + "a", "b", + }, + Output: "ab", + }, + } + + for _, testCase := range testCases { + actual := Concat(testCase.Input...) + if actual != testCase.Output { + t.Error("Unexpected output: ", actual, " but want: ", testCase.Output) + } + } +} + +func BenchmarkConcat(b *testing.B) { + input := []interface{}{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"} + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = Concat(input...) + } +} diff --git a/common/serial/typed_message.go b/common/serial/typed_message.go new file mode 100644 index 00000000..e59d1d0d --- /dev/null +++ b/common/serial/typed_message.go @@ -0,0 +1,47 @@ +package serial + +import ( + "errors" + "reflect" + + "github.com/golang/protobuf/proto" +) + +// ToTypedMessage converts a proto Message into TypedMessage. +func ToTypedMessage(message proto.Message) *TypedMessage { + if message == nil { + return nil + } + settings, _ := proto.Marshal(message) + return &TypedMessage{ + Type: GetMessageType(message), + Value: settings, + } +} + +// GetMessageType returns the name of this proto Message. +func GetMessageType(message proto.Message) string { + return proto.MessageName(message) +} + +// GetInstance creates a new instance of the message with messageType. +func GetInstance(messageType string) (interface{}, error) { + mType := proto.MessageType(messageType) + if mType == nil || mType.Elem() == nil { + return nil, errors.New("Serial: Unknown type: " + messageType) + } + return reflect.New(mType.Elem()).Interface(), nil +} + +// GetInstance converts current TypedMessage into a proto Message. +func (v *TypedMessage) GetInstance() (proto.Message, error) { + instance, err := GetInstance(v.Type) + if err != nil { + return nil, err + } + protoMessage := instance.(proto.Message) + if err := proto.Unmarshal(v.Value, protoMessage); err != nil { + return nil, err + } + return protoMessage, nil +} diff --git a/common/serial/typed_message.pb.go b/common/serial/typed_message.pb.go new file mode 100644 index 00000000..5b237911 --- /dev/null +++ b/common/serial/typed_message.pb.go @@ -0,0 +1,166 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: common/serial/typed_message.proto + +package serial + +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 + +// TypedMessage is a serialized proto message along with its type name. +type TypedMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The name of the message type, retrieved from protobuf API. + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Serialized proto message. + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *TypedMessage) Reset() { + *x = TypedMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_common_serial_typed_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TypedMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TypedMessage) ProtoMessage() {} + +func (x *TypedMessage) ProtoReflect() protoreflect.Message { + mi := &file_common_serial_typed_message_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 TypedMessage.ProtoReflect.Descriptor instead. +func (*TypedMessage) Descriptor() ([]byte, []int) { + return file_common_serial_typed_message_proto_rawDescGZIP(), []int{0} +} + +func (x *TypedMessage) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *TypedMessage) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +var File_common_serial_typed_message_proto protoreflect.FileDescriptor + +var file_common_serial_typed_message_proto_rawDesc = []byte{ + 0x0a, 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, 0x12, 0x12, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x38, 0x0a, 0x0c, 0x54, 0x79, 0x70, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x42, 0x5b, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 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, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0xaa, 0x02, 0x12, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_serial_typed_message_proto_rawDescOnce sync.Once + file_common_serial_typed_message_proto_rawDescData = file_common_serial_typed_message_proto_rawDesc +) + +func file_common_serial_typed_message_proto_rawDescGZIP() []byte { + file_common_serial_typed_message_proto_rawDescOnce.Do(func() { + file_common_serial_typed_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_serial_typed_message_proto_rawDescData) + }) + return file_common_serial_typed_message_proto_rawDescData +} + +var file_common_serial_typed_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_serial_typed_message_proto_goTypes = []interface{}{ + (*TypedMessage)(nil), // 0: xray.common.serial.TypedMessage +} +var file_common_serial_typed_message_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_common_serial_typed_message_proto_init() } +func file_common_serial_typed_message_proto_init() { + if File_common_serial_typed_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_serial_typed_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TypedMessage); 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_common_serial_typed_message_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_serial_typed_message_proto_goTypes, + DependencyIndexes: file_common_serial_typed_message_proto_depIdxs, + MessageInfos: file_common_serial_typed_message_proto_msgTypes, + }.Build() + File_common_serial_typed_message_proto = out.File + file_common_serial_typed_message_proto_rawDesc = nil + file_common_serial_typed_message_proto_goTypes = nil + file_common_serial_typed_message_proto_depIdxs = nil +} diff --git a/common/serial/typed_message.proto b/common/serial/typed_message.proto new file mode 100644 index 00000000..25c8d7b7 --- /dev/null +++ b/common/serial/typed_message.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.common.serial; +option csharp_namespace = "Xray.Common.Serial"; +option go_package = "github.com/xtls/xray-core/v1/common/serial"; +option java_package = "com.xray.common.serial"; +option java_multiple_files = true; + +// TypedMessage is a serialized proto message along with its type name. +message TypedMessage { + // The name of the message type, retrieved from protobuf API. + string type = 1; + // Serialized proto message. + bytes value = 2; +} diff --git a/common/serial/typed_message_test.go b/common/serial/typed_message_test.go new file mode 100644 index 00000000..55c52271 --- /dev/null +++ b/common/serial/typed_message_test.go @@ -0,0 +1,24 @@ +package serial_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/serial" +) + +func TestGetInstance(t *testing.T) { + p, err := GetInstance("") + if p != nil { + t.Error("expected nil instance, but got ", p) + } + if err == nil { + t.Error("expect non-nil error, but got nil") + } +} + +func TestConvertingNilMessage(t *testing.T) { + x := ToTypedMessage(nil) + if x != nil { + t.Error("expect nil, but actually not") + } +} diff --git a/common/session/context.go b/common/session/context.go new file mode 100644 index 00000000..7f1d7df9 --- /dev/null +++ b/common/session/context.go @@ -0,0 +1,86 @@ +package session + +import "context" + +type sessionKey int + +const ( + idSessionKey sessionKey = iota + inboundSessionKey + outboundSessionKey + contentSessionKey + muxPreferedSessionKey + sockoptSessionKey +) + +// ContextWithID returns a new context with the given ID. +func ContextWithID(ctx context.Context, id ID) context.Context { + return context.WithValue(ctx, idSessionKey, id) +} + +// IDFromContext returns ID in this context, or 0 if not contained. +func IDFromContext(ctx context.Context) ID { + if id, ok := ctx.Value(idSessionKey).(ID); ok { + return id + } + return 0 +} + +func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context { + return context.WithValue(ctx, inboundSessionKey, inbound) +} + +func InboundFromContext(ctx context.Context) *Inbound { + if inbound, ok := ctx.Value(inboundSessionKey).(*Inbound); ok { + return inbound + } + return nil +} + +func ContextWithOutbound(ctx context.Context, outbound *Outbound) context.Context { + return context.WithValue(ctx, outboundSessionKey, outbound) +} + +func OutboundFromContext(ctx context.Context) *Outbound { + if outbound, ok := ctx.Value(outboundSessionKey).(*Outbound); ok { + return outbound + } + return nil +} + +func ContextWithContent(ctx context.Context, content *Content) context.Context { + return context.WithValue(ctx, contentSessionKey, content) +} + +func ContentFromContext(ctx context.Context) *Content { + if content, ok := ctx.Value(contentSessionKey).(*Content); ok { + return content + } + return nil +} + +// ContextWithMuxPrefered returns a new context with the given bool +func ContextWithMuxPrefered(ctx context.Context, forced bool) context.Context { + return context.WithValue(ctx, muxPreferedSessionKey, forced) +} + +// MuxPreferedFromContext returns value in this context, or false if not contained. +func MuxPreferedFromContext(ctx context.Context) bool { + if val, ok := ctx.Value(muxPreferedSessionKey).(bool); ok { + return val + } + return false +} + +// ContextWithSockopt returns a new context with Socket configs included +func ContextWithSockopt(ctx context.Context, s *Sockopt) context.Context { + return context.WithValue(ctx, sockoptSessionKey, s) +} + +// SockoptFromContext returns Socket configs in this context, or nil if not contained. +func SockoptFromContext(ctx context.Context) *Sockopt { + if sockopt, ok := ctx.Value(sockoptSessionKey).(*Sockopt); ok { + return sockopt + } + return nil +} diff --git a/common/session/session.go b/common/session/session.go new file mode 100644 index 00000000..a0370ddc --- /dev/null +++ b/common/session/session.go @@ -0,0 +1,94 @@ +// Package session provides functions for sessions of incoming requests. +package session // import "github.com/xtls/xray-core/v1/common/session" + +import ( + "context" + "math/rand" + + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" +) + +// ID of a session. +type ID uint32 + +// NewID generates a new ID. The generated ID is high likely to be unique, but not cryptographically secure. +// The generated ID will never be 0. +func NewID() ID { + for { + id := ID(rand.Uint32()) + if id != 0 { + return id + } + } +} + +// ExportIDToError transfers session.ID into an error object, for logging purpose. +// This can be used with error.WriteToLog(). +func ExportIDToError(ctx context.Context) errors.ExportOption { + id := IDFromContext(ctx) + return func(h *errors.ExportOptionHolder) { + h.SessionID = uint32(id) + } +} + +// Inbound is the metadata of an inbound connection. +type Inbound struct { + // Source address of the inbound connection. + Source net.Destination + // Getaway address + Gateway net.Destination + // Tag of the inbound proxy that handles the connection. + Tag string + // User is the user that authencates for the inbound. May be nil if the protocol allows anounymous traffic. + User *protocol.MemoryUser +} + +// Outbound is the metadata of an outbound connection. +type Outbound struct { + // Target address of the outbound connection. + Target net.Destination + // Gateway address + Gateway net.Address +} + +// SniffingRequest controls the behavior of content sniffing. +type SniffingRequest struct { + OverrideDestinationForProtocol []string + Enabled bool +} + +// Content is the metadata of the connection content. +type Content struct { + // Protocol of current content. + Protocol string + + SniffingRequest SniffingRequest + + Attributes map[string]string + + SkipRoutePick bool +} + +// Sockopt is the settings for socket connection. +type Sockopt struct { + // Mark of the socket connection. + Mark int32 +} + +// SetAttribute attachs additional string attributes to content. +func (c *Content) SetAttribute(name string, value string) { + if c.Attributes == nil { + c.Attributes = make(map[string]string) + } + c.Attributes[name] = value +} + +// Attribute retrieves additional string attributes from content. +func (c *Content) Attribute(name string) string { + if c.Attributes == nil { + return "" + } + return c.Attributes[name] +} diff --git a/common/signal/done/done.go b/common/signal/done/done.go new file mode 100644 index 00000000..189a8cf3 --- /dev/null +++ b/common/signal/done/done.go @@ -0,0 +1,49 @@ +package done + +import ( + "sync" +) + +// Instance is a utility for notifications of something being done. +type Instance struct { + access sync.Mutex + c chan struct{} + closed bool +} + +// New returns a new Done. +func New() *Instance { + return &Instance{ + c: make(chan struct{}), + } +} + +// Done returns true if Close() is called. +func (d *Instance) Done() bool { + select { + case <-d.Wait(): + return true + default: + return false + } +} + +// Wait returns a channel for waiting for done. +func (d *Instance) Wait() <-chan struct{} { + return d.c +} + +// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status. +func (d *Instance) Close() error { + d.access.Lock() + defer d.access.Unlock() + + if d.closed { + return nil + } + + d.closed = true + close(d.c) + + return nil +} diff --git a/common/signal/notifier.go b/common/signal/notifier.go new file mode 100644 index 00000000..19836e54 --- /dev/null +++ b/common/signal/notifier.go @@ -0,0 +1,26 @@ +package signal + +// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously. +type Notifier struct { + c chan struct{} +} + +// NewNotifier creates a new Notifier. +func NewNotifier() *Notifier { + return &Notifier{ + c: make(chan struct{}, 1), + } +} + +// Signal signals a change, usually by producer. This method never blocks. +func (n *Notifier) Signal() { + select { + case n.c <- struct{}{}: + default: + } +} + +// Wait returns a channel for waiting for changes. The returned channel never gets closed. +func (n *Notifier) Wait() <-chan struct{} { + return n.c +} diff --git a/common/signal/notifier_test.go b/common/signal/notifier_test.go new file mode 100644 index 00000000..5fb72a36 --- /dev/null +++ b/common/signal/notifier_test.go @@ -0,0 +1,20 @@ +package signal_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/signal" +) + +func TestNotifierSignal(t *testing.T) { + n := NewNotifier() + + w := n.Wait() + n.Signal() + + select { + case <-w: + default: + t.Fail() + } +} diff --git a/common/signal/pubsub/pubsub.go b/common/signal/pubsub/pubsub.go new file mode 100644 index 00000000..51636d99 --- /dev/null +++ b/common/signal/pubsub/pubsub.go @@ -0,0 +1,106 @@ +package pubsub + +import ( + "errors" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/signal/done" + "github.com/xtls/xray-core/v1/common/task" +) + +type Subscriber struct { + buffer chan interface{} + done *done.Instance +} + +func (s *Subscriber) push(msg interface{}) { + select { + case s.buffer <- msg: + default: + } +} + +func (s *Subscriber) Wait() <-chan interface{} { + return s.buffer +} + +func (s *Subscriber) Close() error { + return s.done.Close() +} + +func (s *Subscriber) IsClosed() bool { + return s.done.Done() +} + +type Service struct { + sync.RWMutex + subs map[string][]*Subscriber + ctask *task.Periodic +} + +func NewService() *Service { + s := &Service{ + subs: make(map[string][]*Subscriber), + } + s.ctask = &task.Periodic{ + Execute: s.Cleanup, + Interval: time.Second * 30, + } + return s +} + +// Cleanup cleans up internal caches of subscribers. +// Visible for testing only. +func (s *Service) Cleanup() error { + s.Lock() + defer s.Unlock() + + if len(s.subs) == 0 { + return errors.New("nothing to do") + } + + for name, subs := range s.subs { + newSub := make([]*Subscriber, 0, len(s.subs)) + for _, sub := range subs { + if !sub.IsClosed() { + newSub = append(newSub, sub) + } + } + if len(newSub) == 0 { + delete(s.subs, name) + } else { + s.subs[name] = newSub + } + } + + if len(s.subs) == 0 { + s.subs = make(map[string][]*Subscriber) + } + return nil +} + +func (s *Service) Subscribe(name string) *Subscriber { + sub := &Subscriber{ + buffer: make(chan interface{}, 16), + done: done.New(), + } + s.Lock() + subs := append(s.subs[name], sub) + s.subs[name] = subs + s.Unlock() + common.Must(s.ctask.Start()) + return sub +} + +func (s *Service) Publish(name string, message interface{}) { + s.RLock() + defer s.RUnlock() + + for _, sub := range s.subs[name] { + if !sub.IsClosed() { + sub.push(message) + } + } +} diff --git a/common/signal/pubsub/pubsub_test.go b/common/signal/pubsub/pubsub_test.go new file mode 100644 index 00000000..4ec99bc4 --- /dev/null +++ b/common/signal/pubsub/pubsub_test.go @@ -0,0 +1,34 @@ +package pubsub_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/common/signal/pubsub" +) + +func TestPubsub(t *testing.T) { + service := NewService() + + sub := service.Subscribe("a") + service.Publish("a", 1) + + select { + case v := <-sub.Wait(): + if v != 1 { + t.Error("expected subscribed value 1, but got ", v) + } + default: + t.Fail() + } + + sub.Close() + service.Publish("a", 2) + + select { + case <-sub.Wait(): + t.Fail() + default: + } + + service.Cleanup() +} diff --git a/common/signal/semaphore/semaphore.go b/common/signal/semaphore/semaphore.go new file mode 100644 index 00000000..8696b148 --- /dev/null +++ b/common/signal/semaphore/semaphore.go @@ -0,0 +1,27 @@ +package semaphore + +// Instance is an implementation of semaphore. +type Instance struct { + token chan struct{} +} + +// New create a new Semaphore with n permits. +func New(n int) *Instance { + s := &Instance{ + token: make(chan struct{}, n), + } + for i := 0; i < n; i++ { + s.token <- struct{}{} + } + return s +} + +// Wait returns a channel for acquiring a permit. +func (s *Instance) Wait() <-chan struct{} { + return s.token +} + +// Signal releases a permit into the semaphore. +func (s *Instance) Signal() { + s.token <- struct{}{} +} diff --git a/common/signal/timer.go b/common/signal/timer.go new file mode 100644 index 00000000..b08f133f --- /dev/null +++ b/common/signal/timer.go @@ -0,0 +1,82 @@ +package signal + +import ( + "context" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/task" +) + +type ActivityUpdater interface { + Update() +} + +type ActivityTimer struct { + sync.RWMutex + updated chan struct{} + checkTask *task.Periodic + onTimeout func() +} + +func (t *ActivityTimer) Update() { + select { + case t.updated <- struct{}{}: + default: + } +} + +func (t *ActivityTimer) check() error { + select { + case <-t.updated: + default: + t.finish() + } + return nil +} + +func (t *ActivityTimer) finish() { + t.Lock() + defer t.Unlock() + + if t.onTimeout != nil { + t.onTimeout() + t.onTimeout = nil + } + if t.checkTask != nil { + t.checkTask.Close() + t.checkTask = nil + } +} + +func (t *ActivityTimer) SetTimeout(timeout time.Duration) { + if timeout == 0 { + t.finish() + return + } + + checkTask := &task.Periodic{ + Interval: timeout, + Execute: t.check, + } + + t.Lock() + + if t.checkTask != nil { + t.checkTask.Close() + } + t.checkTask = checkTask + t.Unlock() + t.Update() + common.Must(checkTask.Start()) +} + +func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer { + timer := &ActivityTimer{ + updated: make(chan struct{}, 1), + onTimeout: cancel, + } + timer.SetTimeout(timeout) + return timer +} diff --git a/common/signal/timer_test.go b/common/signal/timer_test.go new file mode 100644 index 00000000..6987db11 --- /dev/null +++ b/common/signal/timer_test.go @@ -0,0 +1,60 @@ +package signal_test + +import ( + "context" + "runtime" + "testing" + "time" + + . "github.com/xtls/xray-core/v1/common/signal" +) + +func TestActivityTimer(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, time.Second*4) + time.Sleep(time.Second * 6) + if ctx.Err() == nil { + t.Error("expected some error, but got nil") + } + runtime.KeepAlive(timer) +} + +func TestActivityTimerUpdate(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, time.Second*10) + time.Sleep(time.Second * 3) + if ctx.Err() != nil { + t.Error("expected nil, but got ", ctx.Err().Error()) + } + timer.SetTimeout(time.Second * 1) + time.Sleep(time.Second * 2) + if ctx.Err() == nil { + t.Error("expcted some error, but got nil") + } + runtime.KeepAlive(timer) +} + +func TestActivityTimerNonBlocking(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, 0) + time.Sleep(time.Second * 1) + select { + case <-ctx.Done(): + default: + t.Error("context not done") + } + timer.SetTimeout(0) + timer.SetTimeout(1) + timer.SetTimeout(2) +} + +func TestActivityTimerZeroTimeout(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, 0) + select { + case <-ctx.Done(): + default: + t.Error("context not done") + } + runtime.KeepAlive(timer) +} diff --git a/common/strmatcher/benchmark_test.go b/common/strmatcher/benchmark_test.go new file mode 100644 index 00000000..db365e50 --- /dev/null +++ b/common/strmatcher/benchmark_test.go @@ -0,0 +1,49 @@ +package strmatcher_test + +import ( + "strconv" + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/strmatcher" +) + +func BenchmarkDomainMatcherGroup(b *testing.B) { + g := new(DomainMatcherGroup) + + for i := 1; i <= 1024; i++ { + g.Add(strconv.Itoa(i)+".example.com", uint32(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} + +func BenchmarkFullMatcherGroup(b *testing.B) { + g := new(FullMatcherGroup) + + for i := 1; i <= 1024; i++ { + g.Add(strconv.Itoa(i)+".example.com", uint32(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} + +func BenchmarkMarchGroup(b *testing.B) { + g := new(MatcherGroup) + for i := 1; i <= 1024; i++ { + m, err := Domain.New(strconv.Itoa(i) + ".example.com") + common.Must(err) + g.Add(m) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} diff --git a/common/strmatcher/domain_matcher.go b/common/strmatcher/domain_matcher.go new file mode 100644 index 00000000..ae8e65bc --- /dev/null +++ b/common/strmatcher/domain_matcher.go @@ -0,0 +1,98 @@ +package strmatcher + +import "strings" + +func breakDomain(domain string) []string { + return strings.Split(domain, ".") +} + +type node struct { + values []uint32 + sub map[string]*node +} + +// DomainMatcherGroup is a IndexMatcher for a large set of Domain matchers. +// Visible for testing only. +type DomainMatcherGroup struct { + root *node +} + +func (g *DomainMatcherGroup) Add(domain string, value uint32) { + if g.root == nil { + g.root = new(node) + } + + current := g.root + parts := breakDomain(domain) + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + if current.sub == nil { + current.sub = make(map[string]*node) + } + next := current.sub[part] + if next == nil { + next = new(node) + current.sub[part] = next + } + current = next + } + + current.values = append(current.values, value) +} + +func (g *DomainMatcherGroup) addMatcher(m domainMatcher, value uint32) { + g.Add(string(m), value) +} + +func (g *DomainMatcherGroup) Match(domain string) []uint32 { + if domain == "" { + return nil + } + + current := g.root + if current == nil { + return nil + } + + nextPart := func(idx int) int { + for i := idx - 1; i >= 0; i-- { + if domain[i] == '.' { + return i + } + } + return -1 + } + + matches := [][]uint32{} + idx := len(domain) + for { + if idx == -1 || current.sub == nil { + break + } + + nidx := nextPart(idx) + part := domain[nidx+1 : idx] + next := current.sub[part] + if next == nil { + break + } + current = next + idx = nidx + if len(current.values) > 0 { + matches = append(matches, current.values) + } + } + switch len(matches) { + case 0: + return nil + case 1: + return matches[0] + default: + result := []uint32{} + for idx := range matches { + // Insert reversely, the subdomain that matches further ranks higher + result = append(result, matches[len(matches)-1-idx]...) + } + return result + } +} diff --git a/common/strmatcher/domain_matcher_test.go b/common/strmatcher/domain_matcher_test.go new file mode 100644 index 00000000..b67a6d23 --- /dev/null +++ b/common/strmatcher/domain_matcher_test.go @@ -0,0 +1,76 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + . "github.com/xtls/xray-core/v1/common/strmatcher" +) + +func TestDomainMatcherGroup(t *testing.T) { + g := new(DomainMatcherGroup) + g.Add("example.com", 1) + g.Add("google.com", 2) + g.Add("x.a.com", 3) + g.Add("a.b.com", 4) + g.Add("c.a.b.com", 5) + g.Add("x.y.com", 4) + g.Add("x.y.com", 6) + + testCases := []struct { + Domain string + Result []uint32 + }{ + { + Domain: "x.example.com", + Result: []uint32{1}, + }, + { + Domain: "y.com", + Result: nil, + }, + { + Domain: "a.b.com", + Result: []uint32{4}, + }, + { // Matches [c.a.b.com, a.b.com] + Domain: "c.a.b.com", + Result: []uint32{5, 4}, + }, + { + Domain: "c.a..b.com", + Result: nil, + }, + { + Domain: ".com", + Result: nil, + }, + { + Domain: "com", + Result: nil, + }, + { + Domain: "", + Result: nil, + }, + { + Domain: "x.y.com", + Result: []uint32{4, 6}, + }, + } + + for _, testCase := range testCases { + r := g.Match(testCase.Domain) + if !reflect.DeepEqual(r, testCase.Result) { + t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r) + } + } +} + +func TestEmptyDomainMatcherGroup(t *testing.T) { + g := new(DomainMatcherGroup) + r := g.Match("example.com") + if len(r) != 0 { + t.Error("Expect [], but ", r) + } +} diff --git a/common/strmatcher/full_matcher.go b/common/strmatcher/full_matcher.go new file mode 100644 index 00000000..e00d02aa --- /dev/null +++ b/common/strmatcher/full_matcher.go @@ -0,0 +1,25 @@ +package strmatcher + +type FullMatcherGroup struct { + matchers map[string][]uint32 +} + +func (g *FullMatcherGroup) Add(domain string, value uint32) { + if g.matchers == nil { + g.matchers = make(map[string][]uint32) + } + + g.matchers[domain] = append(g.matchers[domain], value) +} + +func (g *FullMatcherGroup) addMatcher(m fullMatcher, value uint32) { + g.Add(string(m), value) +} + +func (g *FullMatcherGroup) Match(str string) []uint32 { + if g.matchers == nil { + return nil + } + + return g.matchers[str] +} diff --git a/common/strmatcher/full_matcher_test.go b/common/strmatcher/full_matcher_test.go new file mode 100644 index 00000000..51990e42 --- /dev/null +++ b/common/strmatcher/full_matcher_test.go @@ -0,0 +1,50 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + . "github.com/xtls/xray-core/v1/common/strmatcher" +) + +func TestFullMatcherGroup(t *testing.T) { + g := new(FullMatcherGroup) + g.Add("example.com", 1) + g.Add("google.com", 2) + g.Add("x.a.com", 3) + g.Add("x.y.com", 4) + g.Add("x.y.com", 6) + + testCases := []struct { + Domain string + Result []uint32 + }{ + { + Domain: "example.com", + Result: []uint32{1}, + }, + { + Domain: "y.com", + Result: nil, + }, + { + Domain: "x.y.com", + Result: []uint32{4, 6}, + }, + } + + for _, testCase := range testCases { + r := g.Match(testCase.Domain) + if !reflect.DeepEqual(r, testCase.Result) { + t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r) + } + } +} + +func TestEmptyFullMatcherGroup(t *testing.T) { + g := new(FullMatcherGroup) + r := g.Match("example.com") + if len(r) != 0 { + t.Error("Expect [], but ", r) + } +} diff --git a/common/strmatcher/matchers.go b/common/strmatcher/matchers.go new file mode 100644 index 00000000..b5ab09c4 --- /dev/null +++ b/common/strmatcher/matchers.go @@ -0,0 +1,52 @@ +package strmatcher + +import ( + "regexp" + "strings" +) + +type fullMatcher string + +func (m fullMatcher) Match(s string) bool { + return string(m) == s +} + +func (m fullMatcher) String() string { + return "full:" + string(m) +} + +type substrMatcher string + +func (m substrMatcher) Match(s string) bool { + return strings.Contains(s, string(m)) +} + +func (m substrMatcher) String() string { + return "keyword:" + string(m) +} + +type domainMatcher string + +func (m domainMatcher) Match(s string) bool { + pattern := string(m) + if !strings.HasSuffix(s, pattern) { + return false + } + return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.' +} + +func (m domainMatcher) String() string { + return "domain:" + string(m) +} + +type regexMatcher struct { + pattern *regexp.Regexp +} + +func (m *regexMatcher) Match(s string) bool { + return m.pattern.MatchString(s) +} + +func (m *regexMatcher) String() string { + return "regexp:" + m.pattern.String() +} diff --git a/common/strmatcher/matchers_test.go b/common/strmatcher/matchers_test.go new file mode 100644 index 00000000..f9020153 --- /dev/null +++ b/common/strmatcher/matchers_test.go @@ -0,0 +1,73 @@ +package strmatcher_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/strmatcher" +) + +func TestMatcher(t *testing.T) { + cases := []struct { + pattern string + mType Type + input string + output bool + }{ + { + pattern: "example.com", + mType: Domain, + input: "www.example.com", + output: true, + }, + { + pattern: "example.com", + mType: Domain, + input: "example.com", + output: true, + }, + { + pattern: "example.com", + mType: Domain, + input: "www.fxample.com", + output: false, + }, + { + pattern: "example.com", + mType: Domain, + input: "xample.com", + output: false, + }, + { + pattern: "example.com", + mType: Domain, + input: "xexample.com", + output: false, + }, + { + pattern: "example.com", + mType: Full, + input: "example.com", + output: true, + }, + { + pattern: "example.com", + mType: Full, + input: "xexample.com", + output: false, + }, + { + pattern: "example.com", + mType: Regex, + input: "examplexcom", + output: true, + }, + } + for _, test := range cases { + matcher, err := test.mType.New(test.pattern) + common.Must(err) + if m := matcher.Match(test.input); m != test.output { + t.Error("unexpected output: ", m, " for test case ", test) + } + } +} diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go new file mode 100644 index 00000000..9728047d --- /dev/null +++ b/common/strmatcher/strmatcher.go @@ -0,0 +1,106 @@ +package strmatcher + +import ( + "regexp" +) + +// Matcher is the interface to determine a string matches a pattern. +type Matcher interface { + // Match returns true if the given string matches a predefined pattern. + Match(string) bool + String() string +} + +// Type is the type of the matcher. +type Type byte + +const ( + // Full is the type of matcher that the input string must exactly equal to the pattern. + Full Type = iota + // Substr is the type of matcher that the input string must contain the pattern as a sub-string. + Substr + // Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern. + Domain + // Regex is the type of matcher that the input string must matches the regular-expression pattern. + Regex +) + +// New creates a new Matcher based on the given pattern. +func (t Type) New(pattern string) (Matcher, error) { + switch t { + case Full: + return fullMatcher(pattern), nil + case Substr: + return substrMatcher(pattern), nil + case Domain: + return domainMatcher(pattern), nil + case Regex: + r, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + return ®exMatcher{ + pattern: r, + }, nil + default: + panic("Unknown type") + } +} + +// IndexMatcher is the interface for matching with a group of matchers. +type IndexMatcher interface { + // Match returns the index of a matcher that matches the input. It returns empty array if no such matcher exists. + Match(input string) []uint32 +} + +type matcherEntry struct { + m Matcher + id uint32 +} + +// MatcherGroup is an implementation of IndexMatcher. +// Empty initialization works. +type MatcherGroup struct { + count uint32 + fullMatcher FullMatcherGroup + domainMatcher DomainMatcherGroup + otherMatchers []matcherEntry +} + +// Add adds a new Matcher into the MatcherGroup, and returns its index. The index will never be 0. +func (g *MatcherGroup) Add(m Matcher) uint32 { + g.count++ + c := g.count + + switch tm := m.(type) { + case fullMatcher: + g.fullMatcher.addMatcher(tm, c) + case domainMatcher: + g.domainMatcher.addMatcher(tm, c) + default: + g.otherMatchers = append(g.otherMatchers, matcherEntry{ + m: m, + id: c, + }) + } + + return c +} + +// Match implements IndexMatcher.Match. +func (g *MatcherGroup) Match(pattern string) []uint32 { + result := []uint32{} + result = append(result, g.fullMatcher.Match(pattern)...) + result = append(result, g.domainMatcher.Match(pattern)...) + for _, e := range g.otherMatchers { + if e.m.Match(pattern) { + result = append(result, e.id) + } + } + return result +} + +// Size returns the number of matchers in the MatcherGroup. +func (g *MatcherGroup) Size() uint32 { + return g.count +} diff --git a/common/strmatcher/strmatcher_test.go b/common/strmatcher/strmatcher_test.go new file mode 100644 index 00000000..1d795b8f --- /dev/null +++ b/common/strmatcher/strmatcher_test.go @@ -0,0 +1,93 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/strmatcher" +) + +func TestMatcherGroup(t *testing.T) { + rules := []struct { + Type Type + Domain string + }{ + { + Type: Regex, + Domain: "apis\\.us$", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Domain, + Domain: "com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Full, + Domain: "fonts.googleapis.com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Domain, + Domain: "example.com", + }, + } + cases := []struct { + Input string + Output []uint32 + }{ + { + Input: "www.baidu.com", + Output: []uint32{5, 9, 4}, + }, + { + Input: "fonts.googleapis.com", + Output: []uint32{8, 3, 7, 4, 2, 6}, + }, + { + Input: "example.googleapis.com", + Output: []uint32{3, 7, 4, 2, 6}, + }, + { + Input: "testapis.us", + Output: []uint32{1, 2, 6}, + }, + { + Input: "example.com", + Output: []uint32{10, 4}, + }, + } + matcherGroup := &MatcherGroup{} + for _, rule := range rules { + matcher, err := rule.Type.New(rule.Domain) + common.Must(err) + matcherGroup.Add(matcher) + } + for _, test := range cases { + if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) { + t.Error("unexpected output: ", m, " for test case ", test) + } + } +} diff --git a/common/task/common.go b/common/task/common.go new file mode 100644 index 00000000..99bc4dc0 --- /dev/null +++ b/common/task/common.go @@ -0,0 +1,10 @@ +package task + +import "github.com/xtls/xray-core/v1/common" + +// Close returns a func() that closes v. +func Close(v interface{}) func() error { + return func() error { + return common.Close(v) + } +} diff --git a/common/task/periodic.go b/common/task/periodic.go new file mode 100644 index 00000000..6abe41ae --- /dev/null +++ b/common/task/periodic.go @@ -0,0 +1,85 @@ +package task + +import ( + "sync" + "time" +) + +// Periodic is a task that runs periodically. +type Periodic struct { + // Interval of the task being run + Interval time.Duration + // Execute is the task function + Execute func() error + + access sync.Mutex + timer *time.Timer + running bool +} + +func (t *Periodic) hasClosed() bool { + t.access.Lock() + defer t.access.Unlock() + + return !t.running +} + +func (t *Periodic) checkedExecute() error { + if t.hasClosed() { + return nil + } + + if err := t.Execute(); err != nil { + t.access.Lock() + t.running = false + t.access.Unlock() + return err + } + + t.access.Lock() + defer t.access.Unlock() + + if !t.running { + return nil + } + + t.timer = time.AfterFunc(t.Interval, func() { + t.checkedExecute() + }) + + return nil +} + +// Start implements common.Runnable. +func (t *Periodic) Start() error { + t.access.Lock() + if t.running { + t.access.Unlock() + return nil + } + t.running = true + t.access.Unlock() + + if err := t.checkedExecute(); err != nil { + t.access.Lock() + t.running = false + t.access.Unlock() + return err + } + + return nil +} + +// Close implements common.Closable. +func (t *Periodic) Close() error { + t.access.Lock() + defer t.access.Unlock() + + t.running = false + if t.timer != nil { + t.timer.Stop() + t.timer = nil + } + + return nil +} diff --git a/common/task/periodic_test.go b/common/task/periodic_test.go new file mode 100644 index 00000000..d6a0c660 --- /dev/null +++ b/common/task/periodic_test.go @@ -0,0 +1,36 @@ +package task_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/task" +) + +func TestPeriodicTaskStop(t *testing.T) { + value := 0 + task := &Periodic{ + Interval: time.Second * 2, + Execute: func() error { + value++ + return nil + }, + } + common.Must(task.Start()) + time.Sleep(time.Second * 5) + common.Must(task.Close()) + if value != 3 { + t.Fatal("expected 3, but got ", value) + } + time.Sleep(time.Second * 4) + if value != 3 { + t.Fatal("expected 3, but got ", value) + } + common.Must(task.Start()) + time.Sleep(time.Second * 3) + if value != 5 { + t.Fatal("Expected 5, but ", value) + } + common.Must(task.Close()) +} diff --git a/common/task/task.go b/common/task/task.go new file mode 100644 index 00000000..2b64babf --- /dev/null +++ b/common/task/task.go @@ -0,0 +1,52 @@ +package task + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/signal/semaphore" +) + +// OnSuccess executes g() after f() returns nil. +func OnSuccess(f func() error, g func() error) func() error { + return func() error { + if err := f(); err != nil { + return err + } + return g() + } +} + +// Run executes a list of tasks in parallel, returns the first error encountered or nil if all tasks pass. +func Run(ctx context.Context, tasks ...func() error) error { + n := len(tasks) + s := semaphore.New(n) + done := make(chan error, 1) + + for _, task := range tasks { + <-s.Wait() + go func(f func() error) { + err := f() + if err == nil { + s.Signal() + return + } + + select { + case done <- err: + default: + } + }(task) + } + + for i := 0; i < n; i++ { + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + case <-s.Wait(): + } + } + + return nil +} diff --git a/common/task/task_test.go b/common/task/task_test.go new file mode 100644 index 00000000..6ffe4d61 --- /dev/null +++ b/common/task/task_test.go @@ -0,0 +1,66 @@ +package task_test + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/task" +) + +func TestExecuteParallel(t *testing.T) { + err := Run(context.Background(), + func() error { + time.Sleep(time.Millisecond * 200) + return errors.New("test") + }, func() error { + time.Sleep(time.Millisecond * 500) + return errors.New("test2") + }) + + if r := cmp.Diff(err.Error(), "test"); r != "" { + t.Error(r) + } +} + +func TestExecuteParallelContextCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + err := Run(ctx, func() error { + time.Sleep(time.Millisecond * 2000) + return errors.New("test") + }, func() error { + time.Sleep(time.Millisecond * 5000) + return errors.New("test2") + }, func() error { + cancel() + return nil + }) + + errStr := err.Error() + if !strings.Contains(errStr, "canceled") { + t.Error("expected error string to contain 'canceled', but actually not: ", errStr) + } +} + +func BenchmarkExecuteOne(b *testing.B) { + noop := func() error { + return nil + } + for i := 0; i < b.N; i++ { + common.Must(Run(context.Background(), noop)) + } +} + +func BenchmarkExecuteTwo(b *testing.B) { + noop := func() error { + return nil + } + for i := 0; i < b.N; i++ { + common.Must(Run(context.Background(), noop, noop)) + } +} diff --git a/common/type.go b/common/type.go new file mode 100644 index 00000000..2cda62e9 --- /dev/null +++ b/common/type.go @@ -0,0 +1,33 @@ +package common + +import ( + "context" + "reflect" +) + +// ConfigCreator is a function to create an object by a config. +type ConfigCreator func(ctx context.Context, config interface{}) (interface{}, error) + +var ( + typeCreatorRegistry = make(map[reflect.Type]ConfigCreator) +) + +// RegisterConfig registers a global config creator. The config can be nil but must have a type. +func RegisterConfig(config interface{}, configCreator ConfigCreator) error { + configType := reflect.TypeOf(config) + if _, found := typeCreatorRegistry[configType]; found { + return newError(configType.Name() + " is already registered").AtError() + } + typeCreatorRegistry[configType] = configCreator + return nil +} + +// CreateObject creates an object by its config. The config type must be registered through RegisterConfig(). +func CreateObject(ctx context.Context, config interface{}) (interface{}, error) { + configType := reflect.TypeOf(config) + creator, found := typeCreatorRegistry[configType] + if !found { + return nil, newError(configType.String() + " is not registered").AtError() + } + return creator(ctx, config) +} diff --git a/common/type_test.go b/common/type_test.go new file mode 100644 index 00000000..46f9e34a --- /dev/null +++ b/common/type_test.go @@ -0,0 +1,41 @@ +package common_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/v1/common" +) + +type TConfig struct { + value int +} + +type YConfig struct { + value string +} + +func TestObjectCreation(t *testing.T) { + var f = func(ctx context.Context, t interface{}) (interface{}, error) { + return func() int { + return t.(*TConfig).value + }, nil + } + + Must(RegisterConfig((*TConfig)(nil), f)) + err := RegisterConfig((*TConfig)(nil), f) + if err == nil { + t.Error("expect non-nil error, but got nil") + } + + g, err := CreateObject(context.Background(), &TConfig{value: 2}) + Must(err) + if v := g.(func() int)(); v != 2 { + t.Error("expect return value 2, but got ", v) + } + + _, err = CreateObject(context.Background(), &YConfig{value: "T"}) + if err == nil { + t.Error("expect non-nil error, but got nil") + } +} diff --git a/common/uuid/uuid.go b/common/uuid/uuid.go new file mode 100644 index 00000000..3adb2260 --- /dev/null +++ b/common/uuid/uuid.go @@ -0,0 +1,90 @@ +package uuid // import "github.com/xtls/xray-core/v1/common/uuid" + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" +) + +var ( + byteGroups = []int{8, 4, 4, 4, 12} +) + +type UUID [16]byte + +// String returns the string representation of this UUID. +func (u *UUID) String() string { + bytes := u.Bytes() + result := hex.EncodeToString(bytes[0 : byteGroups[0]/2]) + start := byteGroups[0] / 2 + for i := 1; i < len(byteGroups); i++ { + nBytes := byteGroups[i] / 2 + result += "-" + result += hex.EncodeToString(bytes[start : start+nBytes]) + start += nBytes + } + return result +} + +// Bytes returns the bytes representation of this UUID. +func (u *UUID) Bytes() []byte { + return u[:] +} + +// Equals returns true if this UUID equals another UUID by value. +func (u *UUID) Equals(another *UUID) bool { + if u == nil && another == nil { + return true + } + if u == nil || another == nil { + return false + } + return bytes.Equal(u.Bytes(), another.Bytes()) +} + +// New creates a UUID with random value. +func New() UUID { + var uuid UUID + common.Must2(rand.Read(uuid.Bytes())) + return uuid +} + +// ParseBytes converts a UUID in byte form to object. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + if len(b) != 16 { + return uuid, errors.New("invalid UUID: ", b) + } + copy(uuid[:], b) + return uuid, nil +} + +// ParseString converts a UUID in string form to object. +func ParseString(str string) (UUID, error) { + var uuid UUID + + text := []byte(str) + if len(text) < 32 { + return uuid, errors.New("invalid UUID: ", str) + } + + b := uuid.Bytes() + + for _, byteGroup := range byteGroups { + if text[0] == '-' { + text = text[1:] + } + + if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil { + return uuid, err + } + + text = text[byteGroup:] + b = b[byteGroup/2:] + } + + return uuid, nil +} diff --git a/common/uuid/uuid_test.go b/common/uuid/uuid_test.go new file mode 100644 index 00000000..ba551819 --- /dev/null +++ b/common/uuid/uuid_test.go @@ -0,0 +1,82 @@ +package uuid_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/common/uuid" +) + +func TestParseBytes(t *testing.T) { + str := "2418d087-648d-4990-86e8-19dca1d006d3" + bytes := []byte{0x24, 0x18, 0xd0, 0x87, 0x64, 0x8d, 0x49, 0x90, 0x86, 0xe8, 0x19, 0xdc, 0xa1, 0xd0, 0x06, 0xd3} + + uuid, err := ParseBytes(bytes) + common.Must(err) + if diff := cmp.Diff(uuid.String(), str); diff != "" { + t.Error(diff) + } + + _, err = ParseBytes([]byte{1, 3, 2, 4}) + if err == nil { + t.Fatal("Expect error but nil") + } +} + +func TestParseString(t *testing.T) { + str := "2418d087-648d-4990-86e8-19dca1d006d3" + expectedBytes := []byte{0x24, 0x18, 0xd0, 0x87, 0x64, 0x8d, 0x49, 0x90, 0x86, 0xe8, 0x19, 0xdc, 0xa1, 0xd0, 0x06, 0xd3} + + uuid, err := ParseString(str) + common.Must(err) + if r := cmp.Diff(expectedBytes, uuid.Bytes()); r != "" { + t.Fatal(r) + } + + _, err = ParseString("2418d087") + if err == nil { + t.Fatal("Expect error but nil") + } + + _, err = ParseString("2418d087-648k-4990-86e8-19dca1d006d3") + if err == nil { + t.Fatal("Expect error but nil") + } +} + +func TestNewUUID(t *testing.T) { + uuid := New() + uuid2, err := ParseString(uuid.String()) + + common.Must(err) + if uuid.String() != uuid2.String() { + t.Error("uuid string: ", uuid.String(), " != ", uuid2.String()) + } + if r := cmp.Diff(uuid.Bytes(), uuid2.Bytes()); r != "" { + t.Error(r) + } +} + +func TestRandom(t *testing.T) { + uuid := New() + uuid2 := New() + + if uuid.String() == uuid2.String() { + t.Error("duplicated uuid") + } +} + +func TestEquals(t *testing.T) { + var uuid *UUID + var uuid2 *UUID + if !uuid.Equals(uuid2) { + t.Error("empty uuid should equal") + } + + uuid3 := New() + if uuid.Equals(&uuid3) { + t.Error("nil uuid equals non-nil uuid") + } +} diff --git a/core/annotations.go b/core/annotations.go new file mode 100644 index 00000000..41fc3fbd --- /dev/null +++ b/core/annotations.go @@ -0,0 +1,14 @@ +package core + +// Annotation is a concept in Xray. This struct is only for documentation. It is not used anywhere. +// Annotations begin with "xray:" in comment, as metadata of functions or types. +type Annotation struct { + // API is for types or functions that can be used in other libs. Possible values are: + // + // * xray:api:beta for types or functions that are ready for use, but maybe changed in the future. + // * xray:api:stable for types or functions with guarantee of backward compatibility. + // * xray:api:deprecated for types or functions that should not be used anymore. + // + // Types or functions without api annotation should not be used externally. + API string +} diff --git a/core/config.go b/core/config.go new file mode 100644 index 00000000..b773eaac --- /dev/null +++ b/core/config.go @@ -0,0 +1,107 @@ +// +build !confonly + +package core + +import ( + "io" + "strings" + + "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/cmdarg" + "github.com/xtls/xray-core/v1/main/confloader" +) + +// ConfigFormat is a configurable format of Xray config file. +type ConfigFormat struct { + Name string + Extension []string + Loader ConfigLoader +} + +// ConfigLoader is a utility to load Xray config from external source. +type ConfigLoader func(input interface{}) (*Config, error) + +var ( + configLoaderByName = make(map[string]*ConfigFormat) + configLoaderByExt = make(map[string]*ConfigFormat) +) + +// RegisterConfigLoader add a new ConfigLoader. +func RegisterConfigLoader(format *ConfigFormat) error { + name := strings.ToLower(format.Name) + if _, found := configLoaderByName[name]; found { + return newError(format.Name, " already registered.") + } + configLoaderByName[name] = format + + for _, ext := range format.Extension { + lext := strings.ToLower(ext) + if f, found := configLoaderByExt[lext]; found { + return newError(ext, " already registered to ", f.Name) + } + configLoaderByExt[lext] = format + } + + return nil +} + +func getExtension(filename string) string { + idx := strings.LastIndexByte(filename, '.') + if idx == -1 { + return "" + } + return filename[idx+1:] +} + +// LoadConfig loads config with given format from given source. +// input accepts 2 different types: +// * []string slice of multiple filename/url(s) to open to read +// * io.Reader that reads a config content (the original way) +func LoadConfig(formatName string, filename string, input interface{}) (*Config, error) { + ext := getExtension(filename) + if len(ext) > 0 { + if f, found := configLoaderByExt[ext]; found { + return f.Loader(input) + } + } + + if f, found := configLoaderByName[formatName]; found { + return f.Loader(input) + } + + return nil, newError("Unable to load config in ", formatName).AtWarning() +} + +func loadProtobufConfig(data []byte) (*Config, error) { + config := new(Config) + if err := proto.Unmarshal(data, config); err != nil { + return nil, err + } + return config, nil +} + +func init() { + common.Must(RegisterConfigLoader(&ConfigFormat{ + Name: "Protobuf", + Extension: []string{"pb"}, + Loader: func(input interface{}) (*Config, error) { + switch v := input.(type) { + case cmdarg.Arg: + r, err := confloader.LoadConfig(v[0]) + common.Must(err) + data, err := buf.ReadAllToBytes(r) + common.Must(err) + return loadProtobufConfig(data) + case io.Reader: + data, err := buf.ReadAllToBytes(v) + common.Must(err) + return loadProtobufConfig(data) + default: + return nil, newError("unknow type") + } + }, + })) +} diff --git a/core/config.pb.go b/core/config.pb.go new file mode 100644 index 00000000..8b36d696 --- /dev/null +++ b/core/config.pb.go @@ -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: core/config.proto + +package core + +import ( + proto "github.com/golang/protobuf/proto" + serial "github.com/xtls/xray-core/v1/common/serial" + transport "github.com/xtls/xray-core/v1/transport" + 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 master config of Xray. Xray takes this config as input and +// functions accordingly. +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Inbound handler configurations. Must have at least one item. + Inbound []*InboundHandlerConfig `protobuf:"bytes,1,rep,name=inbound,proto3" json:"inbound,omitempty"` + // Outbound handler configurations. Must have at least one item. The first + // item is used as default for routing. + Outbound []*OutboundHandlerConfig `protobuf:"bytes,2,rep,name=outbound,proto3" json:"outbound,omitempty"` + // App is for configurations of all features in Xray. A feature must + // implement the Feature interface, and its config type must be registered + // through common.RegisterConfig. + App []*serial.TypedMessage `protobuf:"bytes,4,rep,name=app,proto3" json:"app,omitempty"` + // Transport settings. + // Deprecated. Each inbound and outbound should choose their own transport + // config. Date to remove: 2020-01-13 + // + // Deprecated: Do not use. + Transport *transport.Config `protobuf:"bytes,5,opt,name=transport,proto3" json:"transport,omitempty"` + // Configuration for extensions. The config may not work if corresponding + // extension is not loaded into Xray. Xray will ignore such config during + // initialization. + Extension []*serial.TypedMessage `protobuf:"bytes,6,rep,name=extension,proto3" json:"extension,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_core_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_core_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_core_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetInbound() []*InboundHandlerConfig { + if x != nil { + return x.Inbound + } + return nil +} + +func (x *Config) GetOutbound() []*OutboundHandlerConfig { + if x != nil { + return x.Outbound + } + return nil +} + +func (x *Config) GetApp() []*serial.TypedMessage { + if x != nil { + return x.App + } + return nil +} + +// Deprecated: Do not use. +func (x *Config) GetTransport() *transport.Config { + if x != nil { + return x.Transport + } + return nil +} + +func (x *Config) GetExtension() []*serial.TypedMessage { + if x != nil { + return x.Extension + } + return nil +} + +// InboundHandlerConfig is the configuration for inbound handler. +type InboundHandlerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Tag of the inbound handler. The tag must be unique among all inbound + // handlers + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + // Settings for how this inbound proxy is handled. + ReceiverSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=receiver_settings,json=receiverSettings,proto3" json:"receiver_settings,omitempty"` + // Settings for inbound proxy. Must be one of the inbound proxies. + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` +} + +func (x *InboundHandlerConfig) Reset() { + *x = InboundHandlerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_core_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundHandlerConfig) ProtoMessage() {} + +func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_core_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 InboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*InboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_core_config_proto_rawDescGZIP(), []int{1} +} + +func (x *InboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *InboundHandlerConfig) GetReceiverSettings() *serial.TypedMessage { + if x != nil { + return x.ReceiverSettings + } + return nil +} + +func (x *InboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +// OutboundHandlerConfig is the configuration for outbound handler. +type OutboundHandlerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Tag of this outbound handler. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + // Settings for how to dial connection for this outbound handler. + SenderSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=sender_settings,json=senderSettings,proto3" json:"sender_settings,omitempty"` + // Settings for this outbound proxy. Must be one of the outbound proxies. + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + // If not zero, this outbound will be expired in seconds. Not used for now. + Expire int64 `protobuf:"varint,4,opt,name=expire,proto3" json:"expire,omitempty"` + // Comment of this outbound handler. Not used for now. + Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` +} + +func (x *OutboundHandlerConfig) Reset() { + *x = OutboundHandlerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_core_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OutboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundHandlerConfig) ProtoMessage() {} + +func (x *OutboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_core_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 OutboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*OutboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_core_config_proto_rawDescGZIP(), []int{2} +} + +func (x *OutboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *OutboundHandlerConfig) GetSenderSettings() *serial.TypedMessage { + if x != nil { + return x.SenderSettings + } + return nil +} + +func (x *OutboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +func (x *OutboundHandlerConfig) GetExpire() int64 { + if x != nil { + return x.Expire + } + return 0 +} + +func (x *OutboundHandlerConfig) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +var File_core_config_proto protoreflect.FileDescriptor + +var file_core_config_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x1a, 0x16, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x02, 0x0a, 0x06, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, + 0x3c, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x32, 0x0a, + 0x03, 0x61, 0x70, 0x70, 0x18, 0x04, 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, 0x03, 0x61, 0x70, + 0x70, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x02, 0x18, 0x01, + 0x52, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x09, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 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, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x03, 0x10, + 0x04, 0x22, 0xc0, 0x01, 0x0a, 0x14, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 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, 0x4d, 0x0a, 0x11, + 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x18, 0x02, 0x20, 0x01, 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, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, 0x0a, 0x0e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, + 0x01, 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, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x22, 0xef, 0x01, 0x0a, 0x15, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 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, 0x49, 0x0a, 0x0f, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 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, 0x0e, 0x73, 0x65, 0x6e, + 0x64, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, 0x0a, 0x0e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, + 0x01, 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, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x40, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x50, 0x01, 0x5a, 0x21, 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, 0x63, 0x6f, 0x72, 0x65, 0xaa, 0x02, 0x09, 0x58, + 0x72, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_core_config_proto_rawDescOnce sync.Once + file_core_config_proto_rawDescData = file_core_config_proto_rawDesc +) + +func file_core_config_proto_rawDescGZIP() []byte { + file_core_config_proto_rawDescOnce.Do(func() { + file_core_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_core_config_proto_rawDescData) + }) + return file_core_config_proto_rawDescData +} + +var file_core_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_core_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.core.Config + (*InboundHandlerConfig)(nil), // 1: xray.core.InboundHandlerConfig + (*OutboundHandlerConfig)(nil), // 2: xray.core.OutboundHandlerConfig + (*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage + (*transport.Config)(nil), // 4: xray.transport.Config +} +var file_core_config_proto_depIdxs = []int32{ + 1, // 0: xray.core.Config.inbound:type_name -> xray.core.InboundHandlerConfig + 2, // 1: xray.core.Config.outbound:type_name -> xray.core.OutboundHandlerConfig + 3, // 2: xray.core.Config.app:type_name -> xray.common.serial.TypedMessage + 4, // 3: xray.core.Config.transport:type_name -> xray.transport.Config + 3, // 4: xray.core.Config.extension:type_name -> xray.common.serial.TypedMessage + 3, // 5: xray.core.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage + 3, // 6: xray.core.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 3, // 7: xray.core.OutboundHandlerConfig.sender_settings:type_name -> xray.common.serial.TypedMessage + 3, // 8: xray.core.OutboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_core_config_proto_init() } +func file_core_config_proto_init() { + if File_core_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_core_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_core_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InboundHandlerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_core_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OutboundHandlerConfig); 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_core_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_core_config_proto_goTypes, + DependencyIndexes: file_core_config_proto_depIdxs, + MessageInfos: file_core_config_proto_msgTypes, + }.Build() + File_core_config_proto = out.File + file_core_config_proto_rawDesc = nil + file_core_config_proto_goTypes = nil + file_core_config_proto_depIdxs = nil +} diff --git a/core/config.proto b/core/config.proto new file mode 100644 index 00000000..3299c670 --- /dev/null +++ b/core/config.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package xray.core; +option csharp_namespace = "Xray.Core"; +option go_package = "github.com/xtls/xray-core/v1/core"; +option java_package = "com.xray.core"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; +import "transport/config.proto"; + +// Config is the master config of Xray. Xray takes this config as input and +// functions accordingly. +message Config { + // Inbound handler configurations. Must have at least one item. + repeated InboundHandlerConfig inbound = 1; + + // Outbound handler configurations. Must have at least one item. The first + // item is used as default for routing. + repeated OutboundHandlerConfig outbound = 2; + + reserved 3; + + // App is for configurations of all features in Xray. A feature must + // implement the Feature interface, and its config type must be registered + // through common.RegisterConfig. + repeated xray.common.serial.TypedMessage app = 4; + + // Transport settings. + // Deprecated. Each inbound and outbound should choose their own transport + // config. Date to remove: 2020-01-13 + xray.transport.Config transport = 5 [deprecated = true]; + + // Configuration for extensions. The config may not work if corresponding + // extension is not loaded into Xray. Xray will ignore such config during + // initialization. + repeated xray.common.serial.TypedMessage extension = 6; +} + +// InboundHandlerConfig is the configuration for inbound handler. +message InboundHandlerConfig { + // Tag of the inbound handler. The tag must be unique among all inbound + // handlers + string tag = 1; + // Settings for how this inbound proxy is handled. + xray.common.serial.TypedMessage receiver_settings = 2; + // Settings for inbound proxy. Must be one of the inbound proxies. + xray.common.serial.TypedMessage proxy_settings = 3; +} + +// OutboundHandlerConfig is the configuration for outbound handler. +message OutboundHandlerConfig { + // Tag of this outbound handler. + string tag = 1; + // Settings for how to dial connection for this outbound handler. + xray.common.serial.TypedMessage sender_settings = 2; + // Settings for this outbound proxy. Must be one of the outbound proxies. + xray.common.serial.TypedMessage proxy_settings = 3; + // If not zero, this outbound will be expired in seconds. Not used for now. + int64 expire = 4; + // Comment of this outbound handler. Not used for now. + string comment = 5; +} diff --git a/core/context.go b/core/context.go new file mode 100644 index 00000000..78460334 --- /dev/null +++ b/core/context.go @@ -0,0 +1,29 @@ +// +build !confonly + +package core + +import ( + "context" +) + +// XrayKey is the key type of Instance in Context, exported for test. +type XrayKey int + +const xrayKey XrayKey = 1 + +// FromContext returns an Instance from the given context, or nil if the context doesn't contain one. +func FromContext(ctx context.Context) *Instance { + if s, ok := ctx.Value(xrayKey).(*Instance); ok { + return s + } + return nil +} + +// MustFromContext returns an Instance from the given context, or panics if not present. +func MustFromContext(ctx context.Context) *Instance { + x := FromContext(ctx) + if x == nil { + panic("X is not in context.") + } + return x +} diff --git a/core/context_test.go b/core/context_test.go new file mode 100644 index 00000000..508d15a3 --- /dev/null +++ b/core/context_test.go @@ -0,0 +1,19 @@ +package core_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/v1/core" +) + +func TestContextPanic(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Error("expect panic, but nil") + } + }() + + MustFromContext(context.Background()) +} diff --git a/core/core.go b/core/core.go new file mode 100644 index 00000000..5c784650 --- /dev/null +++ b/core/core.go @@ -0,0 +1,39 @@ +// Package core provides an entry point to use Xray core functionalities. +// +// Xray makes it possible to accept incoming network connections with certain +// protocol, process the data, and send them through another connection with +// the same or a difference protocol on demand. +// +// It may be configured to work with multiple protocols at the same time, and +// uses the internal router to tunnel through different inbound and outbound +// connections. +package core + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "runtime" + + "github.com/xtls/xray-core/v1/common/serial" +) + +var ( + version = "1.0.0" + build = "Custom" + codename = "Xray, Penetrates Everything." + intro = "A unified platform for anti-censorship." +) + +// Version returns Xray's version as a string, in the form of "x.y.z" where x, y and z are numbers. +// ".z" part may be omitted in regular releases. +func Version() string { + return version +} + +// VersionStatement returns a list of strings representing the full version info. +func VersionStatement() []string { + return []string{ + serial.Concat("Xray ", Version(), " (", codename, ") ", build, " (", runtime.Version(), " ", runtime.GOOS, "/", runtime.GOARCH, ")"), + intro, + } +} diff --git a/core/errors.generated.go b/core/errors.generated.go new file mode 100644 index 00000000..672b2684 --- /dev/null +++ b/core/errors.generated.go @@ -0,0 +1,9 @@ +package core + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/core/functions.go b/core/functions.go new file mode 100644 index 00000000..55ba5060 --- /dev/null +++ b/core/functions.go @@ -0,0 +1,79 @@ +// +build !confonly + +package core + +import ( + "bytes" + "context" + + "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/transport/internet/udp" +) + +// CreateObject creates a new object based on the given Xray instance and config. The Xray instance may be nil. +func CreateObject(v *Instance, config interface{}) (interface{}, error) { + ctx := v.ctx + if v != nil { + ctx = context.WithValue(ctx, xrayKey, v) + } + return common.CreateObject(ctx, config) +} + +// StartInstance starts a new Xray instance with given serialized config. +// By default Xray only support config in protobuf format, i.e., configFormat = "protobuf". Caller need to load other packages to add JSON support. +// +// xray:api:stable +func StartInstance(configFormat string, configBytes []byte) (*Instance, error) { + config, err := LoadConfig(configFormat, "", bytes.NewReader(configBytes)) + if err != nil { + return nil, err + } + instance, err := New(config) + if err != nil { + return nil, err + } + if err := instance.Start(); err != nil { + return nil, err + } + return instance, nil +} + +// Dial provides an easy way for upstream caller to create net.Conn through Xray. +// It dispatches the request to the given destination by the given Xray instance. +// Since it is under a proxy context, the LocalAddr() and RemoteAddr() in returned net.Conn +// will not show real addresses being used for communication. +// +// xray:api:stable +func Dial(ctx context.Context, v *Instance, dest net.Destination) (net.Conn, error) { + dispatcher := v.GetFeature(routing.DispatcherType()) + if dispatcher == nil { + return nil, newError("routing.Dispatcher is not registered in Xray core") + } + r, err := dispatcher.(routing.Dispatcher).Dispatch(ctx, dest) + if err != nil { + return nil, err + } + var readerOpt net.ConnectionOption + if dest.Network == net.Network_TCP { + readerOpt = net.ConnectionOutputMulti(r.Reader) + } else { + readerOpt = net.ConnectionOutputMultiUDP(r.Reader) + } + return net.NewConnection(net.ConnectionInputMulti(r.Writer), readerOpt), nil +} + +// DialUDP provides a way to exchange UDP packets through Xray instance to remote servers. +// Since it is under a proxy context, the LocalAddr() in returned PacketConn will not show the real address. +// +// TODO: SetDeadline() / SetReadDeadline() / SetWriteDeadline() are not implemented. +// +// xray:api:beta +func DialUDP(ctx context.Context, v *Instance) (net.PacketConn, error) { + dispatcher := v.GetFeature(routing.DispatcherType()) + if dispatcher == nil { + return nil, newError("routing.Dispatcher is not registered in Xray core") + } + return udp.DialDispatcher(ctx, dispatcher.(routing.Dispatcher)) +} diff --git a/core/functions_test.go b/core/functions_test.go new file mode 100644 index 00000000..aebc231f --- /dev/null +++ b/core/functions_test.go @@ -0,0 +1,231 @@ +package core_test + +import ( + "context" + "crypto/rand" + "io" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/app/dispatcher" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + "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/proxy/freedom" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" +) + +func xor(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'c' + } + return r +} + +func xor2(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'd' + } + return r +} + +func TestXrayDial(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.Dial(context.Background(), server, dest) + common.Must(err) + defer conn.Close() + + const size = 10240 * 1024 + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, err := io.ReadFull(conn, receive); err != nil { + t.Fatal("failed to read all response: ", err) + } + + if r := cmp.Diff(xor(receive), payload); r != "" { + t.Error(r) + } +} + +func TestXrayDialUDPConn(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.Dial(context.Background(), server, dest) + common.Must(err) + defer conn.Close() + + const size = 1024 + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + for i := 0; i < 2; i++ { + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + } + + time.Sleep(time.Millisecond * 500) + + receive := make([]byte, size*2) + for i := 0; i < 2; i++ { + n, err := conn.Read(receive) + if err != nil { + t.Fatal("expect no error, but got ", err) + } + if n != size { + t.Fatal("expect read size ", size, " but got ", n) + } + + if r := cmp.Diff(xor(receive[:n]), payload); r != "" { + t.Fatal(r) + } + } +} + +func TestXrayDialUDP(t *testing.T) { + udpServer1 := udp.Server{ + MsgProcessor: xor, + } + dest1, err := udpServer1.Start() + common.Must(err) + defer udpServer1.Close() + + udpServer2 := udp.Server{ + MsgProcessor: xor2, + } + dest2, err := udpServer2.Start() + common.Must(err) + defer udpServer2.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.DialUDP(context.Background(), server) + common.Must(err) + defer conn.Close() + + const size = 1024 + { + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.WriteTo(payload, &net.UDPAddr{ + IP: dest1.Address.IP(), + Port: int(dest1.Port), + }); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, _, err := conn.ReadFrom(receive); err != nil { + t.Fatal(err) + } + + if r := cmp.Diff(xor(receive), payload); r != "" { + t.Error(r) + } + } + + { + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.WriteTo(payload, &net.UDPAddr{ + IP: dest2.Address.IP(), + Port: int(dest2.Port), + }); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, _, err := conn.ReadFrom(receive); err != nil { + t.Fatal(err) + } + + if r := cmp.Diff(xor2(receive), payload); r != "" { + t.Error(r) + } + } +} diff --git a/core/mocks.go b/core/mocks.go new file mode 100644 index 00000000..025e22fd --- /dev/null +++ b/core/mocks.go @@ -0,0 +1,8 @@ +package core + +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/io.go -mock_names Reader=Reader,Writer=Writer io Reader,Writer +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/log.go -mock_names Handler=LogHandler github.com/xtls/xray-core/v1/common/log Handler +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/mux.go -mock_names ClientWorkerFactory=MuxClientWorkerFactory github.com/xtls/xray-core/v1/common/mux ClientWorkerFactory +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/dns.go -mock_names Client=DNSClient github.com/xtls/xray-core/v1/features/dns Client +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/outbound.go -mock_names Manager=OutboundManager,HandlerSelector=OutboundHandlerSelector github.com/xtls/xray-core/v1/features/outbound Manager,HandlerSelector +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination testing/mocks/proxy.go -mock_names Inbound=ProxyInbound,Outbound=ProxyOutbound github.com/xtls/xray-core/v1/proxy Inbound,Outbound diff --git a/core/proto.go b/core/proto.go new file mode 100644 index 00000000..0672fff5 --- /dev/null +++ b/core/proto.go @@ -0,0 +1,12 @@ +package core + +import "path/filepath" + +//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go +//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc +//go:generate go install -v github.com/gogo/protobuf/protoc-gen-gofast +//go:generate go run ../infra/vprotogen/main.go + +// ProtoFilesUsingProtocGenGoFast is the map of Proto files +// that use `protoc-gen-gofast` to generate pb.go files +var ProtoFilesUsingProtocGenGoFast = map[string]bool{filepath.Join("proxy", "vless", "encoding", "addons.proto"): true} diff --git a/core/xray.go b/core/xray.go new file mode 100644 index 00000000..a6e8e309 --- /dev/null +++ b/core/xray.go @@ -0,0 +1,345 @@ +// +build !confonly + +package core + +import ( + "context" + "reflect" + "sync" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/features" + "github.com/xtls/xray-core/v1/features/dns" + "github.com/xtls/xray-core/v1/features/dns/localdns" + "github.com/xtls/xray-core/v1/features/inbound" + "github.com/xtls/xray-core/v1/features/outbound" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/features/stats" +) + +// Server is an instance of Xray. At any time, there must be at most one Server instance running. +type Server interface { + common.Runnable +} + +// ServerType returns the type of the server. +func ServerType() interface{} { + return (*Instance)(nil) +} + +type resolution struct { + deps []reflect.Type + callback interface{} +} + +func getFeature(allFeatures []features.Feature, t reflect.Type) features.Feature { + for _, f := range allFeatures { + if reflect.TypeOf(f.Type()) == t { + return f + } + } + return nil +} + +func (r *resolution) resolve(allFeatures []features.Feature) (bool, error) { + var fs []features.Feature + for _, d := range r.deps { + f := getFeature(allFeatures, d) + if f == nil { + return false, nil + } + fs = append(fs, f) + } + + callback := reflect.ValueOf(r.callback) + var input []reflect.Value + callbackType := callback.Type() + for i := 0; i < callbackType.NumIn(); i++ { + pt := callbackType.In(i) + for _, f := range fs { + if reflect.TypeOf(f).AssignableTo(pt) { + input = append(input, reflect.ValueOf(f)) + break + } + } + } + + if len(input) != callbackType.NumIn() { + panic("Can't get all input parameters") + } + + var err error + ret := callback.Call(input) + errInterface := reflect.TypeOf((*error)(nil)).Elem() + for i := len(ret) - 1; i >= 0; i-- { + if ret[i].Type() == errInterface { + v := ret[i].Interface() + if v != nil { + err = v.(error) + } + break + } + } + + return true, err +} + +// Instance combines all functionalities in Xray. +type Instance struct { + access sync.Mutex + features []features.Feature + featureResolutions []resolution + running bool + + ctx context.Context +} + +func AddInboundHandler(server *Instance, config *InboundHandlerConfig) error { + inboundManager := server.GetFeature(inbound.ManagerType()).(inbound.Manager) + rawHandler, err := CreateObject(server, config) + if err != nil { + return err + } + handler, ok := rawHandler.(inbound.Handler) + if !ok { + return newError("not an InboundHandler") + } + if err := inboundManager.AddHandler(server.ctx, handler); err != nil { + return err + } + return nil +} + +func addInboundHandlers(server *Instance, configs []*InboundHandlerConfig) error { + for _, inboundConfig := range configs { + if err := AddInboundHandler(server, inboundConfig); err != nil { + return err + } + } + + return nil +} + +func AddOutboundHandler(server *Instance, config *OutboundHandlerConfig) error { + outboundManager := server.GetFeature(outbound.ManagerType()).(outbound.Manager) + rawHandler, err := CreateObject(server, config) + if err != nil { + return err + } + handler, ok := rawHandler.(outbound.Handler) + if !ok { + return newError("not an OutboundHandler") + } + if err := outboundManager.AddHandler(server.ctx, handler); err != nil { + return err + } + return nil +} + +func addOutboundHandlers(server *Instance, configs []*OutboundHandlerConfig) error { + for _, outboundConfig := range configs { + if err := AddOutboundHandler(server, outboundConfig); err != nil { + return err + } + } + + return nil +} + +// RequireFeatures is a helper function to require features from Instance in context. +// See Instance.RequireFeatures for more information. +func RequireFeatures(ctx context.Context, callback interface{}) error { + v := MustFromContext(ctx) + return v.RequireFeatures(callback) +} + +// New returns a new Xray instance based on given configuration. +// The instance is not started at this point. +// To ensure Xray instance works properly, the config must contain one Dispatcher, one InboundHandlerManager and one OutboundHandlerManager. Other features are optional. +func New(config *Config) (*Instance, error) { + var server = &Instance{ctx: context.Background()} + + done, err := initInstanceWithConfig(config, server) + if done { + return nil, err + } + + return server, nil +} + +func NewWithContext(ctx context.Context, config *Config) (*Instance, error) { + var server = &Instance{ctx: ctx} + + done, err := initInstanceWithConfig(config, server) + if done { + return nil, err + } + + return server, nil +} + +func initInstanceWithConfig(config *Config, server *Instance) (bool, error) { + if config.Transport != nil { + features.PrintDeprecatedFeatureWarning("global transport settings") + } + if err := config.Transport.Apply(); err != nil { + return true, err + } + + for _, appSettings := range config.App { + settings, err := appSettings.GetInstance() + if err != nil { + return true, err + } + obj, err := CreateObject(server, settings) + if err != nil { + return true, err + } + if feature, ok := obj.(features.Feature); ok { + if err := server.AddFeature(feature); err != nil { + return true, err + } + } + } + + essentialFeatures := []struct { + Type interface{} + Instance features.Feature + }{ + {dns.ClientType(), localdns.New()}, + {policy.ManagerType(), policy.DefaultManager{}}, + {routing.RouterType(), routing.DefaultRouter{}}, + {stats.ManagerType(), stats.NoopManager{}}, + } + + for _, f := range essentialFeatures { + if server.GetFeature(f.Type) == nil { + if err := server.AddFeature(f.Instance); err != nil { + return true, err + } + } + } + + if server.featureResolutions != nil { + return true, newError("not all dependency are resolved.") + } + + if err := addInboundHandlers(server, config.Inbound); err != nil { + return true, err + } + + if err := addOutboundHandlers(server, config.Outbound); err != nil { + return true, err + } + return false, nil +} + +// Type implements common.HasType. +func (s *Instance) Type() interface{} { + return ServerType() +} + +// Close shutdown the Xray instance. +func (s *Instance) Close() error { + s.access.Lock() + defer s.access.Unlock() + + s.running = false + + var errors []interface{} + for _, f := range s.features { + if err := f.Close(); err != nil { + errors = append(errors, err) + } + } + if len(errors) > 0 { + return newError("failed to close all features").Base(newError(serial.Concat(errors...))) + } + + return nil +} + +// RequireFeatures registers a callback, which will be called when all dependent features are registered. +// The callback must be a func(). All its parameters must be features.Feature. +func (s *Instance) RequireFeatures(callback interface{}) error { + callbackType := reflect.TypeOf(callback) + if callbackType.Kind() != reflect.Func { + panic("not a function") + } + + var featureTypes []reflect.Type + for i := 0; i < callbackType.NumIn(); i++ { + featureTypes = append(featureTypes, reflect.PtrTo(callbackType.In(i))) + } + + r := resolution{ + deps: featureTypes, + callback: callback, + } + if finished, err := r.resolve(s.features); finished { + return err + } + s.featureResolutions = append(s.featureResolutions, r) + return nil +} + +// AddFeature registers a feature into current Instance. +func (s *Instance) AddFeature(feature features.Feature) error { + s.features = append(s.features, feature) + + if s.running { + if err := feature.Start(); err != nil { + newError("failed to start feature").Base(err).WriteToLog() + } + return nil + } + + if s.featureResolutions == nil { + return nil + } + + var pendingResolutions []resolution + for _, r := range s.featureResolutions { + finished, err := r.resolve(s.features) + if finished && err != nil { + return err + } + if !finished { + pendingResolutions = append(pendingResolutions, r) + } + } + if len(pendingResolutions) == 0 { + s.featureResolutions = nil + } else if len(pendingResolutions) < len(s.featureResolutions) { + s.featureResolutions = pendingResolutions + } + + return nil +} + +// GetFeature returns a feature of the given type, or nil if such feature is not registered. +func (s *Instance) GetFeature(featureType interface{}) features.Feature { + return getFeature(s.features, reflect.TypeOf(featureType)) +} + +// Start starts the Xray instance, including all registered features. When Start returns error, the state of the instance is unknown. +// A Xray instance can be started only once. Upon closing, the instance is not guaranteed to start again. +// +// xray:api:stable +func (s *Instance) Start() error { + s.access.Lock() + defer s.access.Unlock() + + s.running = true + for _, f := range s.features { + if err := f.Start(); err != nil { + return err + } + } + + newError("Xray ", Version(), " started").AtWarning().WriteToLog() + + return nil +} diff --git a/core/xray_test.go b/core/xray_test.go new file mode 100644 index 00000000..e5f47c1b --- /dev/null +++ b/core/xray_test.go @@ -0,0 +1,90 @@ +package core_test + +import ( + "testing" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/app/dispatcher" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/uuid" + . "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/dns" + "github.com/xtls/xray-core/v1/features/dns/localdns" + _ "github.com/xtls/xray-core/v1/main/distro/all" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func TestXrayDependency(t *testing.T) { + instance := new(Instance) + + wait := make(chan bool, 1) + instance.RequireFeatures(func(d dns.Client) { + if d == nil { + t.Error("expected dns client fulfilled, but actually nil") + } + wait <- true + }) + instance.AddFeature(localdns.New()) + <-wait +} + +func TestXrayClose(t *testing.T) { + port := tcp.PickPort() + + userID := uuid.New() + config := &Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Inbound: []*InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(port), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(0), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP, net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(0), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := StartInstance("protobuf", cfgBytes) + common.Must(err) + server.Close() +} diff --git a/features/dns/client.go b/features/dns/client.go new file mode 100644 index 00000000..e6bdb645 --- /dev/null +++ b/features/dns/client.go @@ -0,0 +1,59 @@ +package dns + +import ( + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/features" +) + +// Client is a Xray feature for querying DNS information. +// +// xray:api:stable +type Client interface { + features.Feature + + // LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses. + LookupIP(domain string) ([]net.IP, error) +} + +// IPv4Lookup is an optional feature for querying IPv4 addresses only. +// +// xray:api:beta +type IPv4Lookup interface { + LookupIPv4(domain string) ([]net.IP, error) +} + +// IPv6Lookup is an optional feature for querying IPv6 addresses only. +// +// xray:api:beta +type IPv6Lookup interface { + LookupIPv6(domain string) ([]net.IP, error) +} + +// ClientType returns the type of Client interface. Can be used for implementing common.HasType. +// +// xray:api:beta +func ClientType() interface{} { + return (*Client)(nil) +} + +// ErrEmptyResponse indicates that DNS query succeeded but no answer was returned. +var ErrEmptyResponse = errors.New("empty response") + +type RCodeError uint16 + +func (e RCodeError) Error() string { + return serial.Concat("rcode: ", uint16(e)) +} + +func RCodeFromError(err error) uint16 { + if err == nil { + return 0 + } + cause := errors.Cause(err) + if r, ok := cause.(RCodeError); ok { + return uint16(r) + } + return 0 +} diff --git a/features/dns/localdns/client.go b/features/dns/localdns/client.go new file mode 100644 index 00000000..2cfe255d --- /dev/null +++ b/features/dns/localdns/client.go @@ -0,0 +1,80 @@ +package localdns + +import ( + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/features/dns" +) + +// Client is an implementation of dns.Client, which queries localhost for DNS. +type Client struct{} + +// Type implements common.HasType. +func (*Client) Type() interface{} { + return dns.ClientType() +} + +// Start implements common.Runnable. +func (*Client) Start() error { return nil } + +// Close implements common.Closable. +func (*Client) Close() error { return nil } + +// LookupIP implements Client. +func (*Client) LookupIP(host string) ([]net.IP, error) { + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + parsedIPs := make([]net.IP, 0, len(ips)) + for _, ip := range ips { + parsed := net.IPAddress(ip) + if parsed != nil { + parsedIPs = append(parsedIPs, parsed.IP()) + } + } + if len(parsedIPs) == 0 { + return nil, dns.ErrEmptyResponse + } + return parsedIPs, nil +} + +// LookupIPv4 implements IPv4Lookup. +func (c *Client) LookupIPv4(host string) ([]net.IP, error) { + ips, err := c.LookupIP(host) + if err != nil { + return nil, err + } + ipv4 := make([]net.IP, 0, len(ips)) + for _, ip := range ips { + if len(ip) == net.IPv4len { + ipv4 = append(ipv4, ip) + } + } + if len(ipv4) == 0 { + return nil, dns.ErrEmptyResponse + } + return ipv4, nil +} + +// LookupIPv6 implements IPv6Lookup. +func (c *Client) LookupIPv6(host string) ([]net.IP, error) { + ips, err := c.LookupIP(host) + if err != nil { + return nil, err + } + ipv6 := make([]net.IP, 0, len(ips)) + for _, ip := range ips { + if len(ip) == net.IPv6len { + ipv6 = append(ipv6, ip) + } + } + if len(ipv6) == 0 { + return nil, dns.ErrEmptyResponse + } + return ipv6, nil +} + +// New create a new dns.Client that queries localhost for DNS. +func New() *Client { + return &Client{} +} diff --git a/features/errors.generated.go b/features/errors.generated.go new file mode 100644 index 00000000..13fa0672 --- /dev/null +++ b/features/errors.generated.go @@ -0,0 +1,9 @@ +package features + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/features/feature.go b/features/feature.go new file mode 100644 index 00000000..efae323a --- /dev/null +++ b/features/feature.go @@ -0,0 +1,17 @@ +package features + +import "github.com/xtls/xray-core/v1/common" + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +// Feature is the interface for Xray features. All features must implement this interface. +// All existing features have an implementation in app directory. These features can be replaced by third-party ones. +type Feature interface { + common.HasType + common.Runnable +} + +// PrintDeprecatedFeatureWarning prints a warning for deprecated feature. +func PrintDeprecatedFeatureWarning(feature string) { + newError("You are using a deprecated feature: " + feature + ". Please update your config file with latest configuration format, or update your client software.").WriteToLog() +} diff --git a/features/inbound/inbound.go b/features/inbound/inbound.go new file mode 100644 index 00000000..ac57b9ea --- /dev/null +++ b/features/inbound/inbound.go @@ -0,0 +1,42 @@ +package inbound + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/features" +) + +// Handler is the interface for handlers that process inbound connections. +// +// xray:api:stable +type Handler interface { + common.Runnable + // The tag of this handler. + Tag() string + + // Deprecated: Do not use in new code. + GetRandomInboundProxy() (interface{}, net.Port, int) +} + +// Manager is a feature that manages InboundHandlers. +// +// xray:api:stable +type Manager interface { + features.Feature + // GetHandlers returns an InboundHandler for the given tag. + GetHandler(ctx context.Context, tag string) (Handler, error) + // AddHandler adds the given handler into this Manager. + AddHandler(ctx context.Context, handler Handler) error + + // RemoveHandler removes a handler from Manager. + RemoveHandler(ctx context.Context, tag string) error +} + +// ManagerType returns the type of Manager interface. Can be used for implementing common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} diff --git a/features/outbound/outbound.go b/features/outbound/outbound.go new file mode 100644 index 00000000..b25c4427 --- /dev/null +++ b/features/outbound/outbound.go @@ -0,0 +1,45 @@ +package outbound + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/features" + "github.com/xtls/xray-core/v1/transport" +) + +// Handler is the interface for handlers that process outbound connections. +// +// xray:api:stable +type Handler interface { + common.Runnable + Tag() string + Dispatch(ctx context.Context, link *transport.Link) +} + +type HandlerSelector interface { + Select([]string) []string +} + +// Manager is a feature that manages outbound.Handlers. +// +// xray:api:stable +type Manager interface { + features.Feature + // GetHandler returns an outbound.Handler for the given tag. + GetHandler(tag string) Handler + // GetDefaultHandler returns the default outbound.Handler. It is usually the first outbound.Handler specified in the configuration. + GetDefaultHandler() Handler + // AddHandler adds a handler into this outbound.Manager. + AddHandler(ctx context.Context, handler Handler) error + + // RemoveHandler removes a handler from outbound.Manager. + RemoveHandler(ctx context.Context, tag string) error +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} diff --git a/features/policy/default.go b/features/policy/default.go new file mode 100644 index 00000000..8d363c7e --- /dev/null +++ b/features/policy/default.go @@ -0,0 +1,37 @@ +package policy + +import ( + "time" +) + +// DefaultManager is the implementation of the Manager. +type DefaultManager struct{} + +// Type implements common.HasType. +func (DefaultManager) Type() interface{} { + return ManagerType() +} + +// ForLevel implements Manager. +func (DefaultManager) ForLevel(level uint32) Session { + p := SessionDefault() + if level == 1 { + p.Timeouts.ConnectionIdle = time.Second * 600 + } + return p +} + +// ForSystem implements Manager. +func (DefaultManager) ForSystem() System { + return System{} +} + +// Start implements common.Runnable. +func (DefaultManager) Start() error { + return nil +} + +// Close implements common.Closable. +func (DefaultManager) Close() error { + return nil +} diff --git a/features/policy/policy.go b/features/policy/policy.go new file mode 100644 index 00000000..b10708ce --- /dev/null +++ b/features/policy/policy.go @@ -0,0 +1,151 @@ +package policy + +import ( + "context" + "runtime" + "time" + + "github.com/xtls/xray-core/v1/common/platform" + "github.com/xtls/xray-core/v1/features" +) + +// Timeout contains limits for connection timeout. +type Timeout struct { + // Timeout for handshake phase in a connection. + Handshake time.Duration + // Timeout for connection being idle, i.e., there is no egress or ingress traffic in this connection. + ConnectionIdle time.Duration + // Timeout for an uplink only connection, i.e., the downlink of the connection has been closed. + UplinkOnly time.Duration + // Timeout for an downlink only connection, i.e., the uplink of the connection has been closed. + DownlinkOnly time.Duration +} + +// Stats contains settings for stats counters. +type Stats struct { + // Whether or not to enable stat counter for user uplink traffic. + UserUplink bool + // Whether or not to enable stat counter for user downlink traffic. + UserDownlink bool +} + +// Buffer contains settings for internal buffer. +type Buffer struct { + // Size of buffer per connection, in bytes. -1 for unlimited buffer. + PerConnection int32 +} + +// SystemStats contains stat policy settings on system level. +type SystemStats struct { + // Whether or not to enable stat counter for uplink traffic in inbound handlers. + InboundUplink bool + // Whether or not to enable stat counter for downlink traffic in inbound handlers. + InboundDownlink bool + // Whether or not to enable stat counter for uplink traffic in outbound handlers. + OutboundUplink bool + // Whether or not to enable stat counter for downlink traffic in outbound handlers. + OutboundDownlink bool +} + +// System contains policy settings at system level. +type System struct { + Stats SystemStats + Buffer Buffer +} + +// Session is session based settings for controlling Xray requests. It contains various settings (or limits) that may differ for different users in the context. +type Session struct { + Timeouts Timeout // Timeout settings + Stats Stats + Buffer Buffer +} + +// Manager is a feature that provides Policy for the given user by its id or level. +// +// xray:api:stable +type Manager interface { + features.Feature + + // ForLevel returns the Session policy for the given user level. + ForLevel(level uint32) Session + + // ForSystem returns the System policy for Xray system. + ForSystem() System +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} + +var defaultBufferSize int32 + +func init() { + const key = "xray.ray.buffer.size" + const defaultValue = -17 + size := platform.EnvFlag{ + Name: key, + AltName: platform.NormalizeEnvName(key), + }.GetValueAsInt(defaultValue) + + switch size { + case 0: + defaultBufferSize = -1 // For pipe to use unlimited size + case defaultValue: // Env flag not defined. Use default values per CPU-arch. + switch runtime.GOARCH { + case "arm", "mips", "mipsle": + defaultBufferSize = 0 + case "arm64", "mips64", "mips64le": + defaultBufferSize = 4 * 1024 // 4k cache for low-end devices + default: + defaultBufferSize = 512 * 1024 + } + default: + defaultBufferSize = int32(size) * 1024 * 1024 + } +} + +func defaultBufferPolicy() Buffer { + return Buffer{ + PerConnection: defaultBufferSize, + } +} + +// SessionDefault returns the Policy when user is not specified. +func SessionDefault() Session { + return Session{ + Timeouts: Timeout{ + // Align Handshake timeout with nginx client_header_timeout + // So that this value will not indicate server identity + Handshake: time.Second * 60, + ConnectionIdle: time.Second * 300, + UplinkOnly: time.Second * 1, + DownlinkOnly: time.Second * 1, + }, + Stats: Stats{ + UserUplink: false, + UserDownlink: false, + }, + Buffer: defaultBufferPolicy(), + } +} + +type policyKey int32 + +const ( + bufferPolicyKey policyKey = 0 +) + +func ContextWithBufferPolicy(ctx context.Context, p Buffer) context.Context { + return context.WithValue(ctx, bufferPolicyKey, p) +} + +func BufferPolicyFromContext(ctx context.Context) Buffer { + pPolicy := ctx.Value(bufferPolicyKey) + if pPolicy == nil { + return defaultBufferPolicy() + } + return pPolicy.(Buffer) +} diff --git a/features/routing/context.go b/features/routing/context.go new file mode 100644 index 00000000..667678c4 --- /dev/null +++ b/features/routing/context.go @@ -0,0 +1,40 @@ +package routing + +import ( + "github.com/xtls/xray-core/v1/common/net" +) + +// Context is a feature to store connection information for routing. +// +// xray:api:stable +type Context interface { + // GetInboundTag returns the tag of the inbound the connection was from. + GetInboundTag() string + + // GetSourcesIPs returns the source IPs bound to the connection. + GetSourceIPs() []net.IP + + // GetSourcePort returns the source port of the connection. + GetSourcePort() net.Port + + // GetTargetIPs returns the target IP of the connection or resolved IPs of target domain. + GetTargetIPs() []net.IP + + // GetTargetPort returns the target port of the connection. + GetTargetPort() net.Port + + // GetTargetDomain returns the target domain of the connection, if exists. + GetTargetDomain() string + + // GetNetwork returns the network type of the connection. + GetNetwork() net.Network + + // GetProtocol returns the protocol from the connection content, if sniffed out. + GetProtocol() string + + // GetUser returns the user email from the connection content, if exists. + GetUser() string + + // GetAttributes returns extra attributes from the conneciont content. + GetAttributes() map[string]string +} diff --git a/features/routing/dispatcher.go b/features/routing/dispatcher.go new file mode 100644 index 00000000..e953bace --- /dev/null +++ b/features/routing/dispatcher.go @@ -0,0 +1,27 @@ +package routing + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/features" + "github.com/xtls/xray-core/v1/transport" +) + +// Dispatcher is a feature that dispatches inbound requests to outbound handlers based on rules. +// Dispatcher is required to be registered in a Xray instance to make Xray function properly. +// +// xray:api:stable +type Dispatcher interface { + features.Feature + + // Dispatch returns a Ray for transporting data for the given request. + Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) +} + +// DispatcherType returns the type of Dispatcher interface. Can be used to implement common.HasType. +// +// xray:api:stable +func DispatcherType() interface{} { + return (*Dispatcher)(nil) +} diff --git a/features/routing/dns/context.go b/features/routing/dns/context.go new file mode 100644 index 00000000..e3d78ddf --- /dev/null +++ b/features/routing/dns/context.go @@ -0,0 +1,44 @@ +package dns + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/features/dns" + "github.com/xtls/xray-core/v1/features/routing" +) + +// ResolvableContext is an implementation of routing.Context, with domain resolving capability. +type ResolvableContext struct { + routing.Context + dnsClient dns.Client + resolvedIPs []net.IP +} + +// GetTargetIPs overrides original routing.Context's implementation. +func (ctx *ResolvableContext) GetTargetIPs() []net.IP { + if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { + return ips + } + + if len(ctx.resolvedIPs) > 0 { + return ctx.resolvedIPs + } + + if domain := ctx.GetTargetDomain(); len(domain) != 0 { + ips, err := ctx.dnsClient.LookupIP(domain) + if err == nil { + ctx.resolvedIPs = ips + return ips + } + newError("resolve ip for ", domain).Base(err).WriteToLog() + } + + return nil +} + +// ContextWithDNSClient creates a new routing context with domain resolving capability. +// Resolved domain IPs can be retrieved by GetTargetIPs(). +func ContextWithDNSClient(ctx routing.Context, client dns.Client) routing.Context { + return &ResolvableContext{Context: ctx, dnsClient: client} +} diff --git a/features/routing/dns/errors.generated.go b/features/routing/dns/errors.generated.go new file mode 100644 index 00000000..ce4c0605 --- /dev/null +++ b/features/routing/dns/errors.generated.go @@ -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{}) +} diff --git a/features/routing/router.go b/features/routing/router.go new file mode 100644 index 00000000..d81a0b30 --- /dev/null +++ b/features/routing/router.go @@ -0,0 +1,60 @@ +package routing + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/features" +) + +// Router is a feature to choose an outbound tag for the given request. +// +// xray:api:stable +type Router interface { + features.Feature + + // PickRoute returns a route decision based on the given routing context. + PickRoute(ctx Context) (Route, error) +} + +// Route is the routing result of Router feature. +// +// xray:api:stable +type Route interface { + // A Route is also a routing context. + Context + + // GetOutboundGroupTags returns the detoured outbound group tags in sequence before a final outbound is chosen. + GetOutboundGroupTags() []string + + // GetOutboundTag returns the tag of the outbound the connection was dispatched to. + GetOutboundTag() string +} + +// RouterType return the type of Router interface. Can be used to implement common.HasType. +// +// xray:api:stable +func RouterType() interface{} { + return (*Router)(nil) +} + +// DefaultRouter is an implementation of Router, which always returns ErrNoClue for routing decisions. +type DefaultRouter struct{} + +// Type implements common.HasType. +func (DefaultRouter) Type() interface{} { + return RouterType() +} + +// PickRoute implements Router. +func (DefaultRouter) PickRoute(ctx Context) (Route, error) { + return nil, common.ErrNoClue +} + +// Start implements common.Runnable. +func (DefaultRouter) Start() error { + return nil +} + +// Close implements common.Closable. +func (DefaultRouter) Close() error { + return nil +} diff --git a/features/routing/session/context.go b/features/routing/session/context.go new file mode 100644 index 00000000..c81cea50 --- /dev/null +++ b/features/routing/session/context.go @@ -0,0 +1,119 @@ +package session + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/features/routing" +) + +// Context is an implementation of routing.Context, which is a wrapper of context.context with session info. +type Context struct { + Inbound *session.Inbound + Outbound *session.Outbound + Content *session.Content +} + +// GetInboundTag implements routing.Context. +func (ctx *Context) GetInboundTag() string { + if ctx.Inbound == nil { + return "" + } + return ctx.Inbound.Tag +} + +// GetSourceIPs implements routing.Context. +func (ctx *Context) GetSourceIPs() []net.IP { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return nil + } + dest := ctx.Inbound.Source + if dest.Address.Family().IsDomain() { + return nil + } + + return []net.IP{dest.Address.IP()} +} + +// GetSourcePort implements routing.Context. +func (ctx *Context) GetSourcePort() net.Port { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return 0 + } + return ctx.Inbound.Source.Port +} + +// GetTargetIPs implements routing.Context. +func (ctx *Context) GetTargetIPs() []net.IP { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return nil + } + + if ctx.Outbound.Target.Address.Family().IsIP() { + return []net.IP{ctx.Outbound.Target.Address.IP()} + } + + return nil +} + +// GetTargetPort implements routing.Context. +func (ctx *Context) GetTargetPort() net.Port { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return 0 + } + return ctx.Outbound.Target.Port +} + +// GetTargetDomain implements routing.Context. +func (ctx *Context) GetTargetDomain() string { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return "" + } + dest := ctx.Outbound.Target + if !dest.Address.Family().IsDomain() { + return "" + } + return dest.Address.Domain() +} + +// GetNetwork implements routing.Context. +func (ctx *Context) GetNetwork() net.Network { + if ctx.Outbound == nil { + return net.Network_Unknown + } + return ctx.Outbound.Target.Network +} + +// GetProtocol implements routing.Context. +func (ctx *Context) GetProtocol() string { + if ctx.Content == nil { + return "" + } + return ctx.Content.Protocol +} + +// GetUser implements routing.Context. +func (ctx *Context) GetUser() string { + if ctx.Inbound == nil || ctx.Inbound.User == nil { + return "" + } + return ctx.Inbound.User.Email +} + +// GetAttributes implements routing.Context. +func (ctx *Context) GetAttributes() map[string]string { + if ctx.Content == nil { + return nil + } + return ctx.Content.Attributes +} + +// AsRoutingContext creates a context from context.context with session info. +func AsRoutingContext(ctx context.Context) routing.Context { + return &Context{ + Inbound: session.InboundFromContext(ctx), + Outbound: session.OutboundFromContext(ctx), + Content: session.ContentFromContext(ctx), + } +} diff --git a/features/stats/errors.generated.go b/features/stats/errors.generated.go new file mode 100644 index 00000000..d0bc052a --- /dev/null +++ b/features/stats/errors.generated.go @@ -0,0 +1,9 @@ +package stats + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/features/stats/stats.go b/features/stats/stats.go new file mode 100644 index 00000000..b8a4906b --- /dev/null +++ b/features/stats/stats.go @@ -0,0 +1,151 @@ +package stats + +//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/features" +) + +// Counter is the interface for stats counters. +// +// xray:api:stable +type Counter interface { + // Value is the current value of the counter. + Value() int64 + // Set sets a new value to the counter, and returns the previous one. + Set(int64) int64 + // Add adds a value to the current counter value, and returns the previous value. + Add(int64) int64 +} + +// Channel is the interface for stats channel. +// +// xray:api:stable +type Channel interface { + // Channel is a runnable unit. + common.Runnable + // Publish broadcasts a message through the channel with a controlling context. + Publish(context.Context, interface{}) + // SubscriberCount returns the number of the subscribers. + Subscribers() []chan interface{} + // Subscribe registers for listening to channel stream and returns a new listener channel. + Subscribe() (chan interface{}, error) + // Unsubscribe unregisters a listener channel from current Channel object. + Unsubscribe(chan interface{}) error +} + +// SubscribeRunnableChannel subscribes the channel and starts it if there is first subscriber coming. +func SubscribeRunnableChannel(c Channel) (chan interface{}, error) { + if len(c.Subscribers()) == 0 { + if err := c.Start(); err != nil { + return nil, err + } + } + return c.Subscribe() +} + +// UnsubscribeClosableChannel unsubcribes the channel and close it if there is no more subscriber. +func UnsubscribeClosableChannel(c Channel, sub chan interface{}) error { + if err := c.Unsubscribe(sub); err != nil { + return err + } + if len(c.Subscribers()) == 0 { + return c.Close() + } + return nil +} + +// Manager is the interface for stats manager. +// +// xray:api:stable +type Manager interface { + features.Feature + + // RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters. + RegisterCounter(string) (Counter, error) + // UnregisterCounter unregisters a counter from the manager by its identifier. + UnregisterCounter(string) error + // GetCounter returns a counter by its identifier. + GetCounter(string) Counter + + // RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels. + RegisterChannel(string) (Channel, error) + // UnregisterCounter unregisters a channel from the manager by its identifier. + UnregisterChannel(string) error + // GetChannel returns a channel by its identifier. + GetChannel(string) Channel +} + +// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter. +func GetOrRegisterCounter(m Manager, name string) (Counter, error) { + counter := m.GetCounter(name) + if counter != nil { + return counter, nil + } + + return m.RegisterCounter(name) +} + +// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel. +func GetOrRegisterChannel(m Manager, name string) (Channel, error) { + channel := m.GetChannel(name) + if channel != nil { + return channel, nil + } + + return m.RegisterChannel(name) +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} + +// NoopManager is an implementation of Manager, which doesn't has actual functionalities. +type NoopManager struct{} + +// Type implements common.HasType. +func (NoopManager) Type() interface{} { + return ManagerType() +} + +// RegisterCounter implements Manager. +func (NoopManager) RegisterCounter(string) (Counter, error) { + return nil, newError("not implemented") +} + +// UnregisterCounter implements Manager. +func (NoopManager) UnregisterCounter(string) error { + return nil +} + +// GetCounter implements Manager. +func (NoopManager) GetCounter(string) Counter { + return nil +} + +// RegisterChannel implements Manager. +func (NoopManager) RegisterChannel(string) (Channel, error) { + return nil, newError("not implemented") +} + +// UnregisterChannel implements Manager. +func (NoopManager) UnregisterChannel(string) error { + return nil +} + +// GetChannel implements Manager. +func (NoopManager) GetChannel(string) Channel { + return nil +} + +// Start implements common.Runnable. +func (NoopManager) Start() error { return nil } + +// Close implements common.Closable. +func (NoopManager) Close() error { return nil } diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..b2fe207c --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/xtls/xray-core/v1 + +go 1.15 + +require ( + github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect + github.com/golang/mock v1.4.4 + github.com/golang/protobuf v1.4.3 + github.com/google/go-cmp v0.5.3 + github.com/gorilla/websocket v1.4.2 + github.com/lucas-clemente/quic-go v0.19.2 + github.com/miekg/dns v1.1.35 + github.com/pires/go-proxyproto v0.3.2 + github.com/seiflotfy/cuckoofilter v0.0.0-20201009151232-afb285a456ab + github.com/stretchr/testify v1.6.1 + github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 + go.starlark.net v0.0.0-20201118183435-e55f603d8c79 + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 + google.golang.org/grpc v1.33.2 + google.golang.org/protobuf v1.25.0 + h12.io/socks v1.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..720d5910 --- /dev/null +++ b/go.sum @@ -0,0 +1,301 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucas-clemente/quic-go v0.19.2 h1:w8BBYUx5Z+kNpeaOeQW/KzcNsKWhh4O6PeQhb0nURPg= +github.com/lucas-clemente/quic-go v0.19.2/go.mod h1:ZUygOqIoai0ASXXLJ92LTnKdbqh9MHCLTX6Nr1jUrK0= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= +github.com/marten-seemann/qtls-go1-15 v0.1.1 h1:LIH6K34bPVttyXnUWixk0bzH6/N07VxbSabxn5A5gZQ= +github.com/marten-seemann/qtls-go1-15 v0.1.1/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pires/go-proxyproto v0.3.2 h1:E5ig1h9SFGne7IWVY6yRu3UCzyAFkQIukXHMkdFUOCA= +github.com/pires/go-proxyproto v0.3.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/seiflotfy/cuckoofilter v0.0.0-20201009151232-afb285a456ab h1:O43uBnD2Y6fo1oFsXY+Vqp1n3RFfxg1u3XATDGvUXgI= +github.com/seiflotfy/cuckoofilter v0.0.0-20201009151232-afb285a456ab/go.mod h1:ET5mVvNjwaGXRgZxO9UZr7X+8eAf87AfIYNwRSp9s4Y= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo= +github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.starlark.net v0.0.0-20201118183435-e55f603d8c79 h1:JPjLPz44y2N9mkzh2N344kTk1Y4/V4yJAjTrXGmzv8I= +go.starlark.net v0.0.0-20201118183435-e55f603d8c79/go.mod h1:5YFcFnRptTN+41758c2bMPiqpGg4zBfYji1IQz8wNFk= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +h12.io/socks v1.0.1/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/infra/conf/api.go b/infra/conf/api.go new file mode 100644 index 00000000..1305ce4c --- /dev/null +++ b/infra/conf/api.go @@ -0,0 +1,41 @@ +package conf + +import ( + "strings" + + "github.com/xtls/xray-core/v1/app/commander" + loggerservice "github.com/xtls/xray-core/v1/app/log/command" + handlerservice "github.com/xtls/xray-core/v1/app/proxyman/command" + statsservice "github.com/xtls/xray-core/v1/app/stats/command" + "github.com/xtls/xray-core/v1/common/serial" +) + +type APIConfig struct { + Tag string `json:"tag"` + Services []string `json:"services"` +} + +func (c *APIConfig) Build() (*commander.Config, error) { + if c.Tag == "" { + return nil, newError("API tag can't be empty.") + } + + services := make([]*serial.TypedMessage, 0, 16) + for _, s := range c.Services { + switch strings.ToLower(s) { + case "reflectionservice": + services = append(services, serial.ToTypedMessage(&commander.ReflectionConfig{})) + case "handlerservice": + services = append(services, serial.ToTypedMessage(&handlerservice.Config{})) + case "loggerservice": + services = append(services, serial.ToTypedMessage(&loggerservice.Config{})) + case "statsservice": + services = append(services, serial.ToTypedMessage(&statsservice.Config{})) + } + } + + return &commander.Config{ + Tag: c.Tag, + Service: services, + }, nil +} diff --git a/infra/conf/blackhole.go b/infra/conf/blackhole.go new file mode 100644 index 00000000..09050720 --- /dev/null +++ b/infra/conf/blackhole.go @@ -0,0 +1,53 @@ +package conf + +import ( + "encoding/json" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/blackhole" +) + +type NoneResponse struct{} + +func (*NoneResponse) Build() (proto.Message, error) { + return new(blackhole.NoneResponse), nil +} + +type HTTPResponse struct{} + +func (*HTTPResponse) Build() (proto.Message, error) { + return new(blackhole.HTTPResponse), nil +} + +type BlackholeConfig struct { + Response json.RawMessage `json:"response"` +} + +func (v *BlackholeConfig) Build() (proto.Message, error) { + config := new(blackhole.Config) + if v.Response != nil { + response, _, err := configLoader.Load(v.Response) + if err != nil { + return nil, newError("Config: Failed to parse Blackhole response config.").Base(err) + } + responseSettings, err := response.(Buildable).Build() + if err != nil { + return nil, err + } + config.Response = serial.ToTypedMessage(responseSettings) + } + + return config, nil +} + +var ( + configLoader = NewJSONConfigLoader( + ConfigCreatorCache{ + "none": func() interface{} { return new(NoneResponse) }, + "http": func() interface{} { return new(HTTPResponse) }, + }, + "type", + "") +) diff --git a/infra/conf/blackhole_test.go b/infra/conf/blackhole_test.go new file mode 100644 index 00000000..c5e55253 --- /dev/null +++ b/infra/conf/blackhole_test.go @@ -0,0 +1,34 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/blackhole" +) + +func TestHTTPResponseJSON(t *testing.T) { + creator := func() Buildable { + return new(BlackholeConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "response": { + "type": "http" + } + }`, + Parser: loadJSON(creator), + Output: &blackhole.Config{ + Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), + }, + }, + { + Input: `{}`, + Parser: loadJSON(creator), + Output: &blackhole.Config{}, + }, + }) +} diff --git a/infra/conf/buildable.go b/infra/conf/buildable.go new file mode 100644 index 00000000..1d01cd66 --- /dev/null +++ b/infra/conf/buildable.go @@ -0,0 +1,7 @@ +package conf + +import "github.com/golang/protobuf/proto" + +type Buildable interface { + Build() (proto.Message, error) +} diff --git a/infra/conf/common.go b/infra/conf/common.go new file mode 100644 index 00000000..a2674c61 --- /dev/null +++ b/infra/conf/common.go @@ -0,0 +1,241 @@ +package conf + +import ( + "encoding/json" + "os" + "strings" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" +) + +type StringList []string + +func NewStringList(raw []string) *StringList { + list := StringList(raw) + return &list +} + +func (v StringList) Len() int { + return len(v) +} + +func (v *StringList) UnmarshalJSON(data []byte) error { + var strarray []string + if err := json.Unmarshal(data, &strarray); err == nil { + *v = *NewStringList(strarray) + return nil + } + + var rawstr string + if err := json.Unmarshal(data, &rawstr); err == nil { + strlist := strings.Split(rawstr, ",") + *v = *NewStringList(strlist) + return nil + } + return newError("unknown format of a string list: " + string(data)) +} + +type Address struct { + net.Address +} + +func (v *Address) UnmarshalJSON(data []byte) error { + var rawStr string + if err := json.Unmarshal(data, &rawStr); err != nil { + return newError("invalid address: ", string(data)).Base(err) + } + v.Address = net.ParseAddress(rawStr) + + return nil +} + +func (v *Address) Build() *net.IPOrDomain { + return net.NewIPOrDomain(v.Address) +} + +type Network string + +func (v Network) Build() net.Network { + switch strings.ToLower(string(v)) { + case "tcp": + return net.Network_TCP + case "udp": + return net.Network_UDP + case "unix": + return net.Network_UNIX + default: + return net.Network_Unknown + } +} + +type NetworkList []Network + +func (v *NetworkList) UnmarshalJSON(data []byte) error { + var strarray []Network + if err := json.Unmarshal(data, &strarray); err == nil { + nl := NetworkList(strarray) + *v = nl + return nil + } + + var rawstr Network + if err := json.Unmarshal(data, &rawstr); err == nil { + strlist := strings.Split(string(rawstr), ",") + nl := make([]Network, len(strlist)) + for idx, network := range strlist { + nl[idx] = Network(network) + } + *v = nl + return nil + } + return newError("unknown format of a string list: " + string(data)) +} + +func (v *NetworkList) Build() []net.Network { + if v == nil { + return []net.Network{net.Network_TCP} + } + + list := make([]net.Network, 0, len(*v)) + for _, network := range *v { + list = append(list, network.Build()) + } + return list +} + +func parseIntPort(data []byte) (net.Port, error) { + var intPort uint32 + err := json.Unmarshal(data, &intPort) + if err != nil { + return net.Port(0), err + } + return net.PortFromInt(intPort) +} + +func parseStringPort(s string) (net.Port, net.Port, error) { + if strings.HasPrefix(s, "env:") { + s = s[4:] + s = os.Getenv(s) + } + + pair := strings.SplitN(s, "-", 2) + if len(pair) == 0 { + return net.Port(0), net.Port(0), newError("invalid port range: ", s) + } + if len(pair) == 1 { + port, err := net.PortFromString(pair[0]) + return port, port, err + } + + fromPort, err := net.PortFromString(pair[0]) + if err != nil { + return net.Port(0), net.Port(0), err + } + toPort, err := net.PortFromString(pair[1]) + if err != nil { + return net.Port(0), net.Port(0), err + } + return fromPort, toPort, nil +} + +func parseJSONStringPort(data []byte) (net.Port, net.Port, error) { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return net.Port(0), net.Port(0), err + } + return parseStringPort(s) +} + +type PortRange struct { + From uint32 + To uint32 +} + +func (v *PortRange) Build() *net.PortRange { + return &net.PortRange{ + From: v.From, + To: v.To, + } +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (v *PortRange) UnmarshalJSON(data []byte) error { + port, err := parseIntPort(data) + if err == nil { + v.From = uint32(port) + v.To = uint32(port) + return nil + } + + from, to, err := parseJSONStringPort(data) + if err == nil { + v.From = uint32(from) + v.To = uint32(to) + if v.From > v.To { + return newError("invalid port range ", v.From, " -> ", v.To) + } + return nil + } + + return newError("invalid port range: ", string(data)) +} + +type PortList struct { + Range []PortRange +} + +func (list *PortList) Build() *net.PortList { + portList := new(net.PortList) + for _, r := range list.Range { + portList.Range = append(portList.Range, r.Build()) + } + return portList +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (list *PortList) UnmarshalJSON(data []byte) error { + var listStr string + var number uint32 + if err := json.Unmarshal(data, &listStr); err != nil { + if err2 := json.Unmarshal(data, &number); err2 != nil { + return newError("invalid port: ", string(data)).Base(err2) + } + } + rangelist := strings.Split(listStr, ",") + for _, rangeStr := range rangelist { + trimmed := strings.TrimSpace(rangeStr) + if len(trimmed) > 0 { + if strings.Contains(trimmed, "-") { + from, to, err := parseStringPort(trimmed) + if err != nil { + return newError("invalid port range: ", trimmed).Base(err) + } + list.Range = append(list.Range, PortRange{From: uint32(from), To: uint32(to)}) + } else { + port, err := parseIntPort([]byte(trimmed)) + if err != nil { + return newError("invalid port: ", trimmed).Base(err) + } + list.Range = append(list.Range, PortRange{From: uint32(port), To: uint32(port)}) + } + } + } + if number != 0 { + list.Range = append(list.Range, PortRange{From: number, To: number}) + } + return nil +} + +type User struct { + EmailString string `json:"email"` + LevelByte byte `json:"level"` +} + +func (v *User) Build() *protocol.User { + return &protocol.User{ + Email: v.EmailString, + Level: uint32(v.LevelByte), + } +} diff --git a/infra/conf/common_test.go b/infra/conf/common_test.go new file mode 100644 index 00000000..325a1713 --- /dev/null +++ b/infra/conf/common_test.go @@ -0,0 +1,231 @@ +package conf_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + . "github.com/xtls/xray-core/v1/infra/conf" +) + +func TestStringListUnmarshalError(t *testing.T) { + rawJSON := `1234` + list := new(StringList) + err := json.Unmarshal([]byte(rawJSON), list) + if err == nil { + t.Error("expected error, but got nil") + } +} + +func TestStringListLen(t *testing.T) { + rawJSON := `"a, b, c, d"` + var list StringList + err := json.Unmarshal([]byte(rawJSON), &list) + common.Must(err) + if r := cmp.Diff([]string(list), []string{"a", " b", " c", " d"}); r != "" { + t.Error(r) + } +} + +func TestIPParsing(t *testing.T) { + rawJSON := "\"8.8.8.8\"" + var address Address + err := json.Unmarshal([]byte(rawJSON), &address) + common.Must(err) + if r := cmp.Diff(address.IP(), net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} + +func TestDomainParsing(t *testing.T) { + rawJSON := "\"example.com\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "example.com" { + t.Error("domain: ", address.Domain()) + } +} + +func TestURLParsing(t *testing.T) { + { + rawJSON := "\"https://dns.google/dns-query\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "https://dns.google/dns-query" { + t.Error("URL: ", address.Domain()) + } + } + { + rawJSON := "\"https+local://dns.google/dns-query\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "https+local://dns.google/dns-query" { + t.Error("URL: ", address.Domain()) + } + } +} + +func TestInvalidAddressJson(t *testing.T) { + rawJSON := "1234" + var address Address + err := json.Unmarshal([]byte(rawJSON), &address) + if err == nil { + t.Error("nil error") + } +} + +func TestStringNetwork(t *testing.T) { + var network Network + common.Must(json.Unmarshal([]byte(`"tcp"`), &network)) + if v := network.Build(); v != net.Network_TCP { + t.Error("network: ", v) + } +} + +func TestArrayNetworkList(t *testing.T) { + var list NetworkList + common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list)) + + nlist := list.Build() + if !net.HasNetwork(nlist, net.Network_TCP) { + t.Error("no tcp network") + } + if net.HasNetwork(nlist, net.Network_UDP) { + t.Error("has udp network") + } +} + +func TestStringNetworkList(t *testing.T) { + var list NetworkList + common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list)) + + nlist := list.Build() + if !net.HasNetwork(nlist, net.Network_TCP) { + t.Error("no tcp network") + } + if net.HasNetwork(nlist, net.Network_UDP) { + t.Error("has udp network") + } +} + +func TestInvalidNetworkJson(t *testing.T) { + var list NetworkList + err := json.Unmarshal([]byte("0"), &list) + if err == nil { + t.Error("nil error") + } +} + +func TestIntPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("1234"), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestOverRangeIntPort(t *testing.T) { + var portRange PortRange + err := json.Unmarshal([]byte("70000"), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("-1"), &portRange) + if err == nil { + t.Error("nil error") + } +} + +func TestEnvPort(t *testing.T) { + common.Must(os.Setenv("PORT", "1234")) + + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"env:PORT\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestSingleStringPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"1234\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestStringPairPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"1234-5678\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 5678, + }); r != "" { + t.Error(r) + } +} + +func TestOverRangeStringPort(t *testing.T) { + var portRange PortRange + err := json.Unmarshal([]byte("\"65536\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"70000-80000\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"1-90000\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"700-600\""), &portRange) + if err == nil { + t.Error("nil error") + } +} + +func TestUserParsing(t *testing.T) { + user := new(User) + common.Must(json.Unmarshal([]byte(`{ + "id": "96edb838-6d68-42ef-a933-25f7ac3a9d09", + "email": "love@example.com", + "level": 1, + "alterId": 100 + }`), user)) + + nUser := user.Build() + if r := cmp.Diff(nUser, &protocol.User{ + Level: 1, + Email: "love@example.com", + }, cmpopts.IgnoreUnexported(protocol.User{})); r != "" { + t.Error(r) + } +} + +func TestInvalidUserJson(t *testing.T) { + user := new(User) + err := json.Unmarshal([]byte(`{"email": 1234}`), user) + if err == nil { + t.Error("nil error") + } +} diff --git a/infra/conf/conf.go b/infra/conf/conf.go new file mode 100644 index 00000000..d31fec57 --- /dev/null +++ b/infra/conf/conf.go @@ -0,0 +1,3 @@ +package conf + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/infra/conf/dns.go b/infra/conf/dns.go new file mode 100644 index 00000000..5a2e886c --- /dev/null +++ b/infra/conf/dns.go @@ -0,0 +1,240 @@ +package conf + +import ( + "encoding/json" + "sort" + "strings" + + "github.com/xtls/xray-core/v1/app/dns" + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/common/net" +) + +type NameServerConfig struct { + Address *Address + Port uint16 + Domains []string + ExpectIPs StringList +} + +func (c *NameServerConfig) UnmarshalJSON(data []byte) error { + var address Address + if err := json.Unmarshal(data, &address); err == nil { + c.Address = &address + return nil + } + + var advanced struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Domains []string `json:"domains"` + ExpectIPs StringList `json:"expectIps"` + } + if err := json.Unmarshal(data, &advanced); err == nil { + c.Address = advanced.Address + c.Port = advanced.Port + c.Domains = advanced.Domains + c.ExpectIPs = advanced.ExpectIPs + return nil + } + + return newError("failed to parse name server: ", string(data)) +} + +func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType { + switch t { + case router.Domain_Domain: + return dns.DomainMatchingType_Subdomain + case router.Domain_Full: + return dns.DomainMatchingType_Full + case router.Domain_Plain: + return dns.DomainMatchingType_Keyword + case router.Domain_Regex: + return dns.DomainMatchingType_Regex + default: + panic("unknown domain type") + } +} + +func (c *NameServerConfig) Build() (*dns.NameServer, error) { + if c.Address == nil { + return nil, newError("NameServer address is not specified.") + } + + var domains []*dns.NameServer_PriorityDomain + var originalRules []*dns.NameServer_OriginalRule + + for _, rule := range c.Domains { + parsedDomain, err := parseDomainRule(rule) + if err != nil { + return nil, newError("invalid domain rule: ", rule).Base(err) + } + + for _, pd := range parsedDomain { + domains = append(domains, &dns.NameServer_PriorityDomain{ + Type: toDomainMatchingType(pd.Type), + Domain: pd.Value, + }) + } + originalRules = append(originalRules, &dns.NameServer_OriginalRule{ + Rule: rule, + Size: uint32(len(parsedDomain)), + }) + } + + geoipList, err := toCidrList(c.ExpectIPs) + if err != nil { + return nil, newError("invalid ip rule: ", c.ExpectIPs).Base(err) + } + + return &dns.NameServer{ + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: c.Address.Build(), + Port: uint32(c.Port), + }, + PrioritizedDomain: domains, + Geoip: geoipList, + OriginalRules: originalRules, + }, nil +} + +var typeMap = map[router.Domain_Type]dns.DomainMatchingType{ + router.Domain_Full: dns.DomainMatchingType_Full, + router.Domain_Domain: dns.DomainMatchingType_Subdomain, + router.Domain_Plain: dns.DomainMatchingType_Keyword, + router.Domain_Regex: dns.DomainMatchingType_Regex, +} + +// DNSConfig is a JSON serializable object for dns.Config. +type DNSConfig struct { + Servers []*NameServerConfig `json:"servers"` + Hosts map[string]*Address `json:"hosts"` + ClientIP *Address `json:"clientIp"` + Tag string `json:"tag"` +} + +func getHostMapping(addr *Address) *dns.Config_HostMapping { + if addr.Family().IsIP() { + return &dns.Config_HostMapping{ + Ip: [][]byte{[]byte(addr.IP())}, + } + } else { + return &dns.Config_HostMapping{ + ProxiedDomain: addr.Domain(), + } + } +} + +// Build implements Buildable +func (c *DNSConfig) Build() (*dns.Config, error) { + config := &dns.Config{ + Tag: c.Tag, + } + + if c.ClientIP != nil { + if !c.ClientIP.Family().IsIP() { + return nil, newError("not an IP address:", c.ClientIP.String()) + } + config.ClientIp = []byte(c.ClientIP.IP()) + } + + for _, server := range c.Servers { + ns, err := server.Build() + if err != nil { + return nil, newError("failed to build name server").Base(err) + } + config.NameServer = append(config.NameServer, ns) + } + + if c.Hosts != nil && len(c.Hosts) > 0 { + domains := make([]string, 0, len(c.Hosts)) + for domain := range c.Hosts { + domains = append(domains, domain) + } + sort.Strings(domains) + + for _, domain := range domains { + addr := c.Hosts[domain] + var mappings []*dns.Config_HostMapping + switch { + case strings.HasPrefix(domain, "domain:"): + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Subdomain + mapping.Domain = domain[7:] + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "geosite:"): + domains, err := loadGeositeWithAttr("geosite.dat", strings.ToUpper(domain[8:])) + if err != nil { + return nil, newError("invalid geosite settings: ", domain).Base(err) + } + for _, d := range domains { + mapping := getHostMapping(addr) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + case strings.HasPrefix(domain, "regexp:"): + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Regex + mapping.Domain = domain[7:] + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "keyword:"): + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Keyword + mapping.Domain = domain[8:] + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "full:"): + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = domain[5:] + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "dotless:"): + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Regex + switch substr := domain[8:]; { + case substr == "": + mapping.Domain = "^[^.]*$" + case !strings.Contains(substr, "."): + mapping.Domain = "^[^.]*" + substr + "[^.]*$" + default: + return nil, newError("substr in dotless rule should not contain a dot: ", substr) + } + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "ext:"): + kv := strings.Split(domain[4:], ":") + if len(kv) != 2 { + return nil, newError("invalid external resource: ", domain) + } + filename := kv[0] + country := kv[1] + domains, err := loadGeositeWithAttr(filename, country) + if err != nil { + return nil, newError("failed to load domains: ", country, " from ", filename).Base(err) + } + for _, d := range domains { + mapping := getHostMapping(addr) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + default: + mapping := getHostMapping(addr) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = domain + mappings = append(mappings, mapping) + } + + config.StaticHosts = append(config.StaticHosts, mappings...) + } + } + + return config, nil +} diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go new file mode 100644 index 00000000..6b53317f --- /dev/null +++ b/infra/conf/dns_proxy.go @@ -0,0 +1,26 @@ +package conf + +import ( + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/proxy/dns" +) + +type DNSOutboundConfig struct { + Network Network `json:"network"` + Address *Address `json:"address"` + Port uint16 `json:"port"` +} + +func (c *DNSOutboundConfig) Build() (proto.Message, error) { + config := &dns.Config{ + Server: &net.Endpoint{ + Network: c.Network.Build(), + Port: uint32(c.Port), + }, + } + if c.Address != nil { + config.Server.Address = c.Address.Build() + } + return config, nil +} diff --git a/infra/conf/dns_proxy_test.go b/infra/conf/dns_proxy_test.go new file mode 100644 index 00000000..04699a91 --- /dev/null +++ b/infra/conf/dns_proxy_test.go @@ -0,0 +1,33 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/dns" +) + +func TestDnsProxyConfig(t *testing.T) { + creator := func() Buildable { + return new(DNSOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "address": "8.8.8.8", + "port": 53, + "network": "tcp" + }`, + Parser: loadJSON(creator), + Output: &dns.Config{ + Server: &net.Endpoint{ + Network: net.Network_TCP, + Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})), + Port: 53, + }, + }, + }, + }) +} diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go new file mode 100644 index 00000000..bb5f5c73 --- /dev/null +++ b/infra/conf/dns_test.go @@ -0,0 +1,140 @@ +package conf_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/app/dns" + "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" + . "github.com/xtls/xray-core/v1/infra/conf" +) + +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"))) + } + + geositeFilePath := filepath.Join(wd, "geosite.dat") + os.Setenv("xray.location.asset", wd) + geositeFile, err := os.OpenFile(geositeFilePath, os.O_CREATE|os.O_WRONLY, 0600) + common.Must(err) + defer geositeFile.Close() + + list := &router.GeoSiteList{ + Entry: []*router.GeoSite{ + { + CountryCode: "TEST", + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "example.com"}, + }, + }, + }, + } + + listBytes, err := proto.Marshal(list) + common.Must(err) + common.Must2(geositeFile.Write(listBytes)) +} +func TestDNSConfigParsing(t *testing.T) { + geositePath := platform.GetAssetLocation("geosite.dat") + defer func() { + os.Remove(geositePath) + os.Unsetenv("xray.location.asset") + }() + + parserCreator := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(DNSConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "servers": [{ + "address": "8.8.8.8", + "port": 5353, + "domains": ["domain:example.com"] + }], + "hosts": { + "example.com": "127.0.0.1", + "domain:example.com": "google.com", + "geosite:test": "10.0.0.1", + "keyword:google": "8.8.8.8", + "regexp:.*\\.com": "8.8.4.4" + }, + "clientIp": "10.0.0.1" + }`, + Parser: parserCreator(), + Output: &dns.Config{ + NameServer: []*dns.NameServer{ + { + Address: &net.Endpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{8, 8, 8, 8}, + }, + }, + Network: net.Network_UDP, + Port: 5353, + }, + PrioritizedDomain: []*dns.NameServer_PriorityDomain{ + { + Type: dns.DomainMatchingType_Subdomain, + Domain: "example.com", + }, + }, + OriginalRules: []*dns.NameServer_OriginalRule{ + { + Rule: "domain:example.com", + Size: 1, + }, + }, + }, + }, + StaticHosts: []*dns.Config_HostMapping{ + { + Type: dns.DomainMatchingType_Subdomain, + Domain: "example.com", + ProxiedDomain: "google.com", + }, + { + Type: dns.DomainMatchingType_Full, + Domain: "example.com", + Ip: [][]byte{{10, 0, 0, 1}}, + }, + { + Type: dns.DomainMatchingType_Keyword, + Domain: "google", + Ip: [][]byte{{8, 8, 8, 8}}, + }, + { + Type: dns.DomainMatchingType_Regex, + Domain: ".*\\.com", + Ip: [][]byte{{8, 8, 4, 4}}, + }, + { + Type: dns.DomainMatchingType_Full, + Domain: "example.com", + Ip: [][]byte{{127, 0, 0, 1}}, + }, + }, + ClientIp: []byte{10, 0, 0, 1}, + }, + }, + }) +} diff --git a/infra/conf/dokodemo.go b/infra/conf/dokodemo.go new file mode 100644 index 00000000..5a9a3665 --- /dev/null +++ b/infra/conf/dokodemo.go @@ -0,0 +1,28 @@ +package conf + +import ( + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/proxy/dokodemo" +) + +type DokodemoConfig struct { + Host *Address `json:"address"` + PortValue uint16 `json:"port"` + NetworkList *NetworkList `json:"network"` + TimeoutValue uint32 `json:"timeout"` + Redirect bool `json:"followRedirect"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *DokodemoConfig) Build() (proto.Message, error) { + config := new(dokodemo.Config) + if v.Host != nil { + config.Address = v.Host.Build() + } + config.Port = uint32(v.PortValue) + config.Networks = v.NetworkList.Build() + config.Timeout = v.TimeoutValue + config.FollowRedirect = v.Redirect + config.UserLevel = v.UserLevel + return config, nil +} diff --git a/infra/conf/dokodemo_test.go b/infra/conf/dokodemo_test.go new file mode 100644 index 00000000..8e3b5632 --- /dev/null +++ b/infra/conf/dokodemo_test.go @@ -0,0 +1,41 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/dokodemo" +) + +func TestDokodemoConfig(t *testing.T) { + creator := func() Buildable { + return new(DokodemoConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "address": "8.8.8.8", + "port": 53, + "network": "tcp", + "timeout": 10, + "followRedirect": true, + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &dokodemo.Config{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{8, 8, 8, 8}, + }, + }, + Port: 53, + Networks: []net.Network{net.Network_TCP}, + Timeout: 10, + FollowRedirect: true, + UserLevel: 1, + }, + }, + }) +} diff --git a/infra/conf/errors.generated.go b/infra/conf/errors.generated.go new file mode 100644 index 00000000..47ff06bf --- /dev/null +++ b/infra/conf/errors.generated.go @@ -0,0 +1,9 @@ +package conf + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/infra/conf/freedom.go b/infra/conf/freedom.go new file mode 100644 index 00000000..15a377c1 --- /dev/null +++ b/infra/conf/freedom.go @@ -0,0 +1,57 @@ +package conf + +import ( + "net" + "strings" + + "github.com/golang/protobuf/proto" + v2net "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/proxy/freedom" +) + +type FreedomConfig struct { + DomainStrategy string `json:"domainStrategy"` + Timeout *uint32 `json:"timeout"` + Redirect string `json:"redirect"` + UserLevel uint32 `json:"userLevel"` +} + +// Build implements Buildable +func (c *FreedomConfig) Build() (proto.Message, error) { + config := new(freedom.Config) + config.DomainStrategy = freedom.Config_AS_IS + switch strings.ToLower(c.DomainStrategy) { + case "useip", "use_ip": + config.DomainStrategy = freedom.Config_USE_IP + case "useip4", "useipv4", "use_ipv4", "use_ip_v4", "use_ip4": + config.DomainStrategy = freedom.Config_USE_IP4 + case "useip6", "useipv6", "use_ipv6", "use_ip_v6", "use_ip6": + config.DomainStrategy = freedom.Config_USE_IP6 + } + + if c.Timeout != nil { + config.Timeout = *c.Timeout + } + config.UserLevel = c.UserLevel + if len(c.Redirect) > 0 { + host, portStr, err := net.SplitHostPort(c.Redirect) + if err != nil { + return nil, newError("invalid redirect address: ", c.Redirect, ": ", err).Base(err) + } + port, err := v2net.PortFromString(portStr) + if err != nil { + return nil, newError("invalid redirect port: ", c.Redirect, ": ", err).Base(err) + } + config.DestinationOverride = &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Port: uint32(port), + }, + } + + if len(host) > 0 { + config.DestinationOverride.Server.Address = v2net.NewIPOrDomain(v2net.ParseAddress(host)) + } + } + return config, nil +} diff --git a/infra/conf/freedom_test.go b/infra/conf/freedom_test.go new file mode 100644 index 00000000..ed501eb6 --- /dev/null +++ b/infra/conf/freedom_test.go @@ -0,0 +1,43 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/freedom" +) + +func TestFreedomConfig(t *testing.T) { + creator := func() Buildable { + return new(FreedomConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "domainStrategy": "AsIs", + "timeout": 10, + "redirect": "127.0.0.1:3366", + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &freedom.Config{ + DomainStrategy: freedom.Config_AS_IS, + Timeout: 10, + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 3366, + }, + }, + UserLevel: 1, + }, + }, + }) +} diff --git a/infra/conf/general_test.go b/infra/conf/general_test.go new file mode 100644 index 00000000..d537e615 --- /dev/null +++ b/infra/conf/general_test.go @@ -0,0 +1,36 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/infra/conf" +) + +func loadJSON(creator func() Buildable) func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + instance := creator() + if err := json.Unmarshal([]byte(s), instance); err != nil { + return nil, err + } + return instance.Build() + } +} + +type TestCase struct { + Input string + Parser func(string) (proto.Message, error) + Output proto.Message +} + +func runMultiTestCase(t *testing.T, testCases []TestCase) { + for _, testCase := range testCases { + actual, err := testCase.Parser(testCase.Input) + common.Must(err) + if !proto.Equal(actual, testCase.Output) { + t.Fatalf("Failed in test case:\n%s\nActual:\n%v\nExpected:\n%v", testCase.Input, actual, testCase.Output) + } + } +} diff --git a/infra/conf/http.go b/infra/conf/http.go new file mode 100644 index 00000000..ad47d6e3 --- /dev/null +++ b/infra/conf/http.go @@ -0,0 +1,80 @@ +package conf + +import ( + "encoding/json" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/http" +) + +type HTTPAccount struct { + Username string `json:"user"` + Password string `json:"pass"` +} + +func (v *HTTPAccount) Build() *http.Account { + return &http.Account{ + Username: v.Username, + Password: v.Password, + } +} + +type HTTPServerConfig struct { + Timeout uint32 `json:"timeout"` + Accounts []*HTTPAccount `json:"accounts"` + Transparent bool `json:"allowTransparent"` + UserLevel uint32 `json:"userLevel"` +} + +func (c *HTTPServerConfig) Build() (proto.Message, error) { + config := &http.ServerConfig{ + Timeout: c.Timeout, + AllowTransparent: c.Transparent, + UserLevel: c.UserLevel, + } + + if len(c.Accounts) > 0 { + config.Accounts = make(map[string]string) + for _, account := range c.Accounts { + config.Accounts[account.Username] = account.Password + } + } + + return config, nil +} + +type HTTPRemoteConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} +type HTTPClientConfig struct { + Servers []*HTTPRemoteConfig `json:"servers"` +} + +func (v *HTTPClientConfig) Build() (proto.Message, error) { + config := new(http.ClientConfig) + config.Server = make([]*protocol.ServerEndpoint, len(v.Servers)) + for idx, serverConfig := range v.Servers { + server := &protocol.ServerEndpoint{ + Address: serverConfig.Address.Build(), + Port: uint32(serverConfig.Port), + } + for _, rawUser := range serverConfig.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, newError("failed to parse HTTP user").Base(err).AtError() + } + account := new(HTTPAccount) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, newError("failed to parse HTTP account").Base(err).AtError() + } + user.Account = serial.ToTypedMessage(account.Build()) + server.User = append(server.User, user) + } + config.Server[idx] = server + } + return config, nil +} diff --git a/infra/conf/http_test.go b/infra/conf/http_test.go new file mode 100644 index 00000000..fd7d9559 --- /dev/null +++ b/infra/conf/http_test.go @@ -0,0 +1,39 @@ +package conf_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/http" +) + +func TestHTTPServerConfig(t *testing.T) { + creator := func() Buildable { + return new(HTTPServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "timeout": 10, + "accounts": [ + { + "user": "my-username", + "pass": "my-password" + } + ], + "allowTransparent": true, + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &http.ServerConfig{ + Accounts: map[string]string{ + "my-username": "my-password", + }, + AllowTransparent: true, + UserLevel: 1, + Timeout: 10, + }, + }, + }) +} diff --git a/infra/conf/json/reader.go b/infra/conf/json/reader.go new file mode 100644 index 00000000..faa01baa --- /dev/null +++ b/infra/conf/json/reader.go @@ -0,0 +1,133 @@ +package json + +import ( + "io" + + "github.com/xtls/xray-core/v1/common/buf" +) + +// State is the internal state of parser. +type State byte + +const ( + StateContent State = iota + StateEscape + StateDoubleQuote + StateDoubleQuoteEscape + StateSingleQuote + StateSingleQuoteEscape + StateComment + StateSlash + StateMultilineComment + StateMultilineCommentStar +) + +// Reader is a reader for filtering comments. +// It supports Java style single and multi line comment syntax, and Python style single line comment syntax. +type Reader struct { + io.Reader + + state State + br *buf.BufferedReader +} + +// Read implements io.Reader.Read(). Buffer must be at least 3 bytes. +func (v *Reader) Read(b []byte) (int, error) { + if v.br == nil { + v.br = &buf.BufferedReader{Reader: buf.NewReader(v.Reader)} + } + + p := b[:0] + for len(p) < len(b)-2 { + x, err := v.br.ReadByte() + if err != nil { + if len(p) == 0 { + return 0, err + } + return len(p), nil + } + switch v.state { + case StateContent: + switch x { + case '"': + v.state = StateDoubleQuote + p = append(p, x) + case '\'': + v.state = StateSingleQuote + p = append(p, x) + case '\\': + v.state = StateEscape + case '#': + v.state = StateComment + case '/': + v.state = StateSlash + default: + p = append(p, x) + } + case StateEscape: + p = append(p, '\\', x) + v.state = StateContent + case StateDoubleQuote: + switch x { + case '"': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateDoubleQuoteEscape + default: + p = append(p, x) + } + case StateDoubleQuoteEscape: + p = append(p, '\\', x) + v.state = StateDoubleQuote + case StateSingleQuote: + switch x { + case '\'': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateSingleQuoteEscape + default: + p = append(p, x) + } + case StateSingleQuoteEscape: + p = append(p, '\\', x) + v.state = StateSingleQuote + case StateComment: + if x == '\n' { + v.state = StateContent + p = append(p, '\n') + } + case StateSlash: + switch x { + case '/': + v.state = StateComment + case '*': + v.state = StateMultilineComment + default: + p = append(p, '/', x) + } + case StateMultilineComment: + switch x { + case '*': + v.state = StateMultilineCommentStar + case '\n': + p = append(p, '\n') + } + case StateMultilineCommentStar: + switch x { + case '/': + v.state = StateContent + case '*': + // Stay + case '\n': + p = append(p, '\n') + default: + v.state = StateMultilineComment + } + default: + panic("Unknown state.") + } + } + return len(p), nil +} diff --git a/infra/conf/json/reader_test.go b/infra/conf/json/reader_test.go new file mode 100644 index 00000000..05890a55 --- /dev/null +++ b/infra/conf/json/reader_test.go @@ -0,0 +1,96 @@ +package json_test + +import ( + "bytes" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/infra/conf/json" +) + +func TestReader(t *testing.T) { + data := []struct { + input string + output string + }{ + { + ` +content #comment 1 +#comment 2 +content 2`, + ` +content + +content 2`}, + {`content`, `content`}, + {" ", " "}, + {`con/*abcd*/tent`, "content"}, + {` +text // adlkhdf /* +//comment adfkj +text 2*/`, ` +text + +text 2*`}, + {`"//"content`, `"//"content`}, + {`abcd'//'abcd`, `abcd'//'abcd`}, + {`"\""`, `"\""`}, + {`\"/*abcd*/\"`, `\"\"`}, + } + + for _, testCase := range data { + reader := &Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + + actual := make([]byte, 1024) + n, err := reader.Read(actual) + common.Must(err) + if r := cmp.Diff(string(actual[:n]), testCase.output); r != "" { + t.Error(r) + } + } +} + +func TestReader1(t *testing.T) { + type dataStruct struct { + input string + output string + } + + bufLen := 8 + + data := []dataStruct{ + {"loooooooooooooooooooooooooooooooooooooooog", "loooooooooooooooooooooooooooooooooooooooog"}, + {`{"t": "\/testlooooooooooooooooooooooooooooong"}`, `{"t": "\/testlooooooooooooooooooooooooooooong"}`}, + {`{"t": "\/test"}`, `{"t": "\/test"}`}, + {`"\// fake comment"`, `"\// fake comment"`}, + {`"\/\/\/\/\/"`, `"\/\/\/\/\/"`}, + } + + for _, testCase := range data { + reader := &Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + target := make([]byte, 0) + buf := make([]byte, bufLen) + var n int + var err error + for n, err = reader.Read(buf); err == nil; n, err = reader.Read(buf) { + if n > len(buf) { + t.Error("n: ", n) + } + target = append(target, buf[:n]...) + buf = make([]byte, bufLen) + } + if err != nil && err != io.EOF { + t.Error("error: ", err) + } + if string(target) != testCase.output { + t.Error("got ", string(target), " want ", testCase.output) + } + } +} diff --git a/infra/conf/loader.go b/infra/conf/loader.go new file mode 100644 index 00000000..51f268a3 --- /dev/null +++ b/infra/conf/loader.go @@ -0,0 +1,83 @@ +package conf + +import ( + "encoding/json" + "strings" +) + +type ConfigCreator func() interface{} + +type ConfigCreatorCache map[string]ConfigCreator + +func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error { + if _, found := v[id]; found { + return newError(id, " already registered.").AtError() + } + + v[id] = creator + return nil +} + +func (v ConfigCreatorCache) CreateConfig(id string) (interface{}, error) { + creator, found := v[id] + if !found { + return nil, newError("unknown config id: ", id) + } + return creator(), nil +} + +type JSONConfigLoader struct { + cache ConfigCreatorCache + idKey string + configKey string +} + +func NewJSONConfigLoader(cache ConfigCreatorCache, idKey string, configKey string) *JSONConfigLoader { + return &JSONConfigLoader{ + idKey: idKey, + configKey: configKey, + cache: cache, + } +} + +func (v *JSONConfigLoader) LoadWithID(raw []byte, id string) (interface{}, error) { + id = strings.ToLower(id) + config, err := v.cache.CreateConfig(id) + if err != nil { + return nil, err + } + if err := json.Unmarshal(raw, config); err != nil { + return nil, err + } + return config, nil +} + +func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) { + var obj map[string]json.RawMessage + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, "", err + } + rawID, found := obj[v.idKey] + if !found { + return nil, "", newError(v.idKey, " not found in JSON context").AtError() + } + var id string + if err := json.Unmarshal(rawID, &id); err != nil { + return nil, "", err + } + rawConfig := json.RawMessage(raw) + if len(v.configKey) > 0 { + configValue, found := obj[v.configKey] + if found { + rawConfig = configValue + } else { + // Default to empty json object. + rawConfig = json.RawMessage([]byte("{}")) + } + } + config, err := v.LoadWithID([]byte(rawConfig), id) + if err != nil { + return nil, id, err + } + return config, id, nil +} diff --git a/infra/conf/log.go b/infra/conf/log.go new file mode 100644 index 00000000..00a02ba8 --- /dev/null +++ b/infra/conf/log.go @@ -0,0 +1,61 @@ +package conf + +import ( + "strings" + + "github.com/xtls/xray-core/v1/app/log" + clog "github.com/xtls/xray-core/v1/common/log" +) + +func DefaultLogConfig() *log.Config { + return &log.Config{ + AccessLogType: log.LogType_None, + ErrorLogType: log.LogType_Console, + ErrorLogLevel: clog.Severity_Warning, + } +} + +type LogConfig struct { + AccessLog string `json:"access"` + ErrorLog string `json:"error"` + LogLevel string `json:"loglevel"` +} + +func (v *LogConfig) Build() *log.Config { + if v == nil { + return nil + } + config := &log.Config{ + ErrorLogType: log.LogType_Console, + AccessLogType: log.LogType_Console, + } + + if v.AccessLog == "none" { + config.AccessLogType = log.LogType_None + } else if len(v.AccessLog) > 0 { + config.AccessLogPath = v.AccessLog + config.AccessLogType = log.LogType_File + } + if v.ErrorLog == "none" { + config.ErrorLogType = log.LogType_None + } else if len(v.ErrorLog) > 0 { + config.ErrorLogPath = v.ErrorLog + config.ErrorLogType = log.LogType_File + } + + level := strings.ToLower(v.LogLevel) + switch level { + case "debug": + config.ErrorLogLevel = clog.Severity_Debug + case "info": + config.ErrorLogLevel = clog.Severity_Info + case "error": + config.ErrorLogLevel = clog.Severity_Error + case "none": + config.ErrorLogType = log.LogType_None + config.AccessLogType = log.LogType_None + default: + config.ErrorLogLevel = clog.Severity_Warning + } + return config +} diff --git a/infra/conf/mtproto.go b/infra/conf/mtproto.go new file mode 100644 index 00000000..07229b5c --- /dev/null +++ b/infra/conf/mtproto.go @@ -0,0 +1,69 @@ +package conf + +import ( + "encoding/hex" + "encoding/json" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/mtproto" +) + +type MTProtoAccount struct { + Secret string `json:"secret"` +} + +// Build implements Buildable +func (a *MTProtoAccount) Build() (*mtproto.Account, error) { + if len(a.Secret) != 32 { + return nil, newError("MTProto secret must have 32 chars") + } + secret, err := hex.DecodeString(a.Secret) + if err != nil { + return nil, newError("failed to decode secret: ", a.Secret).Base(err) + } + return &mtproto.Account{ + Secret: secret, + }, nil +} + +type MTProtoServerConfig struct { + Users []json.RawMessage `json:"users"` +} + +func (c *MTProtoServerConfig) Build() (proto.Message, error) { + config := &mtproto.ServerConfig{} + + if len(c.Users) == 0 { + return nil, newError("zero MTProto users configured.") + } + config.User = make([]*protocol.User, len(c.Users)) + for idx, rawData := range c.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawData, user); err != nil { + return nil, newError("invalid MTProto user").Base(err) + } + account := new(MTProtoAccount) + if err := json.Unmarshal(rawData, account); err != nil { + return nil, newError("invalid MTProto user").Base(err) + } + accountProto, err := account.Build() + if err != nil { + return nil, newError("failed to parse MTProto user").Base(err) + } + user.Account = serial.ToTypedMessage(accountProto) + config.User[idx] = user + } + + return config, nil +} + +type MTProtoClientConfig struct { +} + +func (c *MTProtoClientConfig) Build() (proto.Message, error) { + config := new(mtproto.ClientConfig) + return config, nil +} diff --git a/infra/conf/mtproto_test.go b/infra/conf/mtproto_test.go new file mode 100644 index 00000000..c7b1d3ae --- /dev/null +++ b/infra/conf/mtproto_test.go @@ -0,0 +1,40 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/mtproto" +) + +func TestMTProtoServerConfig(t *testing.T) { + creator := func() Buildable { + return new(MTProtoServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "users": [{ + "email": "love@example.com", + "level": 1, + "secret": "b0cbcef5a486d9636472ac27f8e11a9d" + }] + }`, + Parser: loadJSON(creator), + Output: &mtproto.ServerConfig{ + User: []*protocol.User{ + { + Email: "love@example.com", + Level: 1, + Account: serial.ToTypedMessage(&mtproto.Account{ + Secret: []byte{176, 203, 206, 245, 164, 134, 217, 99, 100, 114, 172, 39, 248, 225, 26, 157}, + }), + }, + }, + }, + }, + }) +} diff --git a/infra/conf/policy.go b/infra/conf/policy.go new file mode 100644 index 00000000..704cc085 --- /dev/null +++ b/infra/conf/policy.go @@ -0,0 +1,100 @@ +package conf + +import ( + "github.com/xtls/xray-core/v1/app/policy" +) + +type Policy struct { + Handshake *uint32 `json:"handshake"` + ConnectionIdle *uint32 `json:"connIdle"` + UplinkOnly *uint32 `json:"uplinkOnly"` + DownlinkOnly *uint32 `json:"downlinkOnly"` + StatsUserUplink bool `json:"statsUserUplink"` + StatsUserDownlink bool `json:"statsUserDownlink"` + BufferSize *int32 `json:"bufferSize"` +} + +func (t *Policy) Build() (*policy.Policy, error) { + config := new(policy.Policy_Timeout) + if t.Handshake != nil { + config.Handshake = &policy.Second{Value: *t.Handshake} + } + if t.ConnectionIdle != nil { + config.ConnectionIdle = &policy.Second{Value: *t.ConnectionIdle} + } + if t.UplinkOnly != nil { + config.UplinkOnly = &policy.Second{Value: *t.UplinkOnly} + } + if t.DownlinkOnly != nil { + config.DownlinkOnly = &policy.Second{Value: *t.DownlinkOnly} + } + + p := &policy.Policy{ + Timeout: config, + Stats: &policy.Policy_Stats{ + UserUplink: t.StatsUserUplink, + UserDownlink: t.StatsUserDownlink, + }, + } + + if t.BufferSize != nil { + bs := int32(-1) + if *t.BufferSize >= 0 { + bs = (*t.BufferSize) * 1024 + } + p.Buffer = &policy.Policy_Buffer{ + Connection: bs, + } + } + + return p, nil +} + +type SystemPolicy struct { + StatsInboundUplink bool `json:"statsInboundUplink"` + StatsInboundDownlink bool `json:"statsInboundDownlink"` + StatsOutboundUplink bool `json:"statsOutboundUplink"` + StatsOutboundDownlink bool `json:"statsOutboundDownlink"` +} + +func (p *SystemPolicy) Build() (*policy.SystemPolicy, error) { + return &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + InboundUplink: p.StatsInboundUplink, + InboundDownlink: p.StatsInboundDownlink, + OutboundUplink: p.StatsOutboundUplink, + OutboundDownlink: p.StatsOutboundDownlink, + }, + }, nil +} + +type PolicyConfig struct { + Levels map[uint32]*Policy `json:"levels"` + System *SystemPolicy `json:"system"` +} + +func (c *PolicyConfig) Build() (*policy.Config, error) { + levels := make(map[uint32]*policy.Policy) + for l, p := range c.Levels { + if p != nil { + pp, err := p.Build() + if err != nil { + return nil, err + } + levels[l] = pp + } + } + config := &policy.Config{ + Level: levels, + } + + if c.System != nil { + sc, err := c.System.Build() + if err != nil { + return nil, err + } + config.System = sc + } + + return config, nil +} diff --git a/infra/conf/policy_test.go b/infra/conf/policy_test.go new file mode 100644 index 00000000..ca2d3111 --- /dev/null +++ b/infra/conf/policy_test.go @@ -0,0 +1,40 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/infra/conf" +) + +func TestBufferSize(t *testing.T) { + cases := []struct { + Input int32 + Output int32 + }{ + { + Input: 0, + Output: 0, + }, + { + Input: -1, + Output: -1, + }, + { + Input: 1, + Output: 1024, + }, + } + + for _, c := range cases { + bs := c.Input + pConf := Policy{ + BufferSize: &bs, + } + p, err := pConf.Build() + common.Must(err) + if p.Buffer.Connection != c.Output { + t.Error("expected buffer size ", c.Output, " but got ", p.Buffer.Connection) + } + } +} diff --git a/infra/conf/reverse.go b/infra/conf/reverse.go new file mode 100644 index 00000000..4fb78e1d --- /dev/null +++ b/infra/conf/reverse.go @@ -0,0 +1,56 @@ +package conf + +import ( + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/app/reverse" +) + +type BridgeConfig struct { + Tag string `json:"tag"` + Domain string `json:"domain"` +} + +func (c *BridgeConfig) Build() (*reverse.BridgeConfig, error) { + return &reverse.BridgeConfig{ + Tag: c.Tag, + Domain: c.Domain, + }, nil +} + +type PortalConfig struct { + Tag string `json:"tag"` + Domain string `json:"domain"` +} + +func (c *PortalConfig) Build() (*reverse.PortalConfig, error) { + return &reverse.PortalConfig{ + Tag: c.Tag, + Domain: c.Domain, + }, nil +} + +type ReverseConfig struct { + Bridges []BridgeConfig `json:"bridges"` + Portals []PortalConfig `json:"portals"` +} + +func (c *ReverseConfig) Build() (proto.Message, error) { + config := &reverse.Config{} + for _, bconfig := range c.Bridges { + b, err := bconfig.Build() + if err != nil { + return nil, err + } + config.BridgeConfig = append(config.BridgeConfig, b) + } + + for _, pconfig := range c.Portals { + p, err := pconfig.Build() + if err != nil { + return nil, err + } + config.PortalConfig = append(config.PortalConfig, p) + } + + return config, nil +} diff --git a/infra/conf/reverse_test.go b/infra/conf/reverse_test.go new file mode 100644 index 00000000..2eb739e9 --- /dev/null +++ b/infra/conf/reverse_test.go @@ -0,0 +1,45 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/app/reverse" + "github.com/xtls/xray-core/v1/infra/conf" +) + +func TestReverseConfig(t *testing.T) { + creator := func() conf.Buildable { + return new(conf.ReverseConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "bridges": [{ + "tag": "test", + "domain": "test.example.com" + }] + }`, + Parser: loadJSON(creator), + Output: &reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + {Tag: "test", Domain: "test.example.com"}, + }, + }, + }, + { + Input: `{ + "portals": [{ + "tag": "test", + "domain": "test.example.com" + }] + }`, + Parser: loadJSON(creator), + Output: &reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + {Tag: "test", Domain: "test.example.com"}, + }, + }, + }, + }) +} diff --git a/infra/conf/router.go b/infra/conf/router.go new file mode 100644 index 00000000..156d81a4 --- /dev/null +++ b/infra/conf/router.go @@ -0,0 +1,556 @@ +package conf + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/platform/filesystem" +) + +type RouterRulesConfig struct { + RuleList []json.RawMessage `json:"rules"` + DomainStrategy string `json:"domainStrategy"` +} + +type BalancingRule struct { + Tag string `json:"tag"` + Selectors StringList `json:"selector"` +} + +func (r *BalancingRule) Build() (*router.BalancingRule, error) { + if r.Tag == "" { + return nil, newError("empty balancer tag") + } + if len(r.Selectors) == 0 { + return nil, newError("empty selector list") + } + + return &router.BalancingRule{ + Tag: r.Tag, + OutboundSelector: []string(r.Selectors), + }, nil +} + +type RouterConfig struct { + Settings *RouterRulesConfig `json:"settings"` // Deprecated + RuleList []json.RawMessage `json:"rules"` + DomainStrategy *string `json:"domainStrategy"` + Balancers []*BalancingRule `json:"balancers"` +} + +func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { + ds := "" + if c.DomainStrategy != nil { + ds = *c.DomainStrategy + } else if c.Settings != nil { + ds = c.Settings.DomainStrategy + } + + switch strings.ToLower(ds) { + case "alwaysip": + return router.Config_UseIp + case "ipifnonmatch": + return router.Config_IpIfNonMatch + case "ipondemand": + return router.Config_IpOnDemand + default: + return router.Config_AsIs + } +} + +func (c *RouterConfig) Build() (*router.Config, error) { + config := new(router.Config) + config.DomainStrategy = c.getDomainStrategy() + + var rawRuleList []json.RawMessage + if c != nil { + rawRuleList = c.RuleList + if c.Settings != nil { + c.RuleList = append(c.RuleList, c.Settings.RuleList...) + rawRuleList = c.RuleList + } + } + + for _, rawRule := range rawRuleList { + rule, err := ParseRule(rawRule) + if err != nil { + return nil, err + } + config.Rule = append(config.Rule, rule) + } + for _, rawBalancer := range c.Balancers { + balancer, err := rawBalancer.Build() + if err != nil { + return nil, err + } + config.BalancingRule = append(config.BalancingRule, balancer) + } + return config, nil +} + +type RouterRule struct { + Type string `json:"type"` + OutboundTag string `json:"outboundTag"` + BalancerTag string `json:"balancerTag"` +} + +func ParseIP(s string) (*router.CIDR, error) { + var addr, mask string + i := strings.Index(s, "/") + if i < 0 { + addr = s + } else { + addr = s[:i] + mask = s[i+1:] + } + ip := net.ParseAddress(addr) + switch ip.Family() { + case net.AddressFamilyIPv4: + bits := uint32(32) + if len(mask) > 0 { + bits64, err := strconv.ParseUint(mask, 10, 32) + if err != nil { + return nil, newError("invalid network mask for router: ", mask).Base(err) + } + bits = uint32(bits64) + } + if bits > 32 { + return nil, newError("invalid network mask for router: ", bits) + } + return &router.CIDR{ + Ip: []byte(ip.IP()), + Prefix: bits, + }, nil + case net.AddressFamilyIPv6: + bits := uint32(128) + if len(mask) > 0 { + bits64, err := strconv.ParseUint(mask, 10, 32) + if err != nil { + return nil, newError("invalid network mask for router: ", mask).Base(err) + } + bits = uint32(bits64) + } + if bits > 128 { + return nil, newError("invalid network mask for router: ", bits) + } + return &router.CIDR{ + Ip: []byte(ip.IP()), + Prefix: bits, + }, nil + default: + return nil, newError("unsupported address for router: ", s) + } +} + +func loadGeoIP(country string) ([]*router.CIDR, error) { + return loadIP("geoip.dat", country) +} + +func loadIP(filename, country string) ([]*router.CIDR, error) { + geoipBytes, err := filesystem.ReadAsset(filename) + if err != nil { + return nil, newError("failed to open file: ", filename).Base(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 + } + } + + return nil, newError("country not found in ", filename, ": ", country) +} + +func loadSite(filename, country string) ([]*router.Domain, error) { + geositeBytes, err := filesystem.ReadAsset(filename) + if err != nil { + return nil, newError("failed to open file: ", filename).Base(err) + } + var geositeList router.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, newError("list not found in ", filename, ": ", country) +} + +type AttributeMatcher interface { + Match(*router.Domain) bool +} + +type BooleanMatcher string + +func (m BooleanMatcher) Match(domain *router.Domain) bool { + for _, attr := range domain.Attribute { + if attr.Key == string(m) { + return true + } + } + return false +} + +type AttributeList struct { + matcher []AttributeMatcher +} + +func (al *AttributeList) Match(domain *router.Domain) bool { + for _, matcher := range al.matcher { + if !matcher.Match(domain) { + return false + } + } + return true +} + +func (al *AttributeList) IsEmpty() bool { + return len(al.matcher) == 0 +} + +func parseAttrs(attrs []string) *AttributeList { + al := new(AttributeList) + for _, attr := range attrs { + lc := strings.ToLower(attr) + al.matcher = append(al.matcher, BooleanMatcher(lc)) + } + return al +} + +func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { + parts := strings.Split(siteWithAttr, "@") + if len(parts) == 0 { + return nil, newError("empty site") + } + country := strings.ToUpper(parts[0]) + attrs := parseAttrs(parts[1:]) + domains, err := loadSite(file, country) + if err != nil { + return nil, err + } + + if attrs.IsEmpty() { + return domains, nil + } + + filteredDomains := make([]*router.Domain, 0, len(domains)) + for _, domain := range domains { + if attrs.Match(domain) { + filteredDomains = append(filteredDomains, domain) + } + } + + return filteredDomains, nil +} + +func parseDomainRule(domain string) ([]*router.Domain, error) { + if strings.HasPrefix(domain, "geosite:") { + country := strings.ToUpper(domain[8:]) + domains, err := loadGeositeWithAttr("geosite.dat", country) + if err != nil { + return nil, newError("failed to load geosite: ", country).Base(err) + } + return domains, nil + } + var isExtDatFile = 0 + { + const prefix = "ext:" + if strings.HasPrefix(domain, prefix) { + isExtDatFile = len(prefix) + } + const prefixQualified = "ext-domain:" + if strings.HasPrefix(domain, prefixQualified) { + isExtDatFile = len(prefixQualified) + } + } + if isExtDatFile != 0 { + kv := strings.Split(domain[isExtDatFile:], ":") + if len(kv) != 2 { + return nil, newError("invalid external resource: ", domain) + } + filename := kv[0] + country := kv[1] + domains, err := loadGeositeWithAttr(filename, country) + if err != nil { + return nil, newError("failed to load external sites: ", country, " from ", filename).Base(err) + } + return domains, nil + } + + domainRule := new(router.Domain) + switch { + case strings.HasPrefix(domain, "regexp:"): + domainRule.Type = router.Domain_Regex + domainRule.Value = domain[7:] + + case strings.HasPrefix(domain, "domain:"): + domainRule.Type = router.Domain_Domain + domainRule.Value = domain[7:] + + case strings.HasPrefix(domain, "full:"): + domainRule.Type = router.Domain_Full + domainRule.Value = domain[5:] + + case strings.HasPrefix(domain, "keyword:"): + domainRule.Type = router.Domain_Plain + domainRule.Value = domain[8:] + + case strings.HasPrefix(domain, "dotless:"): + domainRule.Type = router.Domain_Regex + switch substr := domain[8:]; { + case substr == "": + domainRule.Value = "^[^.]*$" + case !strings.Contains(substr, "."): + domainRule.Value = "^[^.]*" + substr + "[^.]*$" + default: + return nil, newError("substr in dotless rule should not contain a dot: ", substr) + } + + default: + domainRule.Type = router.Domain_Plain + domainRule.Value = domain + } + return []*router.Domain{domainRule}, nil +} + +func toCidrList(ips StringList) ([]*router.GeoIP, error) { + var geoipList []*router.GeoIP + var customCidrs []*router.CIDR + + for _, ip := range ips { + if strings.HasPrefix(ip, "geoip:") { + country := ip[6:] + geoip, err := loadGeoIP(strings.ToUpper(country)) + if err != nil { + return nil, newError("failed to load GeoIP: ", country).Base(err) + } + + geoipList = append(geoipList, &router.GeoIP{ + CountryCode: strings.ToUpper(country), + Cidr: geoip, + }) + continue + } + var isExtDatFile = 0 + { + const prefix = "ext:" + if strings.HasPrefix(ip, prefix) { + isExtDatFile = len(prefix) + } + const prefixQualified = "ext-ip:" + if strings.HasPrefix(ip, prefixQualified) { + isExtDatFile = len(prefixQualified) + } + } + if isExtDatFile != 0 { + kv := strings.Split(ip[isExtDatFile:], ":") + if len(kv) != 2 { + return nil, newError("invalid external resource: ", ip) + } + + filename := kv[0] + country := kv[1] + geoip, err := loadIP(filename, strings.ToUpper(country)) + if err != nil { + return nil, newError("failed to load IPs: ", country, " from ", filename).Base(err) + } + + geoipList = append(geoipList, &router.GeoIP{ + CountryCode: strings.ToUpper(filename + "_" + country), + Cidr: geoip, + }) + + continue + } + + ipRule, err := ParseIP(ip) + if err != nil { + return nil, newError("invalid IP: ", ip).Base(err) + } + customCidrs = append(customCidrs, ipRule) + } + + if len(customCidrs) > 0 { + geoipList = append(geoipList, &router.GeoIP{ + Cidr: customCidrs, + }) + } + + return geoipList, nil +} + +func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { + type RawFieldRule struct { + RouterRule + Domain *StringList `json:"domain"` + IP *StringList `json:"ip"` + Port *PortList `json:"port"` + Network *NetworkList `json:"network"` + SourceIP *StringList `json:"source"` + SourcePort *PortList `json:"sourcePort"` + User *StringList `json:"user"` + InboundTag *StringList `json:"inboundTag"` + Protocols *StringList `json:"protocol"` + Attributes string `json:"attrs"` + } + rawFieldRule := new(RawFieldRule) + err := json.Unmarshal(msg, rawFieldRule) + if err != nil { + return nil, err + } + + rule := new(router.RoutingRule) + switch { + case len(rawFieldRule.OutboundTag) > 0: + rule.TargetTag = &router.RoutingRule_Tag{ + Tag: rawFieldRule.OutboundTag, + } + case len(rawFieldRule.BalancerTag) > 0: + rule.TargetTag = &router.RoutingRule_BalancingTag{ + BalancingTag: rawFieldRule.BalancerTag, + } + default: + return nil, newError("neither outboundTag nor balancerTag is specified in routing rule") + } + + if rawFieldRule.Domain != nil { + for _, domain := range *rawFieldRule.Domain { + rules, err := parseDomainRule(domain) + if err != nil { + return nil, newError("failed to parse domain rule: ", domain).Base(err) + } + rule.Domain = append(rule.Domain, rules...) + } + } + + if rawFieldRule.IP != nil { + geoipList, err := toCidrList(*rawFieldRule.IP) + if err != nil { + return nil, err + } + rule.Geoip = geoipList + } + + if rawFieldRule.Port != nil { + rule.PortList = rawFieldRule.Port.Build() + } + + if rawFieldRule.Network != nil { + rule.Networks = rawFieldRule.Network.Build() + } + + if rawFieldRule.SourceIP != nil { + geoipList, err := toCidrList(*rawFieldRule.SourceIP) + if err != nil { + return nil, err + } + rule.SourceGeoip = geoipList + } + + if rawFieldRule.SourcePort != nil { + rule.SourcePortList = rawFieldRule.SourcePort.Build() + } + + if rawFieldRule.User != nil { + for _, s := range *rawFieldRule.User { + rule.UserEmail = append(rule.UserEmail, s) + } + } + + if rawFieldRule.InboundTag != nil { + for _, s := range *rawFieldRule.InboundTag { + rule.InboundTag = append(rule.InboundTag, s) + } + } + + if rawFieldRule.Protocols != nil { + for _, s := range *rawFieldRule.Protocols { + rule.Protocol = append(rule.Protocol, s) + } + } + + if len(rawFieldRule.Attributes) > 0 { + rule.Attributes = rawFieldRule.Attributes + } + + return rule, nil +} + +func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) { + rawRule := new(RouterRule) + err := json.Unmarshal(msg, rawRule) + if err != nil { + return nil, newError("invalid router rule").Base(err) + } + if rawRule.Type == "field" { + fieldrule, err := parseFieldRule(msg) + if err != nil { + return nil, newError("invalid field rule").Base(err) + } + return fieldrule, nil + } + if rawRule.Type == "chinaip" { + chinaiprule, err := parseChinaIPRule(msg) + if err != nil { + return nil, newError("invalid chinaip rule").Base(err) + } + return chinaiprule, nil + } + if rawRule.Type == "chinasites" { + chinasitesrule, err := parseChinaSitesRule(msg) + if err != nil { + return nil, newError("invalid chinasites rule").Base(err) + } + return chinasitesrule, nil + } + return nil, newError("unknown router rule type: ", rawRule.Type) +} + +func parseChinaIPRule(data []byte) (*router.RoutingRule, error) { + rawRule := new(RouterRule) + err := json.Unmarshal(data, rawRule) + if err != nil { + return nil, newError("invalid router rule").Base(err) + } + chinaIPs, err := loadGeoIP("CN") + if err != nil { + return nil, newError("failed to load geoip:cn").Base(err) + } + return &router.RoutingRule{ + TargetTag: &router.RoutingRule_Tag{ + Tag: rawRule.OutboundTag, + }, + Cidr: chinaIPs, + }, nil +} + +func parseChinaSitesRule(data []byte) (*router.RoutingRule, error) { + rawRule := new(RouterRule) + err := json.Unmarshal(data, rawRule) + if err != nil { + return nil, newError("invalid router rule").Base(err).AtError() + } + domains, err := loadGeositeWithAttr("geosite.dat", "CN") + if err != nil { + return nil, newError("failed to load geosite:cn.").Base(err) + } + return &router.RoutingRule{ + TargetTag: &router.RoutingRule_Tag{ + Tag: rawRule.OutboundTag, + }, + Domain: domains, + }, nil +} diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go new file mode 100644 index 00000000..3500be87 --- /dev/null +++ b/infra/conf/router_test.go @@ -0,0 +1,264 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/common/net" + . "github.com/xtls/xray-core/v1/infra/conf" +) + +func TestRouterConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(RouterConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "strategy": "rules", + "settings": { + "domainStrategy": "AsIs", + "rules": [ + { + "type": "field", + "domain": [ + "baidu.com", + "qq.com" + ], + "outboundTag": "direct" + }, + { + "type": "field", + "ip": [ + "10.0.0.0/8", + "::1/128" + ], + "outboundTag": "test" + },{ + "type": "field", + "port": "53, 443, 1000-2000", + "outboundTag": "test" + },{ + "type": "field", + "port": 123, + "outboundTag": "test" + } + ] + }, + "balancers": [ + { + "tag": "b1", + "selector": ["test"] + } + ] + }`, + Parser: createParser(), + Output: &router.Config{ + DomainStrategy: router.Config_AsIs, + BalancingRule: []*router.BalancingRule{ + { + Tag: "b1", + OutboundSelector: []string{"test"}, + }, + }, + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + { + Type: router.Domain_Plain, + Value: "baidu.com", + }, + { + Type: router.Domain_Plain, + Value: "qq.com", + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + { + Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Prefix: 128, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + { + PortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 53, To: 53}, + {From: 443, To: 443}, + {From: 1000, To: 2000}, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + { + PortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 123, To: 123}, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + }, + }, + }, + { + Input: `{ + "strategy": "rules", + "settings": { + "domainStrategy": "IPIfNonMatch", + "rules": [ + { + "type": "field", + "domain": [ + "baidu.com", + "qq.com" + ], + "outboundTag": "direct" + }, + { + "type": "field", + "ip": [ + "10.0.0.0/8", + "::1/128" + ], + "outboundTag": "test" + } + ] + } + }`, + Parser: createParser(), + Output: &router.Config{ + DomainStrategy: router.Config_IpIfNonMatch, + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + { + Type: router.Domain_Plain, + Value: "baidu.com", + }, + { + Type: router.Domain_Plain, + Value: "qq.com", + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + { + Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Prefix: 128, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + }, + }, + }, + { + Input: `{ + "domainStrategy": "AsIs", + "rules": [ + { + "type": "field", + "domain": [ + "baidu.com", + "qq.com" + ], + "outboundTag": "direct" + }, + { + "type": "field", + "ip": [ + "10.0.0.0/8", + "::1/128" + ], + "outboundTag": "test" + } + ] + }`, + Parser: createParser(), + Output: &router.Config{ + DomainStrategy: router.Config_AsIs, + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + { + Type: router.Domain_Plain, + Value: "baidu.com", + }, + { + Type: router.Domain_Plain, + Value: "qq.com", + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + { + Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Prefix: 128, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + }, + }, + }, + }) +} diff --git a/infra/conf/serial/errors.generated.go b/infra/conf/serial/errors.generated.go new file mode 100644 index 00000000..a75a8a30 --- /dev/null +++ b/infra/conf/serial/errors.generated.go @@ -0,0 +1,9 @@ +package serial + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/infra/conf/serial/loader.go b/infra/conf/serial/loader.go new file mode 100644 index 00000000..f56101b1 --- /dev/null +++ b/infra/conf/serial/loader.go @@ -0,0 +1,82 @@ +package serial + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/infra/conf" + json_reader "github.com/xtls/xray-core/v1/infra/conf/json" +) + +type offset struct { + line int + char int +} + +func findOffset(b []byte, o int) *offset { + if o >= len(b) || o < 0 { + return nil + } + + line := 1 + char := 0 + for i, x := range b { + if i == o { + break + } + if x == '\n' { + line++ + char = 0 + } else { + char++ + } + } + + return &offset{line: line, char: char} +} + +// DecodeJSONConfig reads from reader and decode the config into *conf.Config +// syntax error could be detected. +func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { + jsonConfig := &conf.Config{} + + jsonContent := bytes.NewBuffer(make([]byte, 0, 10240)) + jsonReader := io.TeeReader(&json_reader.Reader{ + Reader: reader, + }, jsonContent) + decoder := json.NewDecoder(jsonReader) + + if err := decoder.Decode(jsonConfig); err != nil { + var pos *offset + cause := errors.Cause(err) + switch tErr := cause.(type) { + case *json.SyntaxError: + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + case *json.UnmarshalTypeError: + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + } + if pos != nil { + return nil, newError("failed to read config file at line ", pos.line, " char ", pos.char).Base(err) + } + return nil, newError("failed to read config file").Base(err) + } + + return jsonConfig, nil +} + +func LoadJSONConfig(reader io.Reader) (*core.Config, error) { + jsonConfig, err := DecodeJSONConfig(reader) + if err != nil { + return nil, err + } + + pbConfig, err := jsonConfig.Build() + if err != nil { + return nil, newError("failed to parse json config").Base(err) + } + + return pbConfig, nil +} diff --git a/infra/conf/serial/loader_test.go b/infra/conf/serial/loader_test.go new file mode 100644 index 00000000..0847989c --- /dev/null +++ b/infra/conf/serial/loader_test.go @@ -0,0 +1,63 @@ +package serial_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/xtls/xray-core/v1/infra/conf/serial" +) + +func TestLoaderError(t *testing.T) { + testCases := []struct { + Input string + Output string + }{ + { + Input: `{ + "log": { + // abcd + 0, + "loglevel": "info" + } + }`, + Output: "line 4 char 6", + }, + { + Input: `{ + "log": { + // abcd + "loglevel": "info", + } + }`, + Output: "line 5 char 5", + }, + { + Input: `{ + "port": 1, + "inbounds": [{ + "protocol": "test" + }] + }`, + Output: "parse json config", + }, + { + Input: `{ + "inbounds": [{ + "port": 1, + "listen": 0, + "protocol": "test" + }] + }`, + Output: "line 1 char 1", + }, + } + for _, testCase := range testCases { + reader := bytes.NewReader([]byte(testCase.Input)) + _, err := serial.LoadJSONConfig(reader) + errString := err.Error() + if !strings.Contains(errString, testCase.Output) { + t.Error("unexpected output from json: ", testCase.Input, ". expected ", testCase.Output, ", but actually ", errString) + } + } +} diff --git a/infra/conf/serial/serial.go b/infra/conf/serial/serial.go new file mode 100644 index 00000000..81ccf771 --- /dev/null +++ b/infra/conf/serial/serial.go @@ -0,0 +1,3 @@ +package serial + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go new file mode 100644 index 00000000..3b032b72 --- /dev/null +++ b/infra/conf/shadowsocks.go @@ -0,0 +1,128 @@ +package conf + +import ( + "strings" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/shadowsocks" +) + +func cipherFromString(c string) shadowsocks.CipherType { + switch strings.ToLower(c) { + case "aes-256-cfb": + return shadowsocks.CipherType_AES_256_CFB + case "aes-128-cfb": + return shadowsocks.CipherType_AES_128_CFB + case "chacha20": + return shadowsocks.CipherType_CHACHA20 + case "chacha20-ietf": + return shadowsocks.CipherType_CHACHA20_IETF + case "aes-128-gcm", "aead_aes_128_gcm": + return shadowsocks.CipherType_AES_128_GCM + case "aes-256-gcm", "aead_aes_256_gcm": + return shadowsocks.CipherType_AES_256_GCM + case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305": + return shadowsocks.CipherType_CHACHA20_POLY1305 + case "none", "plain": + return shadowsocks.CipherType_NONE + default: + return shadowsocks.CipherType_UNKNOWN + } +} + +type ShadowsocksServerConfig struct { + Cipher string `json:"method"` + Password string `json:"password"` + UDP bool `json:"udp"` + Level byte `json:"level"` + Email string `json:"email"` + NetworkList *NetworkList `json:"network"` +} + +func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { + config := new(shadowsocks.ServerConfig) + config.UdpEnabled = v.UDP + config.Network = v.NetworkList.Build() + + if v.Password == "" { + return nil, newError("Shadowsocks password is not specified.") + } + account := &shadowsocks.Account{ + Password: v.Password, + } + account.CipherType = cipherFromString(v.Cipher) + if account.CipherType == shadowsocks.CipherType_UNKNOWN { + return nil, newError("unknown cipher method: ", v.Cipher) + } + + config.User = &protocol.User{ + Email: v.Email, + Level: uint32(v.Level), + Account: serial.ToTypedMessage(account), + } + + return config, nil +} + +type ShadowsocksServerTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Cipher string `json:"method"` + Password string `json:"password"` + Email string `json:"email"` + Ota bool `json:"ota"` + Level byte `json:"level"` +} + +type ShadowsocksClientConfig struct { + Servers []*ShadowsocksServerTarget `json:"servers"` +} + +func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { + config := new(shadowsocks.ClientConfig) + + if len(v.Servers) == 0 { + return nil, newError("0 Shadowsocks server configured.") + } + + serverSpecs := make([]*protocol.ServerEndpoint, len(v.Servers)) + for idx, server := range v.Servers { + if server.Address == nil { + return nil, newError("Shadowsocks server address is not set.") + } + if server.Port == 0 { + return nil, newError("Invalid Shadowsocks port.") + } + if server.Password == "" { + return nil, newError("Shadowsocks password is not specified.") + } + account := &shadowsocks.Account{ + Password: server.Password, + } + account.CipherType = cipherFromString(server.Cipher) + if account.CipherType == shadowsocks.CipherType_UNKNOWN { + return nil, newError("unknown cipher method: ", server.Cipher) + } + + ss := &protocol.ServerEndpoint{ + Address: server.Address.Build(), + Port: uint32(server.Port), + User: []*protocol.User{ + { + Level: uint32(server.Level), + Email: server.Email, + Account: serial.ToTypedMessage(account), + }, + }, + } + + serverSpecs[idx] = ss + } + + config.Server = serverSpecs + + return config, nil +} diff --git a/infra/conf/shadowsocks_test.go b/infra/conf/shadowsocks_test.go new file mode 100644 index 00000000..65114295 --- /dev/null +++ b/infra/conf/shadowsocks_test.go @@ -0,0 +1,36 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/shadowsocks" +) + +func TestShadowsocksServerConfigParsing(t *testing.T) { + creator := func() Buildable { + return new(ShadowsocksServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "method": "aes-128-cfb", + "password": "xray-password" + }`, + Parser: loadJSON(creator), + Output: &shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: serial.ToTypedMessage(&shadowsocks.Account{ + CipherType: shadowsocks.CipherType_AES_128_CFB, + Password: "xray-password", + }), + }, + Network: []net.Network{net.Network_TCP}, + }, + }, + }) +} diff --git a/infra/conf/socks.go b/infra/conf/socks.go new file mode 100644 index 00000000..4214551d --- /dev/null +++ b/infra/conf/socks.go @@ -0,0 +1,99 @@ +package conf + +import ( + "encoding/json" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/socks" +) + +type SocksAccount struct { + Username string `json:"user"` + Password string `json:"pass"` +} + +func (v *SocksAccount) Build() *socks.Account { + return &socks.Account{ + Username: v.Username, + Password: v.Password, + } +} + +const ( + AuthMethodNoAuth = "noauth" + AuthMethodUserPass = "password" +) + +type SocksServerConfig struct { + AuthMethod string `json:"auth"` + Accounts []*SocksAccount `json:"accounts"` + UDP bool `json:"udp"` + Host *Address `json:"ip"` + Timeout uint32 `json:"timeout"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *SocksServerConfig) Build() (proto.Message, error) { + config := new(socks.ServerConfig) + switch v.AuthMethod { + case AuthMethodNoAuth: + config.AuthType = socks.AuthType_NO_AUTH + case AuthMethodUserPass: + config.AuthType = socks.AuthType_PASSWORD + default: + // newError("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog() + config.AuthType = socks.AuthType_NO_AUTH + } + + if len(v.Accounts) > 0 { + config.Accounts = make(map[string]string, len(v.Accounts)) + for _, account := range v.Accounts { + config.Accounts[account.Username] = account.Password + } + } + + config.UdpEnabled = v.UDP + if v.Host != nil { + config.Address = v.Host.Build() + } + + config.Timeout = v.Timeout + config.UserLevel = v.UserLevel + return config, nil +} + +type SocksRemoteConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} +type SocksClientConfig struct { + Servers []*SocksRemoteConfig `json:"servers"` +} + +func (v *SocksClientConfig) Build() (proto.Message, error) { + config := new(socks.ClientConfig) + config.Server = make([]*protocol.ServerEndpoint, len(v.Servers)) + for idx, serverConfig := range v.Servers { + server := &protocol.ServerEndpoint{ + Address: serverConfig.Address.Build(), + Port: uint32(serverConfig.Port), + } + for _, rawUser := range serverConfig.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, newError("failed to parse Socks user").Base(err).AtError() + } + account := new(SocksAccount) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, newError("failed to parse socks account").Base(err).AtError() + } + user.Account = serial.ToTypedMessage(account.Build()) + server.User = append(server.User, user) + } + config.Server[idx] = server + } + return config, nil +} diff --git a/infra/conf/socks_test.go b/infra/conf/socks_test.go new file mode 100644 index 00000000..4ea2aedd --- /dev/null +++ b/infra/conf/socks_test.go @@ -0,0 +1,92 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/socks" +) + +func TestSocksInboundConfig(t *testing.T) { + creator := func() Buildable { + return new(SocksServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "auth": "password", + "accounts": [ + { + "user": "my-username", + "pass": "my-password" + } + ], + "udp": false, + "ip": "127.0.0.1", + "timeout": 5, + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "my-username": "my-password", + }, + UdpEnabled: false, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Timeout: 5, + UserLevel: 1, + }, + }, + }) +} + +func TestSocksOutboundConfig(t *testing.T) { + creator := func() Buildable { + return new(SocksClientConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "servers": [{ + "address": "127.0.0.1", + "port": 1234, + "users": [ + {"user": "test user", "pass": "test pass", "email": "test@email.com"} + ] + }] + }`, + Parser: loadJSON(creator), + Output: &socks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 1234, + User: []*protocol.User{ + { + Email: "test@email.com", + Account: serial.ToTypedMessage(&socks.Account{ + Username: "test user", + Password: "test pass", + }), + }, + }, + }, + }, + }, + }, + }) +} diff --git a/infra/conf/transport.go b/infra/conf/transport.go new file mode 100644 index 00000000..a8899431 --- /dev/null +++ b/infra/conf/transport.go @@ -0,0 +1,89 @@ +package conf + +import ( + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +type TransportConfig struct { + TCPConfig *TCPConfig `json:"tcpSettings"` + KCPConfig *KCPConfig `json:"kcpSettings"` + WSConfig *WebSocketConfig `json:"wsSettings"` + HTTPConfig *HTTPConfig `json:"httpSettings"` + DSConfig *DomainSocketConfig `json:"dsSettings"` + QUICConfig *QUICConfig `json:"quicSettings"` +} + +// Build implements Buildable. +func (c *TransportConfig) Build() (*transport.Config, error) { + config := new(transport.Config) + + if c.TCPConfig != nil { + ts, err := c.TCPConfig.Build() + if err != nil { + return nil, newError("failed to build TCP config").Base(err).AtError() + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(ts), + }) + } + + if c.KCPConfig != nil { + ts, err := c.KCPConfig.Build() + if err != nil { + return nil, newError("failed to build mKCP config").Base(err).AtError() + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(ts), + }) + } + + if c.WSConfig != nil { + ts, err := c.WSConfig.Build() + if err != nil { + return nil, newError("failed to build WebSocket config").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(ts), + }) + } + + if c.HTTPConfig != nil { + ts, err := c.HTTPConfig.Build() + if err != nil { + return nil, newError("Failed to build HTTP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "http", + Settings: serial.ToTypedMessage(ts), + }) + } + + if c.DSConfig != nil { + ds, err := c.DSConfig.Build() + if err != nil { + return nil, newError("Failed to build DomainSocket config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "domainsocket", + Settings: serial.ToTypedMessage(ds), + }) + } + + if c.QUICConfig != nil { + qs, err := c.QUICConfig.Build() + if err != nil { + return nil, newError("Failed to build QUIC config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "quic", + Settings: serial.ToTypedMessage(qs), + }) + } + + return config, nil +} diff --git a/infra/conf/transport_authenticators.go b/infra/conf/transport_authenticators.go new file mode 100644 index 00000000..a19dd930 --- /dev/null +++ b/infra/conf/transport_authenticators.go @@ -0,0 +1,223 @@ +package conf + +import ( + "sort" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/transport/internet/headers/http" + "github.com/xtls/xray-core/v1/transport/internet/headers/noop" + "github.com/xtls/xray-core/v1/transport/internet/headers/srtp" + "github.com/xtls/xray-core/v1/transport/internet/headers/tls" + "github.com/xtls/xray-core/v1/transport/internet/headers/utp" + "github.com/xtls/xray-core/v1/transport/internet/headers/wechat" + "github.com/xtls/xray-core/v1/transport/internet/headers/wireguard" +) + +type NoOpAuthenticator struct{} + +func (NoOpAuthenticator) Build() (proto.Message, error) { + return new(noop.Config), nil +} + +type NoOpConnectionAuthenticator struct{} + +func (NoOpConnectionAuthenticator) Build() (proto.Message, error) { + return new(noop.ConnectionConfig), nil +} + +type SRTPAuthenticator struct{} + +func (SRTPAuthenticator) Build() (proto.Message, error) { + return new(srtp.Config), nil +} + +type UTPAuthenticator struct{} + +func (UTPAuthenticator) Build() (proto.Message, error) { + return new(utp.Config), nil +} + +type WechatVideoAuthenticator struct{} + +func (WechatVideoAuthenticator) Build() (proto.Message, error) { + return new(wechat.VideoConfig), nil +} + +type WireguardAuthenticator struct{} + +func (WireguardAuthenticator) Build() (proto.Message, error) { + return new(wireguard.WireguardConfig), nil +} + +type DTLSAuthenticator struct{} + +func (DTLSAuthenticator) Build() (proto.Message, error) { + return new(tls.PacketConfig), nil +} + +type AuthenticatorRequest struct { + Version string `json:"version"` + Method string `json:"method"` + Path StringList `json:"path"` + Headers map[string]*StringList `json:"headers"` +} + +func sortMapKeys(m map[string]*StringList) []string { + var keys []string + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { + config := &http.RequestConfig{ + Uri: []string{"/"}, + Header: []*http.Header{ + { + Name: "Host", + Value: []string{"www.baidu.com", "www.bing.com"}, + }, + { + Name: "User-Agent", + Value: []string{ + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46", + }, + }, + { + Name: "Accept-Encoding", + Value: []string{"gzip, deflate"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Method) > 0 { + config.Method = &http.Method{Value: v.Method} + } + + if len(v.Path) > 0 { + config.Uri = append([]string(nil), (v.Path)...) + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, newError("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type AuthenticatorResponse struct { + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + Headers map[string]*StringList `json:"headers"` +} + +func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) { + config := &http.ResponseConfig{ + Header: []*http.Header{ + { + Name: "Content-Type", + Value: []string{"application/octet-stream", "video/mpeg"}, + }, + { + Name: "Transfer-Encoding", + Value: []string{"chunked"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + { + Name: "Cache-Control", + Value: []string{"private", "no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Status) > 0 || len(v.Reason) > 0 { + config.Status = &http.Status{ + Code: "200", + Reason: "OK", + } + if len(v.Status) > 0 { + config.Status.Code = v.Status + } + if len(v.Reason) > 0 { + config.Status.Reason = v.Reason + } + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, newError("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type Authenticator struct { + Request AuthenticatorRequest `json:"request"` + Response AuthenticatorResponse `json:"response"` +} + +func (v *Authenticator) Build() (proto.Message, error) { + config := new(http.Config) + requestConfig, err := v.Request.Build() + if err != nil { + return nil, err + } + config.Request = requestConfig + + responseConfig, err := v.Response.Build() + if err != nil { + return nil, err + } + config.Response = responseConfig + + return config, nil +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go new file mode 100644 index 00000000..3c3b2053 --- /dev/null +++ b/infra/conf/transport_internet.go @@ -0,0 +1,599 @@ +package conf + +import ( + "encoding/json" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common/platform/filesystem" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/domainsocket" + "github.com/xtls/xray-core/v1/transport/internet/http" + "github.com/xtls/xray-core/v1/transport/internet/kcp" + "github.com/xtls/xray-core/v1/transport/internet/quic" + "github.com/xtls/xray-core/v1/transport/internet/tcp" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/websocket" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + kcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "none": func() interface{} { return new(NoOpAuthenticator) }, + "srtp": func() interface{} { return new(SRTPAuthenticator) }, + "utp": func() interface{} { return new(UTPAuthenticator) }, + "wechat-video": func() interface{} { return new(WechatVideoAuthenticator) }, + "dtls": func() interface{} { return new(DTLSAuthenticator) }, + "wireguard": func() interface{} { return new(WireguardAuthenticator) }, + }, "type", "") + + tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, + "http": func() interface{} { return new(Authenticator) }, + }, "type", "") +) + +type KCPConfig struct { + Mtu *uint32 `json:"mtu"` + Tti *uint32 `json:"tti"` + UpCap *uint32 `json:"uplinkCapacity"` + DownCap *uint32 `json:"downlinkCapacity"` + Congestion *bool `json:"congestion"` + ReadBufferSize *uint32 `json:"readBufferSize"` + WriteBufferSize *uint32 `json:"writeBufferSize"` + HeaderConfig json.RawMessage `json:"header"` + Seed *string `json:"seed"` +} + +// Build implements Buildable. +func (c *KCPConfig) Build() (proto.Message, error) { + config := new(kcp.Config) + + if c.Mtu != nil { + mtu := *c.Mtu + if mtu < 576 || mtu > 1460 { + return nil, newError("invalid mKCP MTU size: ", mtu).AtError() + } + config.Mtu = &kcp.MTU{Value: mtu} + } + if c.Tti != nil { + tti := *c.Tti + if tti < 10 || tti > 100 { + return nil, newError("invalid mKCP TTI: ", tti).AtError() + } + config.Tti = &kcp.TTI{Value: tti} + } + if c.UpCap != nil { + config.UplinkCapacity = &kcp.UplinkCapacity{Value: *c.UpCap} + } + if c.DownCap != nil { + config.DownlinkCapacity = &kcp.DownlinkCapacity{Value: *c.DownCap} + } + if c.Congestion != nil { + config.Congestion = *c.Congestion + } + if c.ReadBufferSize != nil { + size := *c.ReadBufferSize + if size > 0 { + config.ReadBuffer = &kcp.ReadBuffer{Size: size * 1024 * 1024} + } else { + config.ReadBuffer = &kcp.ReadBuffer{Size: 512 * 1024} + } + } + if c.WriteBufferSize != nil { + size := *c.WriteBufferSize + if size > 0 { + config.WriteBuffer = &kcp.WriteBuffer{Size: size * 1024 * 1024} + } else { + config.WriteBuffer = &kcp.WriteBuffer{Size: 512 * 1024} + } + } + if len(c.HeaderConfig) > 0 { + headerConfig, _, err := kcpHeaderLoader.Load(c.HeaderConfig) + if err != nil { + return nil, newError("invalid mKCP header config.").Base(err).AtError() + } + ts, err := headerConfig.(Buildable).Build() + if err != nil { + return nil, newError("invalid mKCP header config").Base(err).AtError() + } + config.HeaderConfig = serial.ToTypedMessage(ts) + } + + if c.Seed != nil { + config.Seed = &kcp.EncryptionSeed{Seed: *c.Seed} + } + + return config, nil +} + +type TCPConfig struct { + HeaderConfig json.RawMessage `json:"header"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *TCPConfig) Build() (proto.Message, error) { + config := new(tcp.Config) + if len(c.HeaderConfig) > 0 { + headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig) + if err != nil { + return nil, newError("invalid TCP header config").Base(err).AtError() + } + ts, err := headerConfig.(Buildable).Build() + if err != nil { + return nil, newError("invalid TCP header config").Base(err).AtError() + } + config.HeaderSettings = serial.ToTypedMessage(ts) + } + if c.AcceptProxyProtocol { + config.AcceptProxyProtocol = c.AcceptProxyProtocol + } + return config, nil +} + +type WebSocketConfig struct { + Path string `json:"path"` + Path2 string `json:"Path"` // The key was misspelled. For backward compatibility, we have to keep track the old key. + Headers map[string]string `json:"headers"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *WebSocketConfig) Build() (proto.Message, error) { + path := c.Path + if path == "" && c.Path2 != "" { + path = c.Path2 + } + header := make([]*websocket.Header, 0, 32) + for key, value := range c.Headers { + header = append(header, &websocket.Header{ + Key: key, + Value: value, + }) + } + config := &websocket.Config{ + Path: path, + Header: header, + } + if c.AcceptProxyProtocol { + config.AcceptProxyProtocol = c.AcceptProxyProtocol + } + return config, nil +} + +type HTTPConfig struct { + Host *StringList `json:"host"` + Path string `json:"path"` +} + +// Build implements Buildable. +func (c *HTTPConfig) Build() (proto.Message, error) { + config := &http.Config{ + Path: c.Path, + } + if c.Host != nil { + config.Host = []string(*c.Host) + } + return config, nil +} + +type QUICConfig struct { + Header json.RawMessage `json:"header"` + Security string `json:"security"` + Key string `json:"key"` +} + +// Build implements Buildable. +func (c *QUICConfig) Build() (proto.Message, error) { + config := &quic.Config{ + Key: c.Key, + } + + if len(c.Header) > 0 { + headerConfig, _, err := kcpHeaderLoader.Load(c.Header) + if err != nil { + return nil, newError("invalid QUIC header config.").Base(err).AtError() + } + ts, err := headerConfig.(Buildable).Build() + if err != nil { + return nil, newError("invalid QUIC header config").Base(err).AtError() + } + config.Header = serial.ToTypedMessage(ts) + } + + var st protocol.SecurityType + switch strings.ToLower(c.Security) { + case "aes-128-gcm": + st = protocol.SecurityType_AES128_GCM + case "chacha20-poly1305": + st = protocol.SecurityType_CHACHA20_POLY1305 + default: + st = protocol.SecurityType_NONE + } + + config.Security = &protocol.SecurityConfig{ + Type: st, + } + + return config, nil +} + +type DomainSocketConfig struct { + Path string `json:"path"` + Abstract bool `json:"abstract"` + Padding bool `json:"padding"` +} + +// Build implements Buildable. +func (c *DomainSocketConfig) Build() (proto.Message, error) { + return &domainsocket.Config{ + Path: c.Path, + Abstract: c.Abstract, + Padding: c.Padding, + }, nil +} + +func readFileOrString(f string, s []string) ([]byte, error) { + if len(f) > 0 { + return filesystem.ReadFile(f) + } + if len(s) > 0 { + return []byte(strings.Join(s, "\n")), nil + } + return nil, newError("both file and bytes are empty.") +} + +type TLSCertConfig struct { + CertFile string `json:"certificateFile"` + CertStr []string `json:"certificate"` + KeyFile string `json:"keyFile"` + KeyStr []string `json:"key"` + Usage string `json:"usage"` +} + +// Build implements Buildable. +func (c *TLSCertConfig) Build() (*tls.Certificate, error) { + certificate := new(tls.Certificate) + + cert, err := readFileOrString(c.CertFile, c.CertStr) + if err != nil { + return nil, newError("failed to parse certificate").Base(err) + } + certificate.Certificate = cert + + if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 { + key, err := readFileOrString(c.KeyFile, c.KeyStr) + if err != nil { + return nil, newError("failed to parse key").Base(err) + } + certificate.Key = key + } + + switch strings.ToLower(c.Usage) { + case "encipherment": + certificate.Usage = tls.Certificate_ENCIPHERMENT + case "verify": + certificate.Usage = tls.Certificate_AUTHORITY_VERIFY + case "issue": + certificate.Usage = tls.Certificate_AUTHORITY_ISSUE + default: + certificate.Usage = tls.Certificate_ENCIPHERMENT + } + + return certificate, nil +} + +type TLSConfig struct { + Insecure bool `json:"allowInsecure"` + InsecureCiphers bool `json:"allowInsecureCiphers"` + Certs []*TLSCertConfig `json:"certificates"` + ServerName string `json:"serverName"` + ALPN *StringList `json:"alpn"` + DisableSessionResumption bool `json:"disableSessionResumption"` + DisableSystemRoot bool `json:"disableSystemRoot"` +} + +// Build implements Buildable. +func (c *TLSConfig) Build() (proto.Message, error) { + config := new(tls.Config) + config.Certificate = make([]*tls.Certificate, len(c.Certs)) + for idx, certConf := range c.Certs { + cert, err := certConf.Build() + if err != nil { + return nil, err + } + config.Certificate[idx] = cert + } + serverName := c.ServerName + config.AllowInsecure = c.Insecure + config.AllowInsecureCiphers = c.InsecureCiphers + if len(c.ServerName) > 0 { + config.ServerName = serverName + } + if c.ALPN != nil && len(*c.ALPN) > 0 { + config.NextProtocol = []string(*c.ALPN) + } + config.DisableSessionResumption = c.DisableSessionResumption + config.DisableSystemRoot = c.DisableSystemRoot + return config, nil +} + +type XTLSCertConfig struct { + CertFile string `json:"certificateFile"` + CertStr []string `json:"certificate"` + KeyFile string `json:"keyFile"` + KeyStr []string `json:"key"` + Usage string `json:"usage"` +} + +// Build implements Buildable. +func (c *XTLSCertConfig) Build() (*xtls.Certificate, error) { + certificate := new(xtls.Certificate) + + cert, err := readFileOrString(c.CertFile, c.CertStr) + if err != nil { + return nil, newError("failed to parse certificate").Base(err) + } + certificate.Certificate = cert + + if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 { + key, err := readFileOrString(c.KeyFile, c.KeyStr) + if err != nil { + return nil, newError("failed to parse key").Base(err) + } + certificate.Key = key + } + + switch strings.ToLower(c.Usage) { + case "encipherment": + certificate.Usage = xtls.Certificate_ENCIPHERMENT + case "verify": + certificate.Usage = xtls.Certificate_AUTHORITY_VERIFY + case "issue": + certificate.Usage = xtls.Certificate_AUTHORITY_ISSUE + default: + certificate.Usage = xtls.Certificate_ENCIPHERMENT + } + + return certificate, nil +} + +type XTLSConfig struct { + Insecure bool `json:"allowInsecure"` + InsecureCiphers bool `json:"allowInsecureCiphers"` + Certs []*XTLSCertConfig `json:"certificates"` + ServerName string `json:"serverName"` + ALPN *StringList `json:"alpn"` + DisableSessionResumption bool `json:"disableSessionResumption"` + DisableSystemRoot bool `json:"disableSystemRoot"` +} + +// Build implements Buildable. +func (c *XTLSConfig) Build() (proto.Message, error) { + config := new(xtls.Config) + config.Certificate = make([]*xtls.Certificate, len(c.Certs)) + for idx, certConf := range c.Certs { + cert, err := certConf.Build() + if err != nil { + return nil, err + } + config.Certificate[idx] = cert + } + serverName := c.ServerName + config.AllowInsecure = c.Insecure + config.AllowInsecureCiphers = c.InsecureCiphers + if len(c.ServerName) > 0 { + config.ServerName = serverName + } + if c.ALPN != nil && len(*c.ALPN) > 0 { + config.NextProtocol = []string(*c.ALPN) + } + config.DisableSessionResumption = c.DisableSessionResumption + config.DisableSystemRoot = c.DisableSystemRoot + return config, nil +} + +type TransportProtocol string + +// Build implements Buildable. +func (p TransportProtocol) Build() (string, error) { + switch strings.ToLower(string(p)) { + case "tcp": + return "tcp", nil + case "kcp", "mkcp": + return "mkcp", nil + case "ws", "websocket": + return "websocket", nil + case "h2", "http": + return "http", nil + case "ds", "domainsocket": + return "domainsocket", nil + case "quic": + return "quic", nil + default: + return "", newError("Config: unknown transport protocol: ", p) + } +} + +type SocketConfig struct { + Mark int32 `json:"mark"` + TFO *bool `json:"tcpFastOpen"` + TProxy string `json:"tproxy"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *SocketConfig) Build() (*internet.SocketConfig, error) { + var tfoSettings internet.SocketConfig_TCPFastOpenState + if c.TFO != nil { + if *c.TFO { + tfoSettings = internet.SocketConfig_Enable + } else { + tfoSettings = internet.SocketConfig_Disable + } + } + var tproxy internet.SocketConfig_TProxyMode + switch strings.ToLower(c.TProxy) { + case "tproxy": + tproxy = internet.SocketConfig_TProxy + case "redirect": + tproxy = internet.SocketConfig_Redirect + default: + tproxy = internet.SocketConfig_Off + } + + return &internet.SocketConfig{ + Mark: c.Mark, + Tfo: tfoSettings, + Tproxy: tproxy, + AcceptProxyProtocol: c.AcceptProxyProtocol, + }, nil +} + +type StreamConfig struct { + Network *TransportProtocol `json:"network"` + Security string `json:"security"` + TLSSettings *TLSConfig `json:"tlsSettings"` + XTLSSettings *XTLSConfig `json:"xtlsSettings"` + TCPSettings *TCPConfig `json:"tcpSettings"` + KCPSettings *KCPConfig `json:"kcpSettings"` + WSSettings *WebSocketConfig `json:"wsSettings"` + HTTPSettings *HTTPConfig `json:"httpSettings"` + DSSettings *DomainSocketConfig `json:"dsSettings"` + QUICSettings *QUICConfig `json:"quicSettings"` + SocketSettings *SocketConfig `json:"sockopt"` +} + +// Build implements Buildable. +func (c *StreamConfig) Build() (*internet.StreamConfig, error) { + config := &internet.StreamConfig{ + ProtocolName: "tcp", + } + if c.Network != nil { + protocol, err := c.Network.Build() + if err != nil { + return nil, err + } + config.ProtocolName = protocol + } + if strings.EqualFold(c.Security, "tls") { + tlsSettings := c.TLSSettings + if tlsSettings == nil { + if c.XTLSSettings != nil { + return nil, newError(`TLS: Please use "tlsSettings" instead of "xtlsSettings".`) + } + tlsSettings = &TLSConfig{} + } + ts, err := tlsSettings.Build() + if err != nil { + return nil, newError("Failed to build TLS config.").Base(err) + } + tm := serial.ToTypedMessage(ts) + config.SecuritySettings = append(config.SecuritySettings, tm) + config.SecurityType = tm.Type + } + if strings.EqualFold(c.Security, "xtls") { + if config.ProtocolName != "tcp" && config.ProtocolName != "mkcp" && config.ProtocolName != "domainsocket" { + return nil, newError("XTLS only supports TCP, mKCP and DomainSocket for now.") + } + xtlsSettings := c.XTLSSettings + if xtlsSettings == nil { + if c.TLSSettings != nil { + return nil, newError(`XTLS: Please use "xtlsSettings" instead of "tlsSettings".`) + } + xtlsSettings = &XTLSConfig{} + } + ts, err := xtlsSettings.Build() + if err != nil { + return nil, newError("Failed to build XTLS config.").Base(err) + } + tm := serial.ToTypedMessage(ts) + config.SecuritySettings = append(config.SecuritySettings, tm) + config.SecurityType = tm.Type + } + if c.TCPSettings != nil { + ts, err := c.TCPSettings.Build() + if err != nil { + return nil, newError("Failed to build TCP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.KCPSettings != nil { + ts, err := c.KCPSettings.Build() + if err != nil { + return nil, newError("Failed to build mKCP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.WSSettings != nil { + ts, err := c.WSSettings.Build() + if err != nil { + return nil, newError("Failed to build WebSocket config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.HTTPSettings != nil { + ts, err := c.HTTPSettings.Build() + if err != nil { + return nil, newError("Failed to build HTTP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "http", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.DSSettings != nil { + ds, err := c.DSSettings.Build() + if err != nil { + return nil, newError("Failed to build DomainSocket config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "domainsocket", + Settings: serial.ToTypedMessage(ds), + }) + } + if c.QUICSettings != nil { + qs, err := c.QUICSettings.Build() + if err != nil { + return nil, newError("Failed to build QUIC config").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "quic", + Settings: serial.ToTypedMessage(qs), + }) + } + if c.SocketSettings != nil { + ss, err := c.SocketSettings.Build() + if err != nil { + return nil, newError("Failed to build sockopt").Base(err) + } + config.SocketSettings = ss + } + return config, nil +} + +type ProxyConfig struct { + Tag string `json:"tag"` +} + +// Build implements Buildable. +func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) { + if v.Tag == "" { + return nil, newError("Proxy tag is not set.") + } + return &internet.ProxyConfig{ + Tag: v.Tag, + }, nil +} diff --git a/infra/conf/transport_test.go b/infra/conf/transport_test.go new file mode 100644 index 00000000..5ba05607 --- /dev/null +++ b/infra/conf/transport_test.go @@ -0,0 +1,169 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/headers/http" + "github.com/xtls/xray-core/v1/transport/internet/headers/noop" + "github.com/xtls/xray-core/v1/transport/internet/headers/tls" + "github.com/xtls/xray-core/v1/transport/internet/kcp" + "github.com/xtls/xray-core/v1/transport/internet/quic" + "github.com/xtls/xray-core/v1/transport/internet/tcp" + "github.com/xtls/xray-core/v1/transport/internet/websocket" +) + +func TestSocketConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(SocketConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "mark": 1, + "tcpFastOpen": true + }`, + Parser: createParser(), + Output: &internet.SocketConfig{ + Mark: 1, + Tfo: internet.SocketConfig_Enable, + }, + }, + }) +} + +func TestTransportConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(TransportConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpSettings": { + "header": { + "type": "http", + "request": { + "version": "1.1", + "method": "GET", + "path": "/b", + "headers": { + "a": "b", + "c": "d" + } + }, + "response": { + "version": "1.0", + "status": "404", + "reason": "Not Found" + } + } + }, + "kcpSettings": { + "mtu": 1200, + "header": { + "type": "none" + } + }, + "wsSettings": { + "path": "/t" + }, + "quicSettings": { + "key": "abcd", + "header": { + "type": "dtls" + } + } + }`, + Parser: createParser(), + Output: &transport.Config{ + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&tcp.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{ + Request: &http.RequestConfig{ + Version: &http.Version{Value: "1.1"}, + Method: &http.Method{Value: "GET"}, + Uri: []string{"/b"}, + Header: []*http.Header{ + {Name: "a", Value: []string{"b"}}, + {Name: "c", Value: []string{"d"}}, + }, + }, + Response: &http.ResponseConfig{ + Version: &http.Version{Value: "1.0"}, + Status: &http.Status{Code: "404", Reason: "Not Found"}, + Header: []*http.Header{ + { + Name: "Content-Type", + Value: []string{"application/octet-stream", "video/mpeg"}, + }, + { + Name: "Transfer-Encoding", + Value: []string{"chunked"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + { + Name: "Cache-Control", + Value: []string{"private", "no-cache"}, + }, + }, + }, + }), + }), + }, + { + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(&kcp.Config{ + Mtu: &kcp.MTU{Value: 1200}, + HeaderConfig: serial.ToTypedMessage(&noop.Config{}), + }), + }, + { + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(&websocket.Config{ + Path: "/t", + }), + }, + { + ProtocolName: "quic", + Settings: serial.ToTypedMessage(&quic.Config{ + Key: "abcd", + Security: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + Header: serial.ToTypedMessage(&tls.PacketConfig{}), + }), + }, + }, + }, + }, + }) +} diff --git a/infra/conf/trojan.go b/infra/conf/trojan.go new file mode 100644 index 00000000..dc36b039 --- /dev/null +++ b/infra/conf/trojan.go @@ -0,0 +1,175 @@ +package conf + +import ( + "encoding/json" + "runtime" + "strconv" + "syscall" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/trojan" +) + +// TrojanServerTarget is configuration of a single trojan server +type TrojanServerTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Password string `json:"password"` + Email string `json:"email"` + Level byte `json:"level"` + Flow string `json:"flow"` +} + +// TrojanClientConfig is configuration of trojan servers +type TrojanClientConfig struct { + Servers []*TrojanServerTarget `json:"servers"` +} + +// Build implements Buildable +func (c *TrojanClientConfig) Build() (proto.Message, error) { + config := new(trojan.ClientConfig) + + if len(c.Servers) == 0 { + return nil, newError("0 Trojan server configured.") + } + + serverSpecs := make([]*protocol.ServerEndpoint, len(c.Servers)) + for idx, rec := range c.Servers { + if rec.Address == nil { + return nil, newError("Trojan server address is not set.") + } + if rec.Port == 0 { + return nil, newError("Invalid Trojan port.") + } + if rec.Password == "" { + return nil, newError("Trojan password is not specified.") + } + account := &trojan.Account{ + Password: rec.Password, + Flow: rec.Flow, + } + trojan := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + User: []*protocol.User{ + { + Level: uint32(rec.Level), + Email: rec.Email, + Account: serial.ToTypedMessage(account), + }, + }, + } + + serverSpecs[idx] = trojan + } + + config.Server = serverSpecs + + return config, nil +} + +// TrojanInboundFallback is fallback configuration +type TrojanInboundFallback struct { + Alpn string `json:"alpn"` + Path string `json:"path"` + Type string `json:"type"` + Dest json.RawMessage `json:"dest"` + Xver uint64 `json:"xver"` +} + +// TrojanUserConfig is user configuration +type TrojanUserConfig struct { + Password string `json:"password"` + Level byte `json:"level"` + Email string `json:"email"` + Flow string `json:"flow"` +} + +// TrojanServerConfig is Inbound configuration +type TrojanServerConfig struct { + Clients []*TrojanUserConfig `json:"clients"` + Fallback json.RawMessage `json:"fallback"` + Fallbacks []*TrojanInboundFallback `json:"fallbacks"` +} + +// Build implements Buildable +func (c *TrojanServerConfig) Build() (proto.Message, error) { + config := new(trojan.ServerConfig) + config.Users = make([]*protocol.User, len(c.Clients)) + for idx, rawUser := range c.Clients { + user := new(protocol.User) + account := &trojan.Account{ + Password: rawUser.Password, + Flow: rawUser.Flow, + } + + user.Email = rawUser.Email + user.Level = uint32(rawUser.Level) + user.Account = serial.ToTypedMessage(account) + config.Users[idx] = user + } + + if c.Fallback != nil { + return nil, newError(`Trojan settings: please use "fallbacks":[{}] instead of "fallback":{}`) + } + for _, fb := range c.Fallbacks { + var i uint16 + var s string + if err := json.Unmarshal(fb.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(fb.Dest, &s) + } + config.Fallbacks = append(config.Fallbacks, &trojan.Fallback{ + Alpn: fb.Alpn, + Path: fb.Path, + Type: fb.Type, + Dest: s, + Xver: fb.Xver, + }) + } + for _, fb := range config.Fallbacks { + /* + if fb.Alpn == "h2" && fb.Path != "" { + return nil, newError(`Trojan fallbacks: "alpn":"h2" doesn't support "path"`) + } + */ + if fb.Path != "" && fb.Path[0] != '/' { + return nil, newError(`Trojan fallbacks: "path" must be empty or start with "/"`) + } + if fb.Type == "" && fb.Dest != "" { + if fb.Dest == "serve-ws-none" { + fb.Type = "serve" + } else { + switch fb.Dest[0] { + case '@', '/': + fb.Type = "unix" + if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, fb.Dest[1:]) + fb.Dest = string(fullAddr) + } + default: + if _, err := strconv.Atoi(fb.Dest); err == nil { + fb.Dest = "127.0.0.1:" + fb.Dest + } + if _, _, err := net.SplitHostPort(fb.Dest); err == nil { + fb.Type = "tcp" + } + } + } + } + if fb.Type == "" { + return nil, newError(`Trojan fallbacks: please fill in a valid value for every "dest"`) + } + if fb.Xver > 2 { + return nil, newError(`Trojan fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + } + + return config, nil +} diff --git a/infra/conf/vless.go b/infra/conf/vless.go new file mode 100644 index 00000000..c2d5e5ec --- /dev/null +++ b/infra/conf/vless.go @@ -0,0 +1,185 @@ +package conf + +import ( + "encoding/json" + "runtime" + "strconv" + "syscall" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/vless" + "github.com/xtls/xray-core/v1/proxy/vless/inbound" + "github.com/xtls/xray-core/v1/proxy/vless/outbound" +) + +type VLessInboundFallback struct { + Alpn string `json:"alpn"` + Path string `json:"path"` + Type string `json:"type"` + Dest json.RawMessage `json:"dest"` + Xver uint64 `json:"xver"` +} + +type VLessInboundConfig struct { + Clients []json.RawMessage `json:"clients"` + Decryption string `json:"decryption"` + Fallback json.RawMessage `json:"fallback"` + Fallbacks []*VLessInboundFallback `json:"fallbacks"` +} + +// Build implements Buildable +func (c *VLessInboundConfig) Build() (proto.Message, error) { + config := new(inbound.Config) + config.Clients = make([]*protocol.User, len(c.Clients)) + for idx, rawUser := range c.Clients { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, newError(`VLESS clients: invalid user`).Base(err) + } + account := new(vless.Account) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, newError(`VLESS clients: invalid user`).Base(err) + } + + switch account.Flow { + case "", "xtls-rprx-origin", "xtls-rprx-direct": + default: + return nil, newError(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) + } + + if account.Encryption != "" { + return nil, newError(`VLESS clients: "encryption" should not in inbound settings`) + } + + user.Account = serial.ToTypedMessage(account) + config.Clients[idx] = user + } + + if c.Decryption != "none" { + return nil, newError(`VLESS settings: please add/set "decryption":"none" to every settings`) + } + config.Decryption = c.Decryption + + if c.Fallback != nil { + return nil, newError(`VLESS settings: please use "fallbacks":[{}] instead of "fallback":{}`) + } + for _, fb := range c.Fallbacks { + var i uint16 + var s string + if err := json.Unmarshal(fb.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(fb.Dest, &s) + } + config.Fallbacks = append(config.Fallbacks, &inbound.Fallback{ + Alpn: fb.Alpn, + Path: fb.Path, + Type: fb.Type, + Dest: s, + Xver: fb.Xver, + }) + } + for _, fb := range config.Fallbacks { + /* + if fb.Alpn == "h2" && fb.Path != "" { + return nil, newError(`VLESS fallbacks: "alpn":"h2" doesn't support "path"`) + } + */ + if fb.Path != "" && fb.Path[0] != '/' { + return nil, newError(`VLESS fallbacks: "path" must be empty or start with "/"`) + } + if fb.Type == "" && fb.Dest != "" { + if fb.Dest == "serve-ws-none" { + fb.Type = "serve" + } else { + switch fb.Dest[0] { + case '@', '/': + fb.Type = "unix" + if fb.Dest[0] == '@' && len(fb.Dest) > 1 && fb.Dest[1] == '@' && runtime.GOOS == "linux" { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, fb.Dest[1:]) + fb.Dest = string(fullAddr) + } + default: + if _, err := strconv.Atoi(fb.Dest); err == nil { + fb.Dest = "127.0.0.1:" + fb.Dest + } + if _, _, err := net.SplitHostPort(fb.Dest); err == nil { + fb.Type = "tcp" + } + } + } + } + if fb.Type == "" { + return nil, newError(`VLESS fallbacks: please fill in a valid value for every "dest"`) + } + if fb.Xver > 2 { + return nil, newError(`VLESS fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + } + + return config, nil +} + +type VLessOutboundVnext struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} + +type VLessOutboundConfig struct { + Vnext []*VLessOutboundVnext `json:"vnext"` +} + +// Build implements Buildable +func (c *VLessOutboundConfig) Build() (proto.Message, error) { + config := new(outbound.Config) + + if len(c.Vnext) == 0 { + return nil, newError(`VLESS settings: "vnext" is empty`) + } + config.Vnext = make([]*protocol.ServerEndpoint, len(c.Vnext)) + for idx, rec := range c.Vnext { + if rec.Address == nil { + return nil, newError(`VLESS vnext: "address" is not set`) + } + if len(rec.Users) == 0 { + return nil, newError(`VLESS vnext: "users" is empty`) + } + spec := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + User: make([]*protocol.User, len(rec.Users)), + } + for idx, rawUser := range rec.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, newError(`VLESS users: invalid user`).Base(err) + } + account := new(vless.Account) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, newError(`VLESS users: invalid user`).Base(err) + } + + switch account.Flow { + case "", "xtls-rprx-origin", "xtls-rprx-origin-udp443", "xtls-rprx-direct", "xtls-rprx-direct-udp443": + default: + return nil, newError(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) + } + + if account.Encryption != "none" { + return nil, newError(`VLESS users: please add/set "encryption":"none" for every user`) + } + + user.Account = serial.ToTypedMessage(account) + spec.User[idx] = user + } + config.Vnext[idx] = spec + } + + return config, nil +} diff --git a/infra/conf/vless_test.go b/infra/conf/vless_test.go new file mode 100644 index 00000000..cfdde078 --- /dev/null +++ b/infra/conf/vless_test.go @@ -0,0 +1,134 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/vless" + "github.com/xtls/xray-core/v1/proxy/vless/inbound" + "github.com/xtls/xray-core/v1/proxy/vless/outbound" +) + +func TestVLessOutbound(t *testing.T) { + creator := func() Buildable { + return new(VLessOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "vnext": [{ + "address": "example.com", + "port": 443, + "users": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "flow": "xtls-rprx-direct-udp443", + "encryption": "none", + "level": 0 + } + ] + }] + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Vnext: []*protocol.ServerEndpoint{ + { + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Domain{ + Domain: "example.com", + }, + }, + Port: 443, + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-direct-udp443", + Encryption: "none", + }), + Level: 0, + }, + }, + }, + }, + }, + }, + }) +} + +func TestVLessInbound(t *testing.T) { + creator := func() Buildable { + return new(VLessInboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "clients": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "flow": "xtls-rprx-direct", + "level": 0, + "email": "love@example.com" + } + ], + "decryption": "none", + "fallbacks": [ + { + "dest": 80 + }, + { + "alpn": "h2", + "dest": "@/dev/shm/domain.socket", + "xver": 2 + }, + { + "path": "/innerws", + "dest": "serve-ws-none" + } + ] + }`, + Parser: loadJSON(creator), + Output: &inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-direct", + }), + Level: 0, + Email: "love@example.com", + }, + }, + Decryption: "none", + Fallbacks: []*inbound.Fallback{ + { + Alpn: "", + Path: "", + Type: "tcp", + Dest: "127.0.0.1:80", + Xver: 0, + }, + { + Alpn: "h2", + Path: "", + Type: "unix", + Dest: "@/dev/shm/domain.socket", + Xver: 2, + }, + { + Alpn: "", + Path: "/innerws", + Type: "serve", + Dest: "serve-ws-none", + Xver: 0, + }, + }, + }, + }, + }) +} diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go new file mode 100644 index 00000000..b8e0b7d4 --- /dev/null +++ b/infra/conf/vmess.go @@ -0,0 +1,159 @@ +package conf + +import ( + "encoding/json" + "strings" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" +) + +type VMessAccount struct { + ID string `json:"id"` + AlterIds uint16 `json:"alterId"` + Security string `json:"security"` +} + +// Build implements Buildable +func (a *VMessAccount) Build() *vmess.Account { + var st protocol.SecurityType + switch strings.ToLower(a.Security) { + case "aes-128-gcm": + st = protocol.SecurityType_AES128_GCM + case "chacha20-poly1305": + st = protocol.SecurityType_CHACHA20_POLY1305 + case "auto": + st = protocol.SecurityType_AUTO + case "none": + st = protocol.SecurityType_NONE + default: + st = protocol.SecurityType_AUTO + } + return &vmess.Account{ + Id: a.ID, + AlterId: uint32(a.AlterIds), + SecuritySettings: &protocol.SecurityConfig{ + Type: st, + }, + } +} + +type VMessDetourConfig struct { + ToTag string `json:"to"` +} + +// Build implements Buildable +func (c *VMessDetourConfig) Build() *inbound.DetourConfig { + return &inbound.DetourConfig{ + To: c.ToTag, + } +} + +type FeaturesConfig struct { + Detour *VMessDetourConfig `json:"detour"` +} + +type VMessDefaultConfig struct { + AlterIDs uint16 `json:"alterId"` + Level byte `json:"level"` +} + +// Build implements Buildable +func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig { + config := new(inbound.DefaultConfig) + config.AlterId = uint32(c.AlterIDs) + config.Level = uint32(c.Level) + return config +} + +type VMessInboundConfig struct { + Users []json.RawMessage `json:"clients"` + Features *FeaturesConfig `json:"features"` + Defaults *VMessDefaultConfig `json:"default"` + DetourConfig *VMessDetourConfig `json:"detour"` + SecureOnly bool `json:"disableInsecureEncryption"` +} + +// Build implements Buildable +func (c *VMessInboundConfig) Build() (proto.Message, error) { + config := &inbound.Config{ + SecureEncryptionOnly: c.SecureOnly, + } + + if c.Defaults != nil { + config.Default = c.Defaults.Build() + } + + if c.DetourConfig != nil { + config.Detour = c.DetourConfig.Build() + } else if c.Features != nil && c.Features.Detour != nil { + config.Detour = c.Features.Detour.Build() + } + + config.User = make([]*protocol.User, len(c.Users)) + for idx, rawData := range c.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawData, user); err != nil { + return nil, newError("invalid VMess user").Base(err) + } + account := new(VMessAccount) + if err := json.Unmarshal(rawData, account); err != nil { + return nil, newError("invalid VMess user").Base(err) + } + user.Account = serial.ToTypedMessage(account.Build()) + config.User[idx] = user + } + + return config, nil +} + +type VMessOutboundTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} +type VMessOutboundConfig struct { + Receivers []*VMessOutboundTarget `json:"vnext"` +} + +// Build implements Buildable +func (c *VMessOutboundConfig) Build() (proto.Message, error) { + config := new(outbound.Config) + + if len(c.Receivers) == 0 { + return nil, newError("0 VMess receiver configured") + } + serverSpecs := make([]*protocol.ServerEndpoint, len(c.Receivers)) + for idx, rec := range c.Receivers { + if len(rec.Users) == 0 { + return nil, newError("0 user configured for VMess outbound") + } + if rec.Address == nil { + return nil, newError("address is not set in VMess outbound config") + } + spec := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + } + for _, rawUser := range rec.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, newError("invalid VMess user").Base(err) + } + account := new(VMessAccount) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, newError("invalid VMess user").Base(err) + } + user.Account = serial.ToTypedMessage(account.Build()) + spec.User = append(spec.User, user) + } + serverSpecs[idx] = spec + } + config.Receiver = serverSpecs + return config, nil +} diff --git a/infra/conf/vmess_test.go b/infra/conf/vmess_test.go new file mode 100644 index 00000000..9ad0db5e --- /dev/null +++ b/infra/conf/vmess_test.go @@ -0,0 +1,117 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" +) + +func TestVMessOutbound(t *testing.T) { + creator := func() Buildable { + return new(VMessOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "vnext": [{ + "address": "127.0.0.1", + "port": 80, + "users": [ + { + "id": "e641f5ad-9397-41e3-bf1a-e8740dfed019", + "email": "love@example.com", + "level": 255 + } + ] + }] + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 80, + User: []*protocol.User{ + { + Email: "love@example.com", + Level: 255, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", + AlterId: 0, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AUTO, + }, + }), + }, + }, + }, + }, + }, + }, + }) +} + +func TestVMessInbound(t *testing.T) { + creator := func() Buildable { + return new(VMessInboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "clients": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "level": 0, + "alterId": 16, + "email": "love@example.com", + "security": "aes-128-gcm" + } + ], + "default": { + "level": 0, + "alterId": 32 + }, + "detour": { + "to": "tag_to_detour" + }, + "disableInsecureEncryption": true + }`, + Parser: loadJSON(creator), + Output: &inbound.Config{ + User: []*protocol.User{ + { + Level: 0, + Email: "love@example.com", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + AlterId: 16, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + Default: &inbound.DefaultConfig{ + Level: 0, + AlterId: 32, + }, + Detour: &inbound.DetourConfig{ + To: "tag_to_detour", + }, + SecureEncryptionOnly: true, + }, + }, + }) +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go new file mode 100644 index 00000000..17e20fb8 --- /dev/null +++ b/infra/conf/xray.go @@ -0,0 +1,618 @@ +package conf + +import ( + "encoding/json" + "log" + "os" + "strings" + + "github.com/xtls/xray-core/v1/app/dispatcher" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/app/stats" + "github.com/xtls/xray-core/v1/common/serial" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + inboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "dokodemo-door": func() interface{} { return new(DokodemoConfig) }, + "http": func() interface{} { return new(HTTPServerConfig) }, + "shadowsocks": func() interface{} { return new(ShadowsocksServerConfig) }, + "socks": func() interface{} { return new(SocksServerConfig) }, + "vless": func() interface{} { return new(VLessInboundConfig) }, + "vmess": func() interface{} { return new(VMessInboundConfig) }, + "trojan": func() interface{} { return new(TrojanServerConfig) }, + "mtproto": func() interface{} { return new(MTProtoServerConfig) }, + }, "protocol", "settings") + + outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "blackhole": func() interface{} { return new(BlackholeConfig) }, + "freedom": func() interface{} { return new(FreedomConfig) }, + "http": func() interface{} { return new(HTTPClientConfig) }, + "shadowsocks": func() interface{} { return new(ShadowsocksClientConfig) }, + "socks": func() interface{} { return new(SocksClientConfig) }, + "vless": func() interface{} { return new(VLessOutboundConfig) }, + "vmess": func() interface{} { return new(VMessOutboundConfig) }, + "trojan": func() interface{} { return new(TrojanClientConfig) }, + "mtproto": func() interface{} { return new(MTProtoClientConfig) }, + "dns": func() interface{} { return new(DNSOutboundConfig) }, + }, "protocol", "settings") + + ctllog = log.New(os.Stderr, "xctl> ", 0) +) + +func toProtocolList(s []string) ([]proxyman.KnownProtocols, error) { + kp := make([]proxyman.KnownProtocols, 0, 8) + for _, p := range s { + switch strings.ToLower(p) { + case "http": + kp = append(kp, proxyman.KnownProtocols_HTTP) + case "https", "tls", "ssl": + kp = append(kp, proxyman.KnownProtocols_TLS) + default: + return nil, newError("Unknown protocol: ", p) + } + } + return kp, nil +} + +type SniffingConfig struct { + Enabled bool `json:"enabled"` + DestOverride *StringList `json:"destOverride"` +} + +// Build implements Buildable. +func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) { + var p []string + if c.DestOverride != nil { + for _, domainOverride := range *c.DestOverride { + switch strings.ToLower(domainOverride) { + case "http": + p = append(p, "http") + case "tls", "https", "ssl": + p = append(p, "tls") + default: + return nil, newError("unknown protocol: ", domainOverride) + } + } + } + + return &proxyman.SniffingConfig{ + Enabled: c.Enabled, + DestinationOverride: p, + }, nil +} + +type MuxConfig struct { + Enabled bool `json:"enabled"` + Concurrency int16 `json:"concurrency"` +} + +// Build creates MultiplexingConfig, Concurrency < 0 completely disables mux. +func (m *MuxConfig) Build() *proxyman.MultiplexingConfig { + if m.Concurrency < 0 { + return nil + } + + var con uint32 = 8 + if m.Concurrency > 0 { + con = uint32(m.Concurrency) + } + + return &proxyman.MultiplexingConfig{ + Enabled: m.Enabled, + Concurrency: con, + } +} + +type InboundDetourAllocationConfig struct { + Strategy string `json:"strategy"` + Concurrency *uint32 `json:"concurrency"` + RefreshMin *uint32 `json:"refresh"` +} + +// Build implements Buildable. +func (c *InboundDetourAllocationConfig) Build() (*proxyman.AllocationStrategy, error) { + config := new(proxyman.AllocationStrategy) + switch strings.ToLower(c.Strategy) { + case "always": + config.Type = proxyman.AllocationStrategy_Always + case "random": + config.Type = proxyman.AllocationStrategy_Random + case "external": + config.Type = proxyman.AllocationStrategy_External + default: + return nil, newError("unknown allocation strategy: ", c.Strategy) + } + if c.Concurrency != nil { + config.Concurrency = &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ + Value: *c.Concurrency, + } + } + + if c.RefreshMin != nil { + config.Refresh = &proxyman.AllocationStrategy_AllocationStrategyRefresh{ + Value: *c.RefreshMin, + } + } + + return config, nil +} + +type InboundDetourConfig struct { + Protocol string `json:"protocol"` + PortRange *PortRange `json:"port"` + ListenOn *Address `json:"listen"` + Settings *json.RawMessage `json:"settings"` + Tag string `json:"tag"` + Allocation *InboundDetourAllocationConfig `json:"allocate"` + StreamSetting *StreamConfig `json:"streamSettings"` + DomainOverride *StringList `json:"domainOverride"` + SniffingConfig *SniffingConfig `json:"sniffing"` +} + +// Build implements Buildable. +func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { + receiverSettings := &proxyman.ReceiverConfig{} + + if c.ListenOn == nil { + // Listen on anyip, must set PortRange + if c.PortRange == nil { + return nil, newError("Listen on AnyIP but no Port(s) set in InboundDetour.") + } + receiverSettings.PortRange = c.PortRange.Build() + } else { + // Listen on specific IP or Unix Domain Socket + receiverSettings.Listen = c.ListenOn.Build() + listenDS := c.ListenOn.Family().IsDomain() && (c.ListenOn.Domain()[0] == '/' || c.ListenOn.Domain()[0] == '@') + listenIP := c.ListenOn.Family().IsIP() || (c.ListenOn.Family().IsDomain() && c.ListenOn.Domain() == "localhost") + if listenIP { + // Listen on specific IP, must set PortRange + if c.PortRange == nil { + return nil, newError("Listen on specific ip without port in InboundDetour.") + } + // Listen on IP:Port + receiverSettings.PortRange = c.PortRange.Build() + } else if listenDS { + if c.PortRange != nil { + // Listen on Unix Domain Socket, PortRange should be nil + receiverSettings.PortRange = nil + } + } else { + return nil, newError("unable to listen on domain address: ", c.ListenOn.Domain()) + } + } + + if c.Allocation != nil { + concurrency := -1 + if c.Allocation.Concurrency != nil && c.Allocation.Strategy == "random" { + concurrency = int(*c.Allocation.Concurrency) + } + portRange := int(c.PortRange.To - c.PortRange.From + 1) + if concurrency >= 0 && concurrency >= portRange { + return nil, newError("not enough ports. concurrency = ", concurrency, " ports: ", c.PortRange.From, " - ", c.PortRange.To) + } + + as, err := c.Allocation.Build() + if err != nil { + return nil, err + } + receiverSettings.AllocationStrategy = as + } + if c.StreamSetting != nil { + ss, err := c.StreamSetting.Build() + if err != nil { + return nil, err + } + if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) && !strings.EqualFold(c.Protocol, "vless") && !strings.EqualFold(c.Protocol, "trojan") { + return nil, newError("XTLS doesn't supports " + c.Protocol + " for now.") + } + receiverSettings.StreamSettings = ss + } + if c.SniffingConfig != nil { + s, err := c.SniffingConfig.Build() + if err != nil { + return nil, newError("failed to build sniffing config").Base(err) + } + receiverSettings.SniffingSettings = s + } + if c.DomainOverride != nil { + kp, err := toProtocolList(*c.DomainOverride) + if err != nil { + return nil, newError("failed to parse inbound detour config").Base(err) + } + receiverSettings.DomainOverride = kp + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := inboundConfigLoader.LoadWithID(settings, c.Protocol) + if err != nil { + return nil, newError("failed to load inbound detour config.").Base(err) + } + if dokodemoConfig, ok := rawConfig.(*DokodemoConfig); ok { + receiverSettings.ReceiveOriginalDestination = dokodemoConfig.Redirect + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, err + } + + return &core.InboundHandlerConfig{ + Tag: c.Tag, + ReceiverSettings: serial.ToTypedMessage(receiverSettings), + ProxySettings: serial.ToTypedMessage(ts), + }, nil +} + +type OutboundDetourConfig struct { + Protocol string `json:"protocol"` + SendThrough *Address `json:"sendThrough"` + Tag string `json:"tag"` + Settings *json.RawMessage `json:"settings"` + StreamSetting *StreamConfig `json:"streamSettings"` + ProxySettings *ProxyConfig `json:"proxySettings"` + MuxSettings *MuxConfig `json:"mux"` +} + +// Build implements Buildable. +func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { + senderSettings := &proxyman.SenderConfig{} + + if c.SendThrough != nil { + address := c.SendThrough + if address.Family().IsDomain() { + return nil, newError("unable to send through: " + address.String()) + } + senderSettings.Via = address.Build() + } + + if c.StreamSetting != nil { + ss, err := c.StreamSetting.Build() + if err != nil { + return nil, err + } + if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) && !strings.EqualFold(c.Protocol, "vless") && !strings.EqualFold(c.Protocol, "trojan") { + return nil, newError("XTLS doesn't supports " + c.Protocol + " for now.") + } + senderSettings.StreamSettings = ss + } + + if c.ProxySettings != nil { + ps, err := c.ProxySettings.Build() + if err != nil { + return nil, newError("invalid outbound detour proxy settings.").Base(err) + } + senderSettings.ProxySettings = ps + } + + if c.MuxSettings != nil { + ms := c.MuxSettings.Build() + if ms != nil && ms.Enabled { + if ss := senderSettings.StreamSettings; ss != nil { + if ss.SecurityType == serial.GetMessageType(&xtls.Config{}) { + return nil, newError("XTLS doesn't support Mux for now.") + } + } + } + senderSettings.MultiplexSettings = ms + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := outboundConfigLoader.LoadWithID(settings, c.Protocol) + if err != nil { + return nil, newError("failed to parse to outbound detour config.").Base(err) + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, err + } + + return &core.OutboundHandlerConfig{ + SenderSettings: serial.ToTypedMessage(senderSettings), + Tag: c.Tag, + ProxySettings: serial.ToTypedMessage(ts), + }, nil +} + +type StatsConfig struct{} + +// Build implements Buildable. +func (c *StatsConfig) Build() (*stats.Config, error) { + return &stats.Config{}, nil +} + +type Config struct { + // Port of this Point server. + // Deprecated: Port exists for historical compatibility + // and should not be used. + Port uint16 `json:"port"` + + // Deprecated: InboundConfig exists for historical compatibility + // and should not be used. + InboundConfig *InboundDetourConfig `json:"inbound"` + + // Deprecated: OutboundConfig exists for historical compatibility + // and should not be used. + OutboundConfig *OutboundDetourConfig `json:"outbound"` + + // Deprecated: InboundDetours exists for historical compatibility + // and should not be used. + InboundDetours []InboundDetourConfig `json:"inboundDetour"` + + // Deprecated: OutboundDetours exists for historical compatibility + // and should not be used. + OutboundDetours []OutboundDetourConfig `json:"outboundDetour"` + + LogConfig *LogConfig `json:"log"` + RouterConfig *RouterConfig `json:"routing"` + DNSConfig *DNSConfig `json:"dns"` + InboundConfigs []InboundDetourConfig `json:"inbounds"` + OutboundConfigs []OutboundDetourConfig `json:"outbounds"` + Transport *TransportConfig `json:"transport"` + Policy *PolicyConfig `json:"policy"` + API *APIConfig `json:"api"` + Stats *StatsConfig `json:"stats"` + Reverse *ReverseConfig `json:"reverse"` +} + +func (c *Config) findInboundTag(tag string) int { + found := -1 + for idx, ib := range c.InboundConfigs { + if ib.Tag == tag { + found = idx + break + } + } + return found +} + +func (c *Config) findOutboundTag(tag string) int { + found := -1 + for idx, ob := range c.OutboundConfigs { + if ob.Tag == tag { + found = idx + break + } + } + return found +} + +// Override method accepts another Config overrides the current attribute +func (c *Config) Override(o *Config, fn string) { + // only process the non-deprecated members + + if o.LogConfig != nil { + c.LogConfig = o.LogConfig + } + if o.RouterConfig != nil { + c.RouterConfig = o.RouterConfig + } + if o.DNSConfig != nil { + c.DNSConfig = o.DNSConfig + } + if o.Transport != nil { + c.Transport = o.Transport + } + if o.Policy != nil { + c.Policy = o.Policy + } + if o.API != nil { + c.API = o.API + } + if o.Stats != nil { + c.Stats = o.Stats + } + if o.Reverse != nil { + c.Reverse = o.Reverse + } + + // deprecated attrs... keep them for now + if o.InboundConfig != nil { + c.InboundConfig = o.InboundConfig + } + if o.OutboundConfig != nil { + c.OutboundConfig = o.OutboundConfig + } + if o.InboundDetours != nil { + c.InboundDetours = o.InboundDetours + } + if o.OutboundDetours != nil { + c.OutboundDetours = o.OutboundDetours + } + // deprecated attrs + + // update the Inbound in slice if the only one in overide config has same tag + if len(o.InboundConfigs) > 0 { + if len(c.InboundConfigs) > 0 && len(o.InboundConfigs) == 1 { + if idx := c.findInboundTag(o.InboundConfigs[0].Tag); idx > -1 { + c.InboundConfigs[idx] = o.InboundConfigs[0] + ctllog.Println("[", fn, "] updated inbound with tag: ", o.InboundConfigs[0].Tag) + } else { + c.InboundConfigs = append(c.InboundConfigs, o.InboundConfigs[0]) + ctllog.Println("[", fn, "] appended inbound with tag: ", o.InboundConfigs[0].Tag) + } + } else { + c.InboundConfigs = o.InboundConfigs + } + } + + // update the Outbound in slice if the only one in overide config has same tag + if len(o.OutboundConfigs) > 0 { + if len(c.OutboundConfigs) > 0 && len(o.OutboundConfigs) == 1 { + if idx := c.findOutboundTag(o.OutboundConfigs[0].Tag); idx > -1 { + c.OutboundConfigs[idx] = o.OutboundConfigs[0] + ctllog.Println("[", fn, "] updated outbound with tag: ", o.OutboundConfigs[0].Tag) + } else { + if strings.Contains(strings.ToLower(fn), "tail") { + c.OutboundConfigs = append(c.OutboundConfigs, o.OutboundConfigs[0]) + ctllog.Println("[", fn, "] appended outbound with tag: ", o.OutboundConfigs[0].Tag) + } else { + c.OutboundConfigs = append(o.OutboundConfigs, c.OutboundConfigs...) + ctllog.Println("[", fn, "] prepended outbound with tag: ", o.OutboundConfigs[0].Tag) + } + } + } else { + c.OutboundConfigs = o.OutboundConfigs + } + } +} + +func applyTransportConfig(s *StreamConfig, t *TransportConfig) { + if s.TCPSettings == nil { + s.TCPSettings = t.TCPConfig + } + if s.KCPSettings == nil { + s.KCPSettings = t.KCPConfig + } + if s.WSSettings == nil { + s.WSSettings = t.WSConfig + } + if s.HTTPSettings == nil { + s.HTTPSettings = t.HTTPConfig + } + if s.DSSettings == nil { + s.DSSettings = t.DSConfig + } +} + +// Build implements Buildable. +func (c *Config) Build() (*core.Config, error) { + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + } + + if c.API != nil { + apiConf, err := c.API.Build() + if err != nil { + return nil, err + } + config.App = append(config.App, serial.ToTypedMessage(apiConf)) + } + + if c.Stats != nil { + statsConf, err := c.Stats.Build() + if err != nil { + return nil, err + } + config.App = append(config.App, serial.ToTypedMessage(statsConf)) + } + + var logConfMsg *serial.TypedMessage + if c.LogConfig != nil { + logConfMsg = serial.ToTypedMessage(c.LogConfig.Build()) + } else { + logConfMsg = serial.ToTypedMessage(DefaultLogConfig()) + } + // let logger module be the first App to start, + // so that other modules could print log during initiating + config.App = append([]*serial.TypedMessage{logConfMsg}, config.App...) + + if c.RouterConfig != nil { + routerConfig, err := c.RouterConfig.Build() + if err != nil { + return nil, err + } + config.App = append(config.App, serial.ToTypedMessage(routerConfig)) + } + + if c.DNSConfig != nil { + dnsApp, err := c.DNSConfig.Build() + if err != nil { + return nil, newError("failed to parse DNS config").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(dnsApp)) + } + + if c.Policy != nil { + pc, err := c.Policy.Build() + if err != nil { + return nil, err + } + config.App = append(config.App, serial.ToTypedMessage(pc)) + } + + if c.Reverse != nil { + r, err := c.Reverse.Build() + if err != nil { + return nil, err + } + config.App = append(config.App, serial.ToTypedMessage(r)) + } + + var inbounds []InboundDetourConfig + + if c.InboundConfig != nil { + inbounds = append(inbounds, *c.InboundConfig) + } + + if len(c.InboundDetours) > 0 { + inbounds = append(inbounds, c.InboundDetours...) + } + + if len(c.InboundConfigs) > 0 { + inbounds = append(inbounds, c.InboundConfigs...) + } + + // Backward compatibility. + if len(inbounds) > 0 && inbounds[0].PortRange == nil && c.Port > 0 { + inbounds[0].PortRange = &PortRange{ + From: uint32(c.Port), + To: uint32(c.Port), + } + } + + for _, rawInboundConfig := range inbounds { + if c.Transport != nil { + if rawInboundConfig.StreamSetting == nil { + rawInboundConfig.StreamSetting = &StreamConfig{} + } + applyTransportConfig(rawInboundConfig.StreamSetting, c.Transport) + } + ic, err := rawInboundConfig.Build() + if err != nil { + return nil, err + } + config.Inbound = append(config.Inbound, ic) + } + + var outbounds []OutboundDetourConfig + + if c.OutboundConfig != nil { + outbounds = append(outbounds, *c.OutboundConfig) + } + + if len(c.OutboundDetours) > 0 { + outbounds = append(outbounds, c.OutboundDetours...) + } + + if len(c.OutboundConfigs) > 0 { + outbounds = append(outbounds, c.OutboundConfigs...) + } + + for _, rawOutboundConfig := range outbounds { + if c.Transport != nil { + if rawOutboundConfig.StreamSetting == nil { + rawOutboundConfig.StreamSetting = &StreamConfig{} + } + applyTransportConfig(rawOutboundConfig.StreamSetting, c.Transport) + } + oc, err := rawOutboundConfig.Build() + if err != nil { + return nil, err + } + config.Outbound = append(config.Outbound, oc) + } + + return config, nil +} diff --git a/infra/conf/xray_test.go b/infra/conf/xray_test.go new file mode 100644 index 00000000..55ba54f2 --- /dev/null +++ b/infra/conf/xray_test.go @@ -0,0 +1,449 @@ +package conf_test + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/v1/app/dispatcher" + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + core "github.com/xtls/xray-core/v1/core" + . "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/proxy/blackhole" + dns_proxy "github.com/xtls/xray-core/v1/proxy/dns" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/http" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/websocket" +) + +func TestXrayConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(Config) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "outbound": { + "protocol": "freedom", + "settings": {} + }, + "log": { + "access": "/var/log/xray/access.log", + "loglevel": "error", + "error": "/var/log/xray/error.log" + }, + "inbound": { + "streamSettings": { + "network": "ws", + "wsSettings": { + "headers": { + "host": "example.domain" + }, + "path": "" + }, + "tlsSettings": { + "alpn": "h2" + }, + "security": "tls" + }, + "protocol": "vmess", + "port": 443, + "settings": { + "clients": [ + { + "alterId": 100, + "security": "aes-128-gcm", + "id": "0cdf8a45-303d-4fed-9780-29aa7f54175e" + } + ] + } + }, + "inbounds": [{ + "streamSettings": { + "network": "ws", + "wsSettings": { + "headers": { + "host": "example.domain" + }, + "path": "" + }, + "tlsSettings": { + "alpn": "h2" + }, + "security": "tls" + }, + "protocol": "vmess", + "port": "443-500", + "allocate": { + "strategy": "random", + "concurrency": 3 + }, + "settings": { + "clients": [ + { + "alterId": 100, + "security": "aes-128-gcm", + "id": "0cdf8a45-303d-4fed-9780-29aa7f54175e" + } + ] + } + }], + "outboundDetour": [ + { + "tag": "blocked", + "protocol": "blackhole" + }, + { + "protocol": "dns" + } + ], + "routing": { + "strategy": "rules", + "settings": { + "rules": [ + { + "ip": [ + "10.0.0.0/8" + ], + "type": "field", + "outboundTag": "blocked" + } + ] + } + }, + "transport": { + "httpSettings": { + "path": "/test" + } + } + }`, + Parser: createParser(), + Output: &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogType: log.LogType_File, + ErrorLogPath: "/var/log/xray/error.log", + ErrorLogLevel: clog.Severity_Error, + AccessLogType: log.LogType_File, + AccessLogPath: "/var/log/xray/access.log", + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&router.Config{ + DomainStrategy: router.Config_AsIs, + Rule: []*router.RoutingRule{ + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "blocked", + }, + }, + }, + }), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "http", + Settings: serial.ToTypedMessage(&http.Config{ + Path: "/test", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DomainStrategy: freedom.Config_AS_IS, + UserLevel: 0, + }), + }, + { + Tag: "blocked", + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "http", + Settings: serial.ToTypedMessage(&http.Config{ + Path: "/test", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "http", + Settings: serial.ToTypedMessage(&http.Config{ + Path: "/test", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ + Server: &net.Endpoint{}, + }), + }, + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: &net.PortRange{ + From: 443, + To: 443, + }, + StreamSettings: &internet.StreamConfig{ + ProtocolName: "websocket", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(&websocket.Config{ + Header: []*websocket.Header{ + { + Key: "host", + Value: "example.domain", + }, + }, + }), + }, + { + ProtocolName: "http", + Settings: serial.ToTypedMessage(&http.Config{ + Path: "/test", + }), + }, + }, + SecurityType: "xray.transport.internet.tls.Config", + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + NextProtocol: []string{"h2"}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Level: 0, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "0cdf8a45-303d-4fed-9780-29aa7f54175e", + AlterId: 100, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: &net.PortRange{ + From: 443, + To: 500, + }, + AllocationStrategy: &proxyman.AllocationStrategy{ + Type: proxyman.AllocationStrategy_Random, + Concurrency: &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ + Value: 3, + }, + }, + StreamSettings: &internet.StreamConfig{ + ProtocolName: "websocket", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(&websocket.Config{ + Header: []*websocket.Header{ + { + Key: "host", + Value: "example.domain", + }, + }, + }), + }, + { + ProtocolName: "http", + Settings: serial.ToTypedMessage(&http.Config{ + Path: "/test", + }), + }, + }, + SecurityType: "xray.transport.internet.tls.Config", + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + NextProtocol: []string{"h2"}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Level: 0, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "0cdf8a45-303d-4fed-9780-29aa7f54175e", + AlterId: 100, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + }, + }, + }) +} + +func TestMuxConfig_Build(t *testing.T) { + tests := []struct { + name string + fields string + want *proxyman.MultiplexingConfig + }{ + {"default", `{"enabled": true, "concurrency": 16}`, &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 16, + }}, + {"empty def", `{}`, &proxyman.MultiplexingConfig{ + Enabled: false, + Concurrency: 8, + }}, + {"not enable", `{"enabled": false, "concurrency": 4}`, &proxyman.MultiplexingConfig{ + Enabled: false, + Concurrency: 4, + }}, + {"forbidden", `{"enabled": false, "concurrency": -1}`, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &MuxConfig{} + common.Must(json.Unmarshal([]byte(tt.fields), m)) + if got := m.Build(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MuxConfig.Build() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConfig_Override(t *testing.T) { + tests := []struct { + name string + orig *Config + over *Config + fn string + want *Config + }{ + {"combine/empty", + &Config{}, + &Config{ + LogConfig: &LogConfig{}, + RouterConfig: &RouterConfig{}, + DNSConfig: &DNSConfig{}, + Transport: &TransportConfig{}, + Policy: &PolicyConfig{}, + API: &APIConfig{}, + Stats: &StatsConfig{}, + Reverse: &ReverseConfig{}, + }, + "", + &Config{ + LogConfig: &LogConfig{}, + RouterConfig: &RouterConfig{}, + DNSConfig: &DNSConfig{}, + Transport: &TransportConfig{}, + Policy: &PolicyConfig{}, + API: &APIConfig{}, + Stats: &StatsConfig{}, + Reverse: &ReverseConfig{}, + }, + }, + {"combine/newattr", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "old"}}}, + &Config{LogConfig: &LogConfig{}}, "", + &Config{LogConfig: &LogConfig{}, InboundConfigs: []InboundDetourConfig{{Tag: "old"}}}}, + {"replace/inbounds", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}}, + {"replace/inbounds-replaceall", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}}, + {"replace/notag-append", + &Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}, {Tag: "pos1", Protocol: "kcp"}}}}, + {"replace/outbounds", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}}, + {"replace/outbounds-prepend", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}, + "config.json", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}}, + {"replace/outbounds-append", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos2", Protocol: "kcp"}}}, + "config_tail.json", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}, {Tag: "pos2", Protocol: "kcp"}}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.orig.Override(tt.over, tt.fn) + if r := cmp.Diff(tt.orig, tt.want); r != "" { + t.Error(r) + } + }) + } +} diff --git a/infra/vprotogen/main.go b/infra/vprotogen/main.go new file mode 100644 index 00000000..5a286524 --- /dev/null +++ b/infra/vprotogen/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/core" +) + +func main() { + pwd, wdErr := os.Getwd() + if wdErr != nil { + fmt.Println("Can not get current working directory.") + os.Exit(1) + } + + GOBIN := common.GetGOBIN() + binPath := os.Getenv("PATH") + pathSlice := []string{binPath, GOBIN, pwd} + binPath = strings.Join(pathSlice, string(os.PathListSeparator)) + os.Setenv("PATH", binPath) + + EXE := "" + if runtime.GOOS == "windows" { + EXE = ".exe" + } + protoc := "protoc" + EXE + + if path, err := exec.LookPath(protoc); err != nil { + fmt.Println("Make sure that you have `" + protoc + "` in your system path or current path. To download it, please visit https://github.com/protocolbuffers/protobuf/releases") + os.Exit(1) + } else { + protoc = path + } + + protoFilesMap := make(map[string][]string) + walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + + if info.IsDir() { + return nil + } + + dir := filepath.Dir(path) + filename := filepath.Base(path) + if strings.HasSuffix(filename, ".proto") { + protoFilesMap[dir] = append(protoFilesMap[dir], path) + } + + return nil + }) + if walkErr != nil { + fmt.Println(walkErr) + os.Exit(1) + } + + for _, files := range protoFilesMap { + for _, relProtoFile := range files { + var args []string + if core.ProtoFilesUsingProtocGenGoFast[relProtoFile] { + args = []string{"--gofast_out", pwd, "--gofast_opt", "paths=source_relative", "--plugin", "protoc-gen-gofast=" + GOBIN + "/protoc-gen-gofast" + EXE} + } else { + args = []string{"--go_out", pwd, "--go_opt", "paths=source_relative", "--go-grpc_out", pwd, "--go-grpc_opt", "paths=source_relative", "--plugin", "protoc-gen-go=" + GOBIN + "/protoc-gen-go" + EXE, "--plugin", "protoc-gen-go-grpc=" + GOBIN + "/protoc-gen-go-grpc" + EXE} + } + args = append(args, relProtoFile) + cmd := exec.Command(protoc, args...) + cmd.Env = append(cmd.Env, os.Environ()...) + output, cmdErr := cmd.CombinedOutput() + if len(output) > 0 { + fmt.Println(string(output)) + } + if cmdErr != nil { + fmt.Println(cmdErr) + os.Exit(1) + } + } + } +} diff --git a/main/commands/all/api.go b/main/commands/all/api.go new file mode 100644 index 00000000..20aa3fb2 --- /dev/null +++ b/main/commands/all/api.go @@ -0,0 +1,164 @@ +package all + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + + logService "github.com/xtls/xray-core/v1/app/log/command" + statsService "github.com/xtls/xray-core/v1/app/stats/command" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +// cmdAPI calls an API in an Xray process +var cmdAPI = &base.Command{ + UsageLine: "{{.Exec}} api [-server 127.0.0.1:8080] ", + Short: "Call an API in a Xray process", + Long: ` +Call an API in a Xray process, API calls in this command have a timeout to the server of 3 seconds. + +The following methods are currently supported: + + LoggerService.RestartLogger + StatsService.GetStats + StatsService.QueryStats + +Examples: + + {{.Exec}} api --server=127.0.0.1:8080 LoggerService.RestartLogger '' + {{.Exec}} api --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: "" reset: false' + {{.Exec}} api --server=127.0.0.1:8080 StatsService.GetStats 'name: "inbound>>>statin>>>traffic>>>downlink" reset: false' + {{.Exec}} api --server=127.0.0.1:8080 StatsService.GetSysStats '' + `, +} + +func init() { + cmdAPI.Run = executeAPI // break init loop +} + +var ( + apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "") +) + +func executeAPI(cmd *base.Command, args []string) { + unnamedArgs := cmdAPI.Flag.Args() + if len(unnamedArgs) < 2 { + base.Fatalf("service name or request not specified.") + } + + service, method := getServiceMethod(unnamedArgs[0]) + handler, found := serivceHandlerMap[strings.ToLower(service)] + if !found { + base.Fatalf("unknown service: %s", service) + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + conn, err := grpc.DialContext(ctx, *apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + base.Fatalf("failed to dial %s", *apiServerAddrPtr) + } + defer conn.Close() + + response, err := handler(ctx, conn, method, unnamedArgs[1]) + if err != nil { + base.Fatalf("failed to call service %s", unnamedArgs[0]) + } + + fmt.Println(response) +} + +func getServiceMethod(s string) (string, string) { + ss := strings.Split(s, ".") + service := ss[0] + var method string + if len(ss) > 1 { + method = ss[1] + } + return service, method +} + +type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) + +var serivceHandlerMap = map[string]serviceHandler{ + "statsservice": callStatsService, + "loggerservice": callLogService, +} + +func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) { + client := logService.NewLoggerServiceClient(conn) + + switch strings.ToLower(method) { + case "restartlogger": + r := &logService.RestartLoggerRequest{} + if err := proto.Unmarshal([]byte(request), r); err != nil { + return "", err + } + resp, err := client.RestartLogger(ctx, r) + if err != nil { + return "", err + } + m, err := proto.Marshal(resp) + if err != nil { + return "", err + } + return string(m), nil + default: + return "", errors.New("Unknown method: " + method) + } +} + +func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) { + client := statsService.NewStatsServiceClient(conn) + + switch strings.ToLower(method) { + case "getstats": + r := &statsService.GetStatsRequest{} + if err := proto.Unmarshal([]byte(request), r); err != nil { + return "", err + } + resp, err := client.GetStats(ctx, r) + if err != nil { + return "", err + } + m, err := proto.Marshal(resp) + if err != nil { + return "", err + } + return string(m), nil + case "querystats": + r := &statsService.QueryStatsRequest{} + if err := proto.Unmarshal([]byte(request), r); err != nil { + return "", err + } + resp, err := client.QueryStats(ctx, r) + if err != nil { + return "", err + } + m, err := proto.Marshal(resp) + if err != nil { + return "", err + } + return string(m), nil + case "getsysstats": + // SysStatsRequest is an empty message + r := &statsService.SysStatsRequest{} + resp, err := client.GetSysStats(ctx, r) + if err != nil { + return "", err + } + m, err := proto.Marshal(resp) + if err != nil { + return "", err + } + return string(m), nil + default: + return "", errors.New("Unknown method: " + method) + } +} diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go new file mode 100644 index 00000000..fe08c2f8 --- /dev/null +++ b/main/commands/all/commands.go @@ -0,0 +1,15 @@ +package all + +import "github.com/xtls/xray-core/v1/main/commands/base" + +// go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +func init() { + base.RootCommand.Commands = append( + base.RootCommand.Commands, + cmdAPI, + cmdConvert, + cmdTLS, + cmdUUID, + ) +} diff --git a/main/commands/all/convert.go b/main/commands/all/convert.go new file mode 100644 index 00000000..d98813b1 --- /dev/null +++ b/main/commands/all/convert.go @@ -0,0 +1,126 @@ +package all + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/infra/conf/serial" + "github.com/xtls/xray-core/v1/main/commands/base" + "google.golang.org/protobuf/proto" +) + +var cmdConvert = &base.Command{ + UsageLine: "{{.Exec}} convert [json file] [json file] ...", + Short: "Convert multiple json config to protobuf", + Long: ` +Convert multiple json config to protobuf. + +Examples: + + {{.Exec}} convert config.json c1.json c2.json .json + `, +} + +func init() { + cmdConvert.Run = executeConvert // break init loop +} + +func executeConvert(cmd *base.Command, args []string) { + unnamedArgs := cmdConvert.Flag.Args() + if len(unnamedArgs) < 1 { + base.Fatalf("empty config list") + } + + conf := &conf.Config{} + for _, arg := range unnamedArgs { + fmt.Fprintf(os.Stderr, "Read config: %s", arg) + r, err := loadArg(arg) + common.Must(err) + c, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf(err.Error()) + } + conf.Override(c, arg) + } + + pbConfig, err := conf.Build() + if err != nil { + base.Fatalf(err.Error()) + } + + bytesConfig, err := proto.Marshal(pbConfig) + if err != nil { + base.Fatalf("failed to marshal proto config: %s", err) + } + + if _, err := os.Stdout.Write(bytesConfig); err != nil { + base.Fatalf("failed to write proto config: %s", err) + } +} + +// loadArg loads one arg, maybe an remote url, or local file path +func loadArg(arg string) (out io.Reader, err error) { + var data []byte + switch { + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + data, err = FetchHTTPContent(arg) + + case arg == "stdin:": + data, err = ioutil.ReadAll(os.Stdin) + + default: + data, err = ioutil.ReadFile(arg) + } + + if err != nil { + return + } + out = bytes.NewBuffer(data) + return +} + +// FetchHTTPContent dials https for remote content +func FetchHTTPContent(target string) ([]byte, error) { + parsedTarget, err := url.Parse(target) + if err != nil { + return nil, newError("invalid URL: ", target).Base(err) + } + + if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { + return nil, newError("invalid scheme: ", parsedTarget.Scheme) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(&http.Request{ + Method: "GET", + URL: parsedTarget, + Close: true, + }) + if err != nil { + return nil, newError("failed to dial to ", target).Base(err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, newError("unexpected HTTP status code: ", resp.StatusCode) + } + + content, err := buf.ReadAllToBytes(resp.Body) + if err != nil { + return nil, newError("failed to read HTTP response").Base(err) + } + + return content, nil +} diff --git a/main/commands/all/errors.generated.go b/main/commands/all/errors.generated.go new file mode 100644 index 00000000..254912c1 --- /dev/null +++ b/main/commands/all/errors.generated.go @@ -0,0 +1,9 @@ +package all + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/commands/all/tls.go b/main/commands/all/tls.go new file mode 100644 index 00000000..2274d535 --- /dev/null +++ b/main/commands/all/tls.go @@ -0,0 +1,18 @@ +package all + +import ( + "github.com/xtls/xray-core/v1/main/commands/all/tlscmd" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +var cmdTLS = &base.Command{ + UsageLine: "{{.Exec}} tls", + Short: "TLS tools", + Long: `{{.Exec}} tls provides tools for TLS. + `, + + Commands: []*base.Command{ + tlscmd.CmdCert, + tlscmd.CmdPing, + }, +} diff --git a/main/commands/all/tlscmd/cert.go b/main/commands/all/tlscmd/cert.go new file mode 100644 index 00000000..d4372109 --- /dev/null +++ b/main/commands/all/tlscmd/cert.go @@ -0,0 +1,138 @@ +package tlscmd + +import ( + "context" + "crypto/x509" + "encoding/json" + "os" + "strings" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +// CmdCert is the tls cert command +var CmdCert = &base.Command{ + UsageLine: "{{.Exec}} tls cert [--ca] [--domain=xray.com] [--expire=240h]", + Short: "Generate TLS certificates", + Long: ` +Generate TLS certificates. + +The -domain=domain_name flag sets the domain name for the +certificate. + +The -org=organization flag sets the organization name for the +certificate. + +The -ca flag sets whether this certificate is a CA + +The -json flag sets the output of certificate to JSON + +The -file flag sets the certificate path to save. + +The -expire flag expire time of the certificate. Default +value 3 months. + `, +} + +func init() { + CmdCert.Run = executeCert // break init loop +} + +var ( + certDomainNames stringList + _ = func() bool { + CmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate") + return true + }() + + certCommonName = CmdCert.Flag.String("name", "Xray Inc", "The common name of this certificate") + certOrganization = CmdCert.Flag.String("org", "Xray Inc", "Organization of the certificate") + certIsCA = CmdCert.Flag.Bool("ca", false, "Whether this certificate is a CA") + certJSONOutput = CmdCert.Flag.Bool("json", true, "Print certificate in JSON format") + certFileOutput = CmdCert.Flag.String("file", "", "Save certificate in file.") + certExpire = CmdCert.Flag.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.") +) + +func executeCert(cmd *base.Command, args []string) { + var opts []cert.Option + if *certIsCA { + opts = append(opts, cert.Authority(*certIsCA)) + opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature)) + } + + opts = append(opts, cert.NotAfter(time.Now().Add(*certExpire))) + opts = append(opts, cert.CommonName(*certCommonName)) + if len(certDomainNames) > 0 { + opts = append(opts, cert.DNSNames(certDomainNames...)) + } + opts = append(opts, cert.Organization(*certOrganization)) + + cert, err := cert.Generate(nil, opts...) + if err != nil { + base.Fatalf("failed to generate TLS certificate: %s", err) + } + + if *certJSONOutput { + printJSON(cert) + } + + if len(*certFileOutput) > 0 { + if err := printFile(cert, *certFileOutput); err != nil { + base.Fatalf("failed to save file: %s", err) + } + } +} + +func printJSON(certificate *cert.Certificate) { + certPEM, keyPEM := certificate.ToPEM() + jCert := &jsonCert{ + Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"), + Key: strings.Split(strings.TrimSpace(string(keyPEM)), "\n"), + } + content, err := json.MarshalIndent(jCert, "", " ") + common.Must(err) + os.Stdout.Write(content) + os.Stdout.WriteString("\n") +} + +func writeFile(content []byte, name string) error { + f, err := os.Create(name) + if err != nil { + return err + } + defer f.Close() + + return common.Error2(f.Write(content)) +} + +func printFile(certificate *cert.Certificate, name string) error { + certPEM, keyPEM := certificate.ToPEM() + return task.Run(context.Background(), func() error { + return writeFile(certPEM, name+"_cert.pem") + }, func() error { + return writeFile(keyPEM, name+"_key.pem") + }) +} + +type stringList []string + +func (l *stringList) String() string { + return "String list" +} + +func (l *stringList) Set(v string) error { + if v == "" { + base.Fatalf("empty value") + } + *l = append(*l, v) + return nil +} + +type jsonCert struct { + Certificate []string `json:"certificate"` + Key []string `json:"key"` +} diff --git a/main/commands/all/tlscmd/ping.go b/main/commands/all/tlscmd/ping.go new file mode 100644 index 00000000..09772a4e --- /dev/null +++ b/main/commands/all/tlscmd/ping.go @@ -0,0 +1,111 @@ +package tlscmd + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net" + + "github.com/xtls/xray-core/v1/main/commands/base" +) + +// CmdPing is the tls ping command +var CmdPing = &base.Command{ + UsageLine: "{{.Exec}} tls ping [-ip ] ", + Short: "Ping the domain with TLS handshake", + Long: ` +Ping the domain with TLS handshake. + +The -ip flag sets the IP address of the domain. + `, +} + +func init() { + CmdPing.Run = executePing // break init loop +} + +var ( + pingIPStr = CmdPing.Flag.String("ip", "", "") +) + +func executePing(cmd *base.Command, args []string) { + if CmdPing.Flag.NArg() < 1 { + base.Fatalf("domain not specified") + } + + domain := CmdPing.Flag.Arg(0) + fmt.Println("Tls ping: ", domain) + + var ip net.IP + if len(*pingIPStr) > 0 { + v := net.ParseIP(*pingIPStr) + if v == nil { + base.Fatalf("invalid IP: %s", *pingIPStr) + } + ip = v + } else { + v, err := net.ResolveIPAddr("ip", domain) + if err != nil { + base.Fatalf("Failed to resolve IP: %s", err) + } + ip = v.IP + } + fmt.Println("Using IP: ", ip.String()) + + fmt.Println("-------------------") + fmt.Println("Pinging without SNI") + { + tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443}) + if err != nil { + base.Fatalf("Failed to dial tcp: %s", err) + } + tlsConn := tls.Client(tcpConn, &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"http/1.1"}, + MaxVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS12, + }) + err = tlsConn.Handshake() + if err != nil { + fmt.Println("Handshake failure: ", err) + } else { + fmt.Println("Handshake succeeded") + printCertificates(tlsConn.ConnectionState().PeerCertificates) + } + tlsConn.Close() + } + + fmt.Println("-------------------") + fmt.Println("Pinging with SNI") + { + tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443}) + if err != nil { + base.Fatalf("Failed to dial tcp: %s", err) + } + tlsConn := tls.Client(tcpConn, &tls.Config{ + ServerName: domain, + NextProtos: []string{"http/1.1"}, + MaxVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS12, + }) + err = tlsConn.Handshake() + if err != nil { + fmt.Println("handshake failure: ", err) + } else { + fmt.Println("handshake succeeded") + printCertificates(tlsConn.ConnectionState().PeerCertificates) + } + tlsConn.Close() + } + + fmt.Println("Tls ping finished") +} + +func printCertificates(certs []*x509.Certificate) { + for _, cert := range certs { + if len(cert.DNSNames) == 0 { + continue + } + fmt.Println("Allowed domains: ", cert.DNSNames) + } +} diff --git a/main/commands/all/uuid.go b/main/commands/all/uuid.go new file mode 100644 index 00000000..0fdbdb17 --- /dev/null +++ b/main/commands/all/uuid.go @@ -0,0 +1,22 @@ +package all + +import ( + "fmt" + + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +var cmdUUID = &base.Command{ + UsageLine: "{{.Exec}} uuid", + Short: "Generate new UUIDs", + Long: ` +Generate new UUIDs. + `, + Run: executeUUID, +} + +func executeUUID(cmd *base.Command, args []string) { + u := uuid.New() + fmt.Println(u.String()) +} diff --git a/main/commands/base/command.go b/main/commands/base/command.go new file mode 100644 index 00000000..0dcdc1d3 --- /dev/null +++ b/main/commands/base/command.go @@ -0,0 +1,122 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package base defines shared basic pieces of the commands, +// in particular logging and the Command structure. +package base + +import ( + "flag" + "fmt" + "os" + "strings" + "sync" +) + +// A Command is an implementation of a xray command +// like xray run or xray version. +type Command struct { + // Run runs the command. + // The args are the arguments after the command name. + Run func(cmd *Command, args []string) + + // UsageLine is the one-line usage message. + // The words between "go" and the first flag or argument in the line are taken to be the command name. + UsageLine string + + // Short is the short description shown in the 'go help' output. + Short string + + // Long is the long message shown in the 'go help ' output. + Long string + + // Flag is a set of flags specific to this command. + Flag flag.FlagSet + + // CustomFlags indicates that the command will do its own + // flag parsing. + CustomFlags bool + + // Commands lists the available commands and help topics. + // The order here is the order in which they are printed by 'go help'. + // Note that subcommands are in general best avoided. + Commands []*Command +} + +// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument, +func (c *Command) LongName() string { + name := c.UsageLine + if i := strings.Index(name, " ["); i >= 0 { + name = name[:i] + } + if name == CommandEnv.Exec { + return "" + } + return strings.TrimPrefix(name, CommandEnv.Exec+" ") +} + +// Name returns the command's short name: the last word in the usage line before a flag or argument. +func (c *Command) Name() string { + name := c.LongName() + if i := strings.LastIndex(name, " "); i >= 0 { + name = name[i+1:] + } + return name +} + +// Usage prints usage of the Command +func (c *Command) Usage() { + fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine) + fmt.Fprintf(os.Stderr, "Run 'xray help %s' for details.\n", c.LongName()) + SetExitStatus(2) + Exit() +} + +// Runnable reports whether the command can be run; otherwise +// it is a documentation pseudo-command such as importpath. +func (c *Command) Runnable() bool { + return c.Run != nil +} + +// Exit exits with code set with SetExitStatus() +func Exit() { + os.Exit(exitStatus) +} + +// Fatalf logs error and exit with code 1 +func Fatalf(format string, args ...interface{}) { + Errorf(format, args...) + Exit() +} + +// Errorf logs error and set exit status to 1, but not exit +func Errorf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + fmt.Fprintln(os.Stderr) + SetExitStatus(1) +} + +// ExitIfErrors exits if current status is not zero +func ExitIfErrors() { + if exitStatus != 0 { + Exit() + } +} + +var exitStatus = 0 +var exitMu sync.Mutex + +// SetExitStatus set exit status code +func SetExitStatus(n int) { + exitMu.Lock() + if exitStatus < n { + exitStatus = n + } + exitMu.Unlock() +} + +// GetExitStatus get exit status code +func GetExitStatus() int { + return exitStatus +} diff --git a/main/commands/base/env.go b/main/commands/base/env.go new file mode 100644 index 00000000..ffb71503 --- /dev/null +++ b/main/commands/base/env.go @@ -0,0 +1,23 @@ +package base + +import ( + "os" + "path" +) + +// CommandEnvHolder is a struct holds the environment info of commands +type CommandEnvHolder struct { + Exec string +} + +// CommandEnv holds the environment info of commands +var CommandEnv CommandEnvHolder + +func init() { + exec, err := os.Executable() + if err != nil { + return + } + CommandEnv.Exec = path.Base(exec) + CommandEnv.Exec = "xray" +} diff --git a/main/commands/base/execute.go b/main/commands/base/execute.go new file mode 100644 index 00000000..b0aba96b --- /dev/null +++ b/main/commands/base/execute.go @@ -0,0 +1,88 @@ +package base + +import ( + "flag" + "fmt" + "os" + "sort" + "strings" +) + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// copied from "github.com/golang/go/main.go" + +// Execute excute the commands +func Execute() { + buildCommandsText(RootCommand) + flag.Parse() + args := flag.Args() + if len(args) < 1 { + PrintUsage(os.Stderr, RootCommand) + return + } + cmdName := args[0] // for error messages + if args[0] == "help" { + Help(os.Stdout, args[1:]) + return + } + +BigCmdLoop: + for bigCmd := RootCommand; ; { + for _, cmd := range bigCmd.Commands { + if cmd.Name() != args[0] { + continue + } + if len(cmd.Commands) > 0 { + // test sub commands + bigCmd = cmd + args = args[1:] + if len(args) == 0 { + PrintUsage(os.Stderr, bigCmd) + SetExitStatus(2) + Exit() + } + if args[0] == "help" { + // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'. + Help(os.Stdout, append(strings.Split(cmdName, " "), args[1:]...)) + return + } + cmdName += " " + args[0] + continue BigCmdLoop + } + if !cmd.Runnable() { + continue + } + cmd.Flag.Usage = func() { cmd.Usage() } + if cmd.CustomFlags { + args = args[1:] + } else { + cmd.Flag.Parse(args[1:]) + args = cmd.Flag.Args() + } + + cmd.Run(cmd, args) + Exit() + return + } + helpArg := "" + if i := strings.LastIndex(cmdName, " "); i >= 0 { + helpArg = " " + cmdName[:i] + } + fmt.Fprintf(os.Stderr, "%s %s: unknown command\nRun '%s help%s' for usage.\n", CommandEnv.Exec, cmdName, CommandEnv.Exec, helpArg) + SetExitStatus(2) + Exit() + } +} + +// Sort sorts the commands +func Sort() { + sort.Slice(RootCommand.Commands, func(i, j int) bool { + return SortLessFunc(RootCommand.Commands[i], RootCommand.Commands[j]) + }) +} + +// SortLessFunc used for sort commands list, can be override from outside +var SortLessFunc = func(i, j *Command) bool { + return i.Name() < j.Name() +} diff --git a/main/commands/base/help.go b/main/commands/base/help.go new file mode 100644 index 00000000..89dbb6dd --- /dev/null +++ b/main/commands/base/help.go @@ -0,0 +1,157 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package base + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + "text/template" + "unicode" + "unicode/utf8" +) + +// Help implements the 'help' command. +func Help(w io.Writer, args []string) { + cmd := RootCommand +Args: + for i, arg := range args { + for _, sub := range cmd.Commands { + if sub.Name() == arg { + cmd = sub + continue Args + } + } + + // helpSuccess is the help command using as many args as possible that would succeed. + helpSuccess := CommandEnv.Exec + " help" + if i > 0 { + helpSuccess += " " + strings.Join(args[:i], " ") + } + fmt.Fprintf(os.Stderr, "%s help %s: unknown help topic. Run '%s'.\n", CommandEnv.Exec, strings.Join(args, " "), helpSuccess) + SetExitStatus(2) // failed at 'xray help cmd' + Exit() + } + + if len(cmd.Commands) > 0 { + PrintUsage(os.Stdout, cmd) + } else { + tmpl(os.Stdout, helpTemplate, makeTmplData(cmd)) + } +} + +var usageTemplate = `{{.Long | trim}} + +Usage: + + {{.Exec}} [arguments] + +The commands are: +{{range .Commands}}{{if and (ne .Short "") (or (.Runnable) .Commands)}} + {{.Name | printf "%-12s"}} {{.Short}}{{end}}{{end}} + +Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} " for more information about a command. +` + +// APPEND FOLLOWING TO 'usageTemplate' IF YOU WANT DOC, +// A DOC TOPIC IS JUST A COMMAND NOT RUNNABLE: +// +// {{if eq (.UsageLine) (.Exec)}} +// Additional help topics: +// {{range .Commands}}{{if and (not .Runnable) (not .Commands)}} +// {{.Name | printf "%-15s"}} {{.Short}}{{end}}{{end}} +// +// Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} " for more information about that topic. +// {{end}} + +var helpTemplate = `{{if .Runnable}}usage: {{.UsageLine}} + +{{end}}{{.Long | trim}} +` + +// An errWriter wraps a writer, recording whether a write error occurred. +type errWriter struct { + w io.Writer + err error +} + +func (w *errWriter) Write(b []byte) (int, error) { + n, err := w.w.Write(b) + if err != nil { + w.err = err + } + return n, err +} + +// tmpl executes the given template text on data, writing the result to w. +func tmpl(w io.Writer, text string, data interface{}) { + t := template.New("top") + t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize}) + template.Must(t.Parse(text)) + ew := &errWriter{w: w} + err := t.Execute(ew, data) + if ew.err != nil { + // I/O error writing. Ignore write on closed pipe. + if strings.Contains(ew.err.Error(), "pipe") { + SetExitStatus(1) + Exit() + } + Fatalf("writing output: %v", ew.err) + } + if err != nil { + panic(err) + } +} + +func capitalize(s string) string { + if s == "" { + return s + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToTitle(r)) + s[n:] +} + +// PrintUsage prints usage of cmd to w +func PrintUsage(w io.Writer, cmd *Command) { + bw := bufio.NewWriter(w) + tmpl(bw, usageTemplate, makeTmplData(cmd)) + bw.Flush() +} + +// buildCommandsText build text of command and its children as template +func buildCommandsText(cmd *Command) { + buildCommandText(cmd) + for _, cmd := range cmd.Commands { + buildCommandsText(cmd) + } +} + +// buildCommandText build command text as template +func buildCommandText(cmd *Command) { + cmd.UsageLine = buildText(cmd.UsageLine, makeTmplData(cmd)) + cmd.Short = buildText(cmd.Short, makeTmplData(cmd)) + cmd.Long = buildText(cmd.Long, makeTmplData(cmd)) +} + +func buildText(text string, data interface{}) string { + buf := bytes.NewBuffer([]byte{}) + tmpl(buf, text, data) + return buf.String() +} + +type tmplData struct { + *Command + *CommandEnvHolder +} + +func makeTmplData(cmd *Command) tmplData { + return tmplData{ + Command: cmd, + CommandEnvHolder: &CommandEnv, + } +} diff --git a/main/commands/base/root.go b/main/commands/base/root.go new file mode 100644 index 00000000..8f3bf82b --- /dev/null +++ b/main/commands/base/root.go @@ -0,0 +1,16 @@ +package base + +// RootCommand is the root command of all commands +var RootCommand *Command + +func init() { + RootCommand = &Command{ + UsageLine: CommandEnv.Exec, + Long: "The root command", + } +} + +// RegisterCommand register a command to RootCommand +func RegisterCommand(cmd *Command) { + RootCommand.Commands = append(RootCommand.Commands, cmd) +} diff --git a/main/confloader/confloader.go b/main/confloader/confloader.go new file mode 100644 index 00000000..c0e4d62f --- /dev/null +++ b/main/confloader/confloader.go @@ -0,0 +1,34 @@ +package confloader + +import ( + "io" + "os" +) + +type configFileLoader func(string) (io.Reader, error) +type extconfigLoader func([]string, io.Reader) (io.Reader, error) + +var ( + EffectiveConfigFileLoader configFileLoader + EffectiveExtConfigLoader extconfigLoader +) + +// LoadConfig reads from a path/url/stdin +// actual work is in external module +func LoadConfig(file string) (io.Reader, error) { + if EffectiveConfigFileLoader == nil { + newError("external config module not loaded, reading from stdin").AtInfo().WriteToLog() + return os.Stdin, nil + } + return EffectiveConfigFileLoader(file) +} + +// LoadExtConfig calls xctl to handle multiple config +// the actual work also in external module +func LoadExtConfig(files []string, reader io.Reader) (io.Reader, error) { + if EffectiveExtConfigLoader == nil { + return nil, newError("external config module not loaded").AtError() + } + + return EffectiveExtConfigLoader(files, reader) +} diff --git a/main/confloader/errors.generated.go b/main/confloader/errors.generated.go new file mode 100644 index 00000000..037851a8 --- /dev/null +++ b/main/confloader/errors.generated.go @@ -0,0 +1,9 @@ +package confloader + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/confloader/external/errors.generated.go b/main/confloader/external/errors.generated.go new file mode 100644 index 00000000..b1dcb94f --- /dev/null +++ b/main/confloader/external/errors.generated.go @@ -0,0 +1,9 @@ +package external + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go new file mode 100644 index 00000000..eaa9e835 --- /dev/null +++ b/main/confloader/external/external.go @@ -0,0 +1,87 @@ +package external + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/platform/ctlcmd" + "github.com/xtls/xray-core/v1/main/confloader" +) + +func ConfigLoader(arg string) (out io.Reader, err error) { + var data []byte + switch { + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + data, err = FetchHTTPContent(arg) + + case arg == "stdin:": + data, err = ioutil.ReadAll(os.Stdin) + + default: + data, err = ioutil.ReadFile(arg) + } + + if err != nil { + return + } + out = bytes.NewBuffer(data) + return +} + +func FetchHTTPContent(target string) ([]byte, error) { + parsedTarget, err := url.Parse(target) + if err != nil { + return nil, newError("invalid URL: ", target).Base(err) + } + + if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { + return nil, newError("invalid scheme: ", parsedTarget.Scheme) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(&http.Request{ + Method: "GET", + URL: parsedTarget, + Close: true, + }) + if err != nil { + return nil, newError("failed to dial to ", target).Base(err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, newError("unexpected HTTP status code: ", resp.StatusCode) + } + + content, err := buf.ReadAllToBytes(resp.Body) + if err != nil { + return nil, newError("failed to read HTTP response").Base(err) + } + + return content, nil +} + +func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, error) { + buf, err := ctlcmd.Run(append([]string{"convert"}, files...), reader) + if err != nil { + return nil, err + } + + return strings.NewReader(buf.String()), nil +} + +func init() { + confloader.EffectiveConfigFileLoader = ConfigLoader + confloader.EffectiveExtConfigLoader = ExtConfigLoader +} diff --git a/main/distro/all/all.go b/main/distro/all/all.go new file mode 100644 index 00000000..b89a25f2 --- /dev/null +++ b/main/distro/all/all.go @@ -0,0 +1,71 @@ +package all + +import ( + // The following are necessary as they register handlers in their init functions. + + // Required features. Can't remove unless there is replacements. + _ "github.com/xtls/xray-core/v1/app/dispatcher" + _ "github.com/xtls/xray-core/v1/app/proxyman/inbound" + _ "github.com/xtls/xray-core/v1/app/proxyman/outbound" + + // Default commander and all its services. This is an optional feature. + _ "github.com/xtls/xray-core/v1/app/commander" + _ "github.com/xtls/xray-core/v1/app/log/command" + _ "github.com/xtls/xray-core/v1/app/proxyman/command" + _ "github.com/xtls/xray-core/v1/app/stats/command" + + // Other optional features. + _ "github.com/xtls/xray-core/v1/app/dns" + _ "github.com/xtls/xray-core/v1/app/log" + _ "github.com/xtls/xray-core/v1/app/policy" + _ "github.com/xtls/xray-core/v1/app/reverse" + _ "github.com/xtls/xray-core/v1/app/router" + _ "github.com/xtls/xray-core/v1/app/stats" + + // Inbound and outbound proxies. + _ "github.com/xtls/xray-core/v1/proxy/blackhole" + _ "github.com/xtls/xray-core/v1/proxy/dns" + _ "github.com/xtls/xray-core/v1/proxy/dokodemo" + _ "github.com/xtls/xray-core/v1/proxy/freedom" + _ "github.com/xtls/xray-core/v1/proxy/http" + _ "github.com/xtls/xray-core/v1/proxy/mtproto" + _ "github.com/xtls/xray-core/v1/proxy/shadowsocks" + _ "github.com/xtls/xray-core/v1/proxy/socks" + _ "github.com/xtls/xray-core/v1/proxy/trojan" + _ "github.com/xtls/xray-core/v1/proxy/vless/inbound" + _ "github.com/xtls/xray-core/v1/proxy/vless/outbound" + _ "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + _ "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + + // Transports + _ "github.com/xtls/xray-core/v1/transport/internet/domainsocket" + _ "github.com/xtls/xray-core/v1/transport/internet/http" + _ "github.com/xtls/xray-core/v1/transport/internet/kcp" + _ "github.com/xtls/xray-core/v1/transport/internet/quic" + _ "github.com/xtls/xray-core/v1/transport/internet/tcp" + _ "github.com/xtls/xray-core/v1/transport/internet/tls" + _ "github.com/xtls/xray-core/v1/transport/internet/udp" + _ "github.com/xtls/xray-core/v1/transport/internet/websocket" + _ "github.com/xtls/xray-core/v1/transport/internet/xtls" + + // Transport headers + _ "github.com/xtls/xray-core/v1/transport/internet/headers/http" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/noop" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/srtp" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/tls" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/utp" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/wechat" + _ "github.com/xtls/xray-core/v1/transport/internet/headers/wireguard" + + // JSON config support. Choose only one from the two below. + // The following line loads JSON from xctl + // _ "github.com/xtls/xray-core/v1/main/json" + // The following line loads JSON internally + _ "github.com/xtls/xray-core/v1/main/jsonem" + + // Load config from file or http(s) + _ "github.com/xtls/xray-core/v1/main/confloader/external" + + // commands + _ "github.com/xtls/xray-core/v1/main/commands/all" +) diff --git a/main/distro/debug/debug.go b/main/distro/debug/debug.go new file mode 100644 index 00000000..7448ecdf --- /dev/null +++ b/main/distro/debug/debug.go @@ -0,0 +1,11 @@ +package debug + +import ( + "net/http" +) + +func init() { + go func() { + http.ListenAndServe(":6060", nil) + }() +} diff --git a/main/errors.generated.go b/main/errors.generated.go new file mode 100644 index 00000000..4287a6d9 --- /dev/null +++ b/main/errors.generated.go @@ -0,0 +1,9 @@ +package main + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/json/config_json.go b/main/json/config_json.go new file mode 100644 index 00000000..5514bea2 --- /dev/null +++ b/main/json/config_json.go @@ -0,0 +1,38 @@ +package json + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "io" + "os" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/cmdarg" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/main/confloader" +) + +func init() { + common.Must(core.RegisterConfigLoader(&core.ConfigFormat{ + Name: "JSON", + Extension: []string{"json"}, + Loader: func(input interface{}) (*core.Config, error) { + switch v := input.(type) { + case cmdarg.Arg: + r, err := confloader.LoadExtConfig(v, os.Stdin) + if err != nil { + return nil, newError("failed to execute xctl to convert config file.").Base(err).AtWarning() + } + return core.LoadConfig("protobuf", "", r) + case io.Reader: + r, err := confloader.LoadExtConfig([]string{"stdin:"}, os.Stdin) + if err != nil { + return nil, newError("failed to execute xctl to convert config file.").Base(err).AtWarning() + } + return core.LoadConfig("protobuf", "", r) + default: + return nil, newError("unknown type") + } + }, + })) +} diff --git a/main/json/errors.generated.go b/main/json/errors.generated.go new file mode 100644 index 00000000..71d741bf --- /dev/null +++ b/main/json/errors.generated.go @@ -0,0 +1,9 @@ +package json + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/jsonem/errors.generated.go b/main/jsonem/errors.generated.go new file mode 100644 index 00000000..ac1760f3 --- /dev/null +++ b/main/jsonem/errors.generated.go @@ -0,0 +1,9 @@ +package jsonem + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/jsonem/jsonem.go b/main/jsonem/jsonem.go new file mode 100644 index 00000000..66a16bc4 --- /dev/null +++ b/main/jsonem/jsonem.go @@ -0,0 +1,38 @@ +package jsonem + +import ( + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/cmdarg" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/infra/conf" + "github.com/xtls/xray-core/v1/infra/conf/serial" + "github.com/xtls/xray-core/v1/main/confloader" +) + +func init() { + common.Must(core.RegisterConfigLoader(&core.ConfigFormat{ + Name: "JSON", + Extension: []string{"json"}, + Loader: func(input interface{}) (*core.Config, error) { + switch v := input.(type) { + case cmdarg.Arg: + cf := &conf.Config{} + for _, arg := range v { + newError("Reading config: ", arg).AtInfo().WriteToLog() + r, err := confloader.LoadConfig(arg) + common.Must(err) + c, err := serial.DecodeJSONConfig(r) + common.Must(err) + cf.Override(c, arg) + } + return cf.Build() + case io.Reader: + return serial.LoadJSONConfig(v) + default: + return nil, newError("unknow type") + } + }, + })) +} diff --git a/main/main.go b/main/main.go new file mode 100644 index 00000000..e3759cf3 --- /dev/null +++ b/main/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "os" + + "github.com/xtls/xray-core/v1/main/commands/base" + _ "github.com/xtls/xray-core/v1/main/distro/all" +) + +func main() { + os.Args = getArgsV4Compatible() + + base.RootCommand.Long = "Xray is a platform for building proxies." + base.RootCommand.Commands = append( + []*base.Command{ + cmdRun, + cmdVersion, + }, + base.RootCommand.Commands..., + ) + base.Execute() +} + +func getArgsV4Compatible() []string { + if len(os.Args) == 1 { + return []string{os.Args[0], "run"} + } + if os.Args[1][0] != '-' { + return os.Args + } + version := false + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.BoolVar(&version, "version", false, "") + // parse silently, no usage, no error output + fs.Usage = func() {} + fs.SetOutput(&null{}) + err := fs.Parse(os.Args[1:]) + if err == flag.ErrHelp { + //fmt.Println("DEPRECATED: -h, WILL BE REMOVED IN V5.") + //fmt.Println("PLEASE USE: xray help") + //fmt.Println() + return []string{os.Args[0], "help"} + } + if version { + //fmt.Println("DEPRECATED: -version, WILL BE REMOVED IN V5.") + //fmt.Println("PLEASE USE: xray version") + //fmt.Println() + return []string{os.Args[0], "version"} + } + //fmt.Println("COMPATIBLE MODE, DEPRECATED.") + //fmt.Println("PLEASE USE: xray run [arguments] INSTEAD.") + //fmt.Println() + return append([]string{os.Args[0], "run"}, os.Args[1:]...) +} + +type null struct{} + +func (n *null) Write(p []byte) (int, error) { + return len(p), nil +} diff --git a/main/main_test.go b/main/main_test.go new file mode 100644 index 00000000..63c18234 --- /dev/null +++ b/main/main_test.go @@ -0,0 +1,11 @@ +// +build coveragemain + +package main + +import ( + "testing" +) + +func TestRunMainForCoverage(t *testing.T) { + main() +} diff --git a/main/run.go b/main/run.go new file mode 100644 index 00000000..b9349c75 --- /dev/null +++ b/main/run.go @@ -0,0 +1,169 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/signal" + "path" + "path/filepath" + "runtime" + "strings" + "syscall" + + "github.com/xtls/xray-core/v1/common/cmdarg" + "github.com/xtls/xray-core/v1/common/platform" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +var cmdRun = &base.Command{ + UsageLine: "{{.Exec}} run [-c config.json] [-confdir dir]", + Short: "Run Xray with config, the default command", + Long: ` +Run Xray with config, the default command. + +The -config=file, -c=file flags set the config files for +Xray. Multiple assign is accepted. + +The -confdir=dir flag sets a dir with multiple json config + +The -format=json flag sets the format of config files. +Default "json". + +The -test flag tells Xray to test config files only, +without launching the server + `, +} + +func init() { + cmdRun.Run = executeRun //break init loop +} + +var ( + configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main + configDir string + test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.") + format = cmdRun.Flag.String("format", "json", "Format of input file.") + + /* We have to do this here because Golang's Test will also need to parse flag, before + * main func in this file is run. + */ + _ = func() bool { + + cmdRun.Flag.Var(&configFiles, "config", "Config path for Xray.") + cmdRun.Flag.Var(&configFiles, "c", "Short alias of -config") + cmdRun.Flag.StringVar(&configDir, "confdir", "", "A dir with multiple json config") + + return true + }() +) + +func executeRun(cmd *base.Command, args []string) { + printVersion() + server, err := startXray() + if err != nil { + base.Fatalf("Filed to start: %s", err) + } + + if *test { + fmt.Println("Configuration OK.") + base.SetExitStatus(0) + base.Exit() + } + + if err := server.Start(); err != nil { + base.Fatalf("Filed to start: %s", err) + } + defer server.Close() + + // Explicitly triggering GC to remove garbage from config loading. + runtime.GC() + + { + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + <-osSignals + } +} + +func fileExists(file string) bool { + info, err := os.Stat(file) + return err == nil && !info.IsDir() +} + +func dirExists(file string) bool { + if file == "" { + return false + } + info, err := os.Stat(file) + return err == nil && info.IsDir() +} + +func readConfDir(dirPath string) { + confs, err := ioutil.ReadDir(dirPath) + if err != nil { + log.Fatalln(err) + } + for _, f := range confs { + if strings.HasSuffix(f.Name(), ".json") { + configFiles.Set(path.Join(dirPath, f.Name())) + } + } +} + +func getConfigFilePath() cmdarg.Arg { + if dirExists(configDir) { + log.Println("Using confdir from arg:", configDir) + readConfDir(configDir) + } else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) { + log.Println("Using confdir from env:", envConfDir) + readConfDir(envConfDir) + } + + if len(configFiles) > 0 { + return configFiles + } + + if workingDir, err := os.Getwd(); err == nil { + configFile := filepath.Join(workingDir, "config.json") + if fileExists(configFile) { + log.Println("Using default config: ", configFile) + return cmdarg.Arg{configFile} + } + } + + if configFile := platform.GetConfigurationPath(); fileExists(configFile) { + log.Println("Using config from env: ", configFile) + return cmdarg.Arg{configFile} + } + + log.Println("Using config from STDIN") + return cmdarg.Arg{"stdin:"} +} + +func getConfigFormat() string { + switch strings.ToLower(*format) { + case "pb", "protobuf": + return "protobuf" + default: + return "json" + } +} + +func startXray() (core.Server, error) { + configFiles := getConfigFilePath() + + config, err := core.LoadConfig(getConfigFormat(), configFiles[0], configFiles) + if err != nil { + return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err) + } + + server, err := core.New(config) + if err != nil { + return nil, newError("failed to create server").Base(err) + } + + return server, nil +} diff --git a/main/version.go b/main/version.go new file mode 100644 index 00000000..730372e3 --- /dev/null +++ b/main/version.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/main/commands/base" +) + +var cmdVersion = &base.Command{ + UsageLine: "{{.Exec}} version", + Short: "Show current version of Xray", + Long: `Version prints the build information for Xray executables. + `, + Run: executeVersion, +} + +func executeVersion(cmd *base.Command, args []string) { + printVersion() +} + +func printVersion() { + version := core.VersionStatement() + for _, s := range version { + fmt.Println(s) + } +} diff --git a/proxy/blackhole/blackhole.go b/proxy/blackhole/blackhole.go new file mode 100644 index 00000000..82320b0d --- /dev/null +++ b/proxy/blackhole/blackhole.go @@ -0,0 +1,48 @@ +// +build !confonly + +// Package blackhole is an outbound handler that blocks all connections. +package blackhole + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Handler is an outbound connection that silently swallow the entire payload. +type Handler struct { + response ResponseConfig +} + +// New creates a new blackhole handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + response, err := config.GetInternalResponse() + if err != nil { + return nil, err + } + return &Handler{ + response: response, + }, nil +} + +// Process implements OutboundHandler.Dispatch(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + nBytes := h.response.WriteTo(link.Writer) + if nBytes > 0 { + // Sleep a little here to make sure the response is sent to client. + time.Sleep(time.Second) + } + common.Interrupt(link.Writer) + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/proxy/blackhole/blackhole_test.go b/proxy/blackhole/blackhole_test.go new file mode 100644 index 00000000..84c1ed45 --- /dev/null +++ b/proxy/blackhole/blackhole_test.go @@ -0,0 +1,40 @@ +package blackhole_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/blackhole" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +func TestBlackholeHTTPResponse(t *testing.T) { + handler, err := blackhole.New(context.Background(), &blackhole.Config{ + Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), + }) + common.Must(err) + + reader, writer := pipe.New(pipe.WithoutSizeLimit()) + + var mb buf.MultiBuffer + var rerr error + go func() { + b, e := reader.ReadMultiBuffer() + mb = b + rerr = e + }() + + link := transport.Link{ + Reader: reader, + Writer: writer, + } + common.Must(handler.Process(context.Background(), &link, nil)) + common.Must(rerr) + if mb.IsEmpty() { + t.Error("expect http response, but nothing") + } +} diff --git a/proxy/blackhole/config.go b/proxy/blackhole/config.go new file mode 100644 index 00000000..f31d7977 --- /dev/null +++ b/proxy/blackhole/config.go @@ -0,0 +1,47 @@ +package blackhole + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" +) + +const ( + http403response = `HTTP/1.1 403 Forbidden +Connection: close +Cache-Control: max-age=3600, public +Content-Length: 0 + + +` +) + +// ResponseConfig is the configuration for blackhole responses. +type ResponseConfig interface { + // WriteTo writes predefined response to the give buffer. + WriteTo(buf.Writer) int32 +} + +// WriteTo implements ResponseConfig.WriteTo(). +func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 } + +// WriteTo implements ResponseConfig.WriteTo(). +func (*HTTPResponse) WriteTo(writer buf.Writer) int32 { + b := buf.New() + common.Must2(b.WriteString(http403response)) + n := b.Len() + writer.WriteMultiBuffer(buf.MultiBuffer{b}) + return n +} + +// GetInternalResponse converts response settings from proto to internal data structure. +func (c *Config) GetInternalResponse() (ResponseConfig, error) { + if c.GetResponse() == nil { + return new(NoneResponse), nil + } + + config, err := c.GetResponse().GetInstance() + if err != nil { + return nil, err + } + return config.(ResponseConfig), nil +} diff --git a/proxy/blackhole/config.pb.go b/proxy/blackhole/config.pb.go new file mode 100644 index 00000000..d0010e5d --- /dev/null +++ b/proxy/blackhole/config.pb.go @@ -0,0 +1,265 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/blackhole/config.proto + +package blackhole + +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 + +type NoneResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *NoneResponse) Reset() { + *x = NoneResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_blackhole_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NoneResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NoneResponse) ProtoMessage() {} + +func (x *NoneResponse) ProtoReflect() protoreflect.Message { + mi := &file_proxy_blackhole_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 NoneResponse.ProtoReflect.Descriptor instead. +func (*NoneResponse) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0} +} + +type HTTPResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *HTTPResponse) Reset() { + *x = HTTPResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_blackhole_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HTTPResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPResponse) ProtoMessage() {} + +func (x *HTTPResponse) ProtoReflect() protoreflect.Message { + mi := &file_proxy_blackhole_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 HTTPResponse.ProtoReflect.Descriptor instead. +func (*HTTPResponse) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1} +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_blackhole_config_proto_msgTypes[2] + 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_proxy_blackhole_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetResponse() *serial.TypedMessage { + if x != nil { + return x.Response + } + return nil +} + +var File_proxy_blackhole_config_proto protoreflect.FileDescriptor + +var file_proxy_blackhole_config_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x6f, 0x6c, + 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x62, 0x6c, 0x61, 0x63, 0x6b, + 0x68, 0x6f, 0x6c, 0x65, 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, 0x0e, 0x0a, 0x0c, 0x4e, 0x6f, 0x6e, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0e, 0x0a, 0x0c, 0x48, 0x54, 0x54, 0x50, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x3c, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 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, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x61, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x6f, 0x6c, 0x65, 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, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2f, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x6f, 0x6c, 0x65, 0xaa, 0x02, 0x14, 0x58, 0x72, + 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x6f, + 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_blackhole_config_proto_rawDescOnce sync.Once + file_proxy_blackhole_config_proto_rawDescData = file_proxy_blackhole_config_proto_rawDesc +) + +func file_proxy_blackhole_config_proto_rawDescGZIP() []byte { + file_proxy_blackhole_config_proto_rawDescOnce.Do(func() { + file_proxy_blackhole_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_blackhole_config_proto_rawDescData) + }) + return file_proxy_blackhole_config_proto_rawDescData +} + +var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_blackhole_config_proto_goTypes = []interface{}{ + (*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse + (*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse + (*Config)(nil), // 2: xray.proxy.blackhole.Config + (*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage +} +var file_proxy_blackhole_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.blackhole.Config.response: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_proxy_blackhole_config_proto_init() } +func file_proxy_blackhole_config_proto_init() { + if File_proxy_blackhole_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_blackhole_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NoneResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_blackhole_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HTTPResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_blackhole_config_proto_msgTypes[2].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_proxy_blackhole_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_blackhole_config_proto_goTypes, + DependencyIndexes: file_proxy_blackhole_config_proto_depIdxs, + MessageInfos: file_proxy_blackhole_config_proto_msgTypes, + }.Build() + File_proxy_blackhole_config_proto = out.File + file_proxy_blackhole_config_proto_rawDesc = nil + file_proxy_blackhole_config_proto_goTypes = nil + file_proxy_blackhole_config_proto_depIdxs = nil +} diff --git a/proxy/blackhole/config.proto b/proxy/blackhole/config.proto new file mode 100644 index 00000000..d1dbfcee --- /dev/null +++ b/proxy/blackhole/config.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.proxy.blackhole; +option csharp_namespace = "Xray.Proxy.Blackhole"; +option go_package = "github.com/xtls/xray-core/v1/proxy/blackhole"; +option java_package = "com.xray.proxy.blackhole"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +message NoneResponse {} + +message HTTPResponse {} + +message Config { + xray.common.serial.TypedMessage response = 1; +} diff --git a/proxy/blackhole/config_test.go b/proxy/blackhole/config_test.go new file mode 100644 index 00000000..5f3e5113 --- /dev/null +++ b/proxy/blackhole/config_test.go @@ -0,0 +1,26 @@ +package blackhole_test + +import ( + "bufio" + "net/http" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/proxy/blackhole" +) + +func TestHTTPResponse(t *testing.T) { + buffer := buf.New() + + httpResponse := new(HTTPResponse) + httpResponse.WriteTo(buf.NewWriter(buffer)) + + reader := bufio.NewReader(buffer) + response, err := http.ReadResponse(reader, nil) + common.Must(err) + + if response.StatusCode != 403 { + t.Error("expected status code 403, but got ", response.StatusCode) + } +} diff --git a/proxy/blackhole/errors.generated.go b/proxy/blackhole/errors.generated.go new file mode 100644 index 00000000..5def7a78 --- /dev/null +++ b/proxy/blackhole/errors.generated.go @@ -0,0 +1,9 @@ +package blackhole + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/dns/config.pb.go b/proxy/dns/config.pb.go new file mode 100644 index 00000000..0c0a8991 --- /dev/null +++ b/proxy/dns/config.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/dns/config.proto + +package dns + +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 + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Server is the DNS server address. If specified, this address overrides the + // original one. + Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_dns_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_proxy_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_dns_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetServer() *net.Endpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_dns_config_proto protoreflect.FileDescriptor + +var file_proxy_dns_config_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x64, 0x6e, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6e, 0x73, 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, 0x22, 0x3b, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x31, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 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, 0x06, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x42, 0x4f, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x26, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, + 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0e, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_dns_config_proto_rawDescOnce sync.Once + file_proxy_dns_config_proto_rawDescData = file_proxy_dns_config_proto_rawDesc +) + +func file_proxy_dns_config_proto_rawDescGZIP() []byte { + file_proxy_dns_config_proto_rawDescOnce.Do(func() { + file_proxy_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_dns_config_proto_rawDescData) + }) + return file_proxy_dns_config_proto_rawDescData +} + +var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_dns_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.proxy.dns.Config + (*net.Endpoint)(nil), // 1: xray.common.net.Endpoint +} +var file_proxy_dns_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint + 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_proxy_dns_config_proto_init() } +func file_proxy_dns_config_proto_init() { + if File_proxy_dns_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_dns_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_proxy_dns_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_dns_config_proto_goTypes, + DependencyIndexes: file_proxy_dns_config_proto_depIdxs, + MessageInfos: file_proxy_dns_config_proto_msgTypes, + }.Build() + File_proxy_dns_config_proto = out.File + file_proxy_dns_config_proto_rawDesc = nil + file_proxy_dns_config_proto_goTypes = nil + file_proxy_dns_config_proto_depIdxs = nil +} diff --git a/proxy/dns/config.proto b/proxy/dns/config.proto new file mode 100644 index 00000000..0e59f2ac --- /dev/null +++ b/proxy/dns/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.proxy.dns; +option csharp_namespace = "Xray.Proxy.Dns"; +option go_package = "github.com/xtls/xray-core/v1/proxy/dns"; +option java_package = "com.xray.proxy.dns"; +option java_multiple_files = true; + +import "common/net/destination.proto"; + +message Config { + // Server is the DNS server address. If specified, this address overrides the + // original one. + xray.common.net.Endpoint server = 1; +} diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go new file mode 100644 index 00000000..1326a664 --- /dev/null +++ b/proxy/dns/dns.go @@ -0,0 +1,330 @@ +// +build !confonly + +package dns + +import ( + "context" + "io" + "sync" + + "golang.org/x/net/dns/dnsmessage" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" + dns_proto "github.com/xtls/xray-core/v1/common/protocol/dns" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/dns" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + h := new(Handler) + if err := core.RequireFeatures(ctx, func(dnsClient dns.Client) error { + return h.Init(config.(*Config), dnsClient) + }); err != nil { + return nil, err + } + return h, nil + })) +} + +type ownLinkVerifier interface { + IsOwnLink(ctx context.Context) bool +} + +type Handler struct { + ipv4Lookup dns.IPv4Lookup + ipv6Lookup dns.IPv6Lookup + ownLinkVerifier ownLinkVerifier + server net.Destination +} + +func (h *Handler) Init(config *Config, dnsClient dns.Client) error { + ipv4lookup, ok := dnsClient.(dns.IPv4Lookup) + if !ok { + return newError("dns.Client doesn't implement IPv4Lookup") + } + h.ipv4Lookup = ipv4lookup + + ipv6lookup, ok := dnsClient.(dns.IPv6Lookup) + if !ok { + return newError("dns.Client doesn't implement IPv6Lookup") + } + h.ipv6Lookup = ipv6lookup + + if v, ok := dnsClient.(ownLinkVerifier); ok { + h.ownLinkVerifier = v + } + + if config.Server != nil { + h.server = config.Server.AsDestination() + } + return nil +} + +func (h *Handler) isOwnLink(ctx context.Context) bool { + return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx) +} + +func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) { + var parser dnsmessage.Parser + header, err := parser.Start(b) + if err != nil { + newError("parser start").Base(err).WriteToLog() + return + } + + id = header.ID + q, err := parser.Question() + if err != nil { + newError("question").Base(err).WriteToLog() + return + } + qType = q.Type + if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { + return + } + + domain = q.Name.String() + r = true + return +} + +// Process implements proxy.Outbound. +func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("invalid outbound") + } + + srcNetwork := outbound.Target.Network + + dest := outbound.Target + if h.server.Network != net.Network_Unknown { + dest.Network = h.server.Network + } + if h.server.Address != nil { + dest.Address = h.server.Address + } + if h.server.Port != 0 { + dest.Port = h.server.Port + } + + newError("handling DNS traffic to ", dest).WriteToLog(session.ExportIDToError(ctx)) + + conn := &outboundConn{ + dialer: func() (internet.Connection, error) { + return d.Dial(ctx, dest) + }, + connReady: make(chan struct{}, 1), + } + + var reader dns_proto.MessageReader + var writer dns_proto.MessageWriter + if srcNetwork == net.Network_TCP { + reader = dns_proto.NewTCPReader(link.Reader) + writer = &dns_proto.TCPWriter{ + Writer: link.Writer, + } + } else { + reader = &dns_proto.UDPReader{ + Reader: link.Reader, + } + writer = &dns_proto.UDPWriter{ + Writer: link.Writer, + } + } + + var connReader dns_proto.MessageReader + var connWriter dns_proto.MessageWriter + if dest.Network == net.Network_TCP { + connReader = dns_proto.NewTCPReader(buf.NewReader(conn)) + connWriter = &dns_proto.TCPWriter{ + Writer: buf.NewWriter(conn), + } + } else { + connReader = &dns_proto.UDPReader{ + Reader: buf.NewPacketReader(conn), + } + connWriter = &dns_proto.UDPWriter{ + Writer: buf.NewWriter(conn), + } + } + + request := func() error { + defer conn.Close() + + for { + b, err := reader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + if !h.isOwnLink(ctx) { + isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) + if isIPQuery { + go h.handleIPQuery(id, qType, domain, writer) + continue + } + } + + if err := connWriter.WriteMessage(b); err != nil { + return err + } + } + } + + response := func() error { + for { + b, err := connReader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + if err := writer.WriteMessage(b); err != nil { + return err + } + } + } + + if err := task.Run(ctx, request, response); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter) { + var ips []net.IP + var err error + + switch qType { + case dnsmessage.TypeA: + ips, err = h.ipv4Lookup.LookupIPv4(domain) + case dnsmessage.TypeAAAA: + ips, err = h.ipv6Lookup.LookupIPv6(domain) + } + + rcode := dns.RCodeFromError(err) + if rcode == 0 && len(ips) == 0 && err != dns.ErrEmptyResponse { + newError("ip query").Base(err).WriteToLog() + return + } + + b := buf.New() + rawBytes := b.Extend(buf.Size) + builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ + ID: id, + RCode: dnsmessage.RCode(rcode), + RecursionAvailable: true, + RecursionDesired: true, + Response: true, + Authoritative: true, + }) + builder.EnableCompression() + common.Must(builder.StartQuestions()) + common.Must(builder.Question(dnsmessage.Question{ + Name: dnsmessage.MustNewName(domain), + Class: dnsmessage.ClassINET, + Type: qType, + })) + common.Must(builder.StartAnswers()) + + rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: 600} + for _, ip := range ips { + if len(ip) == net.IPv4len { + var r dnsmessage.AResource + copy(r.A[:], ip) + common.Must(builder.AResource(rHeader, r)) + } else { + var r dnsmessage.AAAAResource + copy(r.AAAA[:], ip) + common.Must(builder.AAAAResource(rHeader, r)) + } + } + msgBytes, err := builder.Finish() + if err != nil { + newError("pack message").Base(err).WriteToLog() + b.Release() + return + } + b.Resize(0, int32(len(msgBytes))) + + if err := writer.WriteMessage(b); err != nil { + newError("write IP answer").Base(err).WriteToLog() + } +} + +type outboundConn struct { + access sync.Mutex + dialer func() (internet.Connection, error) + + conn net.Conn + connReady chan struct{} +} + +func (c *outboundConn) dial() error { + conn, err := c.dialer() + if err != nil { + return err + } + c.conn = conn + c.connReady <- struct{}{} + return nil +} + +func (c *outboundConn) Write(b []byte) (int, error) { + c.access.Lock() + + if c.conn == nil { + if err := c.dial(); err != nil { + c.access.Unlock() + newError("failed to dial outbound connection").Base(err).AtWarning().WriteToLog() + return len(b), nil + } + } + + c.access.Unlock() + + return c.conn.Write(b) +} + +func (c *outboundConn) Read(b []byte) (int, error) { + var conn net.Conn + c.access.Lock() + conn = c.conn + c.access.Unlock() + + if conn == nil { + _, open := <-c.connReady + if !open { + return 0, io.EOF + } + conn = c.conn + } + + return conn.Read(b) +} + +func (c *outboundConn) Close() error { + c.access.Lock() + close(c.connReady) + if c.conn != nil { + c.conn.Close() + } + c.access.Unlock() + return nil +} diff --git a/proxy/dns/dns_test.go b/proxy/dns/dns_test.go new file mode 100644 index 00000000..92bd8e9b --- /dev/null +++ b/proxy/dns/dns_test.go @@ -0,0 +1,370 @@ +package dns_test + +import ( + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" + + "github.com/xtls/xray-core/v1/app/dispatcher" + dnsapp "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/inbound" + _ "github.com/xtls/xray-core/v1/app/proxyman/outbound" + "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" + dns_proxy "github.com/xtls/xray-core/v1/proxy/dns" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "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 == "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 + } + } + w.WriteMsg(ans) +} + +func TestUDPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := udp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.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(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_UDP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := new(dns.Client) + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } + } + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "ipv4only.google.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET} + + c := new(dns.Client) + c.Timeout = 10 * time.Second + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if len(in.Answer) != 0 { + t.Fatal("len(answer): ", len(in.Answer)) + } + } + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "notexist.google.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET} + + c := new(dns.Client) + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if in.Rcode != dns.RcodeNameError { + t.Error("expected NameError, but got ", in.Rcode) + } + } +} + +func TestTCPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := tcp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &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(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_TCP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := &dns.Client{ + Net: "tcp", + } + in, _, err := c.Exchange(m1, "127.0.0.1:"+serverPort.String()) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} + +func TestUDP2TCPDNSTunnel(t *testing.T) { + port := tcp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "tcp", + Handler: &staticHandler{}, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := tcp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &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(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_TCP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ + Server: &net.Endpoint{ + Network: net.Network_TCP, + }, + }), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := &dns.Client{ + Net: "tcp", + } + in, _, err := c.Exchange(m1, "127.0.0.1:"+serverPort.String()) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} diff --git a/proxy/dns/errors.generated.go b/proxy/dns/errors.generated.go new file mode 100644 index 00000000..ce4c0605 --- /dev/null +++ b/proxy/dns/errors.generated.go @@ -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{}) +} diff --git a/proxy/dokodemo/config.go b/proxy/dokodemo/config.go new file mode 100644 index 00000000..12f1086b --- /dev/null +++ b/proxy/dokodemo/config.go @@ -0,0 +1,14 @@ +package dokodemo + +import ( + "github.com/xtls/xray-core/v1/common/net" +) + +// GetPredefinedAddress returns the defined address from proto config. Null if address is not valid. +func (v *Config) GetPredefinedAddress() net.Address { + addr := v.Address.AsAddress() + if addr == nil { + return nil + } + return addr +} diff --git a/proxy/dokodemo/config.pb.go b/proxy/dokodemo/config.pb.go new file mode 100644 index 00000000..18c5af49 --- /dev/null +++ b/proxy/dokodemo/config.pb.go @@ -0,0 +1,237 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/dokodemo/config.proto + +package dokodemo + +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 + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + // List of networks that the Dokodemo accepts. + // Deprecated. Use networks. + // + // Deprecated: Do not use. + NetworkList *net.NetworkList `protobuf:"bytes,3,opt,name=network_list,json=networkList,proto3" json:"network_list,omitempty"` + // List of networks that the Dokodemo accepts. + Networks []net.Network `protobuf:"varint,7,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` + // Deprecated: Do not use. + Timeout uint32 `protobuf:"varint,4,opt,name=timeout,proto3" json:"timeout,omitempty"` + FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` + UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_dokodemo_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_proxy_dokodemo_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_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *Config) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +// Deprecated: Do not use. +func (x *Config) GetNetworkList() *net.NetworkList { + if x != nil { + return x.NetworkList + } + return nil +} + +func (x *Config) GetNetworks() []net.Network { + if x != nil { + return x.Networks + } + return nil +} + +// Deprecated: Do not use. +func (x *Config) GetTimeout() uint32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *Config) GetFollowRedirect() bool { + if x != nil { + return x.FollowRedirect + } + return false +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor + +var file_proxy_dokodemo_config_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x64, 0x6f, 0x6b, 0x6f, 0x64, 0x65, 0x6d, 0x6f, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x64, 0x6f, 0x6b, 0x6f, 0x64, 0x65, + 0x6d, 0x6f, 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, 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, 0xb4, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 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, + 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x43, 0x0a, 0x0c, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4c, 0x69, 0x73, 0x74, + 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, + 0x03, 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, 0x08, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1c, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, 0x74, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, + 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, + 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x42, 0x5e, 0x0a, + 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x64, 0x6f, 0x6b, 0x6f, 0x64, 0x65, 0x6d, 0x6f, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x64, + 0x6f, 0x6b, 0x6f, 0x64, 0x65, 0x6d, 0x6f, 0xaa, 0x02, 0x13, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x44, 0x6f, 0x6b, 0x6f, 0x64, 0x65, 0x6d, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_dokodemo_config_proto_rawDescOnce sync.Once + file_proxy_dokodemo_config_proto_rawDescData = file_proxy_dokodemo_config_proto_rawDesc +) + +func file_proxy_dokodemo_config_proto_rawDescGZIP() []byte { + file_proxy_dokodemo_config_proto_rawDescOnce.Do(func() { + file_proxy_dokodemo_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_dokodemo_config_proto_rawDescData) + }) + return file_proxy_dokodemo_config_proto_rawDescData +} + +var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_dokodemo_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.proxy.dokodemo.Config + (*net.IPOrDomain)(nil), // 1: xray.common.net.IPOrDomain + (*net.NetworkList)(nil), // 2: xray.common.net.NetworkList + (net.Network)(0), // 3: xray.common.net.Network +} +var file_proxy_dokodemo_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.dokodemo.Config.address:type_name -> xray.common.net.IPOrDomain + 2, // 1: xray.proxy.dokodemo.Config.network_list:type_name -> xray.common.net.NetworkList + 3, // 2: xray.proxy.dokodemo.Config.networks:type_name -> xray.common.net.Network + 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_proxy_dokodemo_config_proto_init() } +func file_proxy_dokodemo_config_proto_init() { + if File_proxy_dokodemo_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_dokodemo_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_proxy_dokodemo_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_dokodemo_config_proto_goTypes, + DependencyIndexes: file_proxy_dokodemo_config_proto_depIdxs, + MessageInfos: file_proxy_dokodemo_config_proto_msgTypes, + }.Build() + File_proxy_dokodemo_config_proto = out.File + file_proxy_dokodemo_config_proto_rawDesc = nil + file_proxy_dokodemo_config_proto_goTypes = nil + file_proxy_dokodemo_config_proto_depIdxs = nil +} diff --git a/proxy/dokodemo/config.proto b/proxy/dokodemo/config.proto new file mode 100644 index 00000000..a12f9907 --- /dev/null +++ b/proxy/dokodemo/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xray.proxy.dokodemo; +option csharp_namespace = "Xray.Proxy.Dokodemo"; +option go_package = "github.com/xtls/xray-core/v1/proxy/dokodemo"; +option java_package = "com.xray.proxy.dokodemo"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/net/network.proto"; + +message Config { + xray.common.net.IPOrDomain address = 1; + uint32 port = 2; + + // List of networks that the Dokodemo accepts. + // Deprecated. Use networks. + xray.common.net.NetworkList network_list = 3 [deprecated = true]; + // List of networks that the Dokodemo accepts. + repeated xray.common.net.Network networks = 7; + + uint32 timeout = 4 [deprecated = true]; + bool follow_redirect = 5; + uint32 user_level = 6; +} diff --git a/proxy/dokodemo/dokodemo.go b/proxy/dokodemo/dokodemo.go new file mode 100644 index 00000000..dba80672 --- /dev/null +++ b/proxy/dokodemo/dokodemo.go @@ -0,0 +1,214 @@ +// +build !confonly + +package dokodemo + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "sync/atomic" + "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/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + d := new(DokodemoDoor) + err := core.RequireFeatures(ctx, func(pm policy.Manager) error { + return d.Init(config.(*Config), pm, session.SockoptFromContext(ctx)) + }) + return d, err + })) +} + +type DokodemoDoor struct { + policyManager policy.Manager + config *Config + address net.Address + port net.Port + sockopt *session.Sockopt +} + +// Init initializes the DokodemoDoor instance with necessary parameters. +func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.Sockopt) error { + if (config.NetworkList == nil || len(config.NetworkList.Network) == 0) && len(config.Networks) == 0 { + return newError("no network specified") + } + d.config = config + d.address = config.GetPredefinedAddress() + d.port = net.Port(config.Port) + d.policyManager = pm + d.sockopt = sockopt + + return nil +} + +// Network implements proxy.Inbound. +func (d *DokodemoDoor) Network() []net.Network { + if len(d.config.Networks) > 0 { + return d.config.Networks + } + + return d.config.NetworkList.Network +} + +func (d *DokodemoDoor) policy() policy.Session { + config := d.config + p := d.policyManager.ForLevel(config.UserLevel) + if config.Timeout > 0 && config.UserLevel == 0 { + p.Timeouts.ConnectionIdle = time.Duration(config.Timeout) * time.Second + } + return p +} + +type hasHandshakeAddress interface { + HandshakeAddress() net.Address +} + +// Process implements proxy.Inbound. +func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + newError("processing connection from: ", conn.RemoteAddr()).AtDebug().WriteToLog(session.ExportIDToError(ctx)) + dest := net.Destination{ + Network: network, + Address: d.address, + Port: d.port, + } + + destinationOverridden := false + if d.config.FollowRedirect { + if outbound := session.OutboundFromContext(ctx); outbound != nil && outbound.Target.IsValid() { + dest = outbound.Target + destinationOverridden = true + } else if handshake, ok := conn.(hasHandshakeAddress); ok { + addr := handshake.HandshakeAddress() + if addr != nil { + dest.Address = addr + destinationOverridden = true + } + } + } + if !dest.IsValid() || dest.Address == nil { + return newError("unable to get destination") + } + + if inbound := session.InboundFromContext(ctx); inbound != nil { + inbound.User = &protocol.MemoryUser{ + Level: d.config.UserLevel, + } + } + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: dest, + Status: log.AccessAccepted, + Reason: "", + }) + newError("received request for ", conn.RemoteAddr()).WriteToLog(session.ExportIDToError(ctx)) + + plcy := d.policy() + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, plcy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, plcy.Buffer) + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return newError("failed to dispatch request").Base(err) + } + + requestCount := int32(1) + requestDone := func() error { + defer func() { + if atomic.AddInt32(&requestCount, -1) == 0 { + timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + } + }() + + var reader buf.Reader + if dest.Network == net.Network_UDP { + reader = buf.NewPacketReader(conn) + } else { + reader = buf.NewReader(conn) + } + if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport request").Base(err) + } + + return nil + } + + tproxyRequest := func() error { + return nil + } + + var writer buf.Writer + if network == net.Network_TCP { + writer = buf.NewWriter(conn) + } else { + // if we are in TPROXY mode, use linux's udp forging functionality + if !destinationOverridden { + writer = &buf.SequentialWriter{Writer: conn} + } else { + sockopt := &internet.SocketConfig{ + Tproxy: internet.SocketConfig_TProxy, + } + if dest.Address.Family().IsIP() { + sockopt.BindAddress = dest.Address.IP() + sockopt.BindPort = uint32(dest.Port) + } + if d.sockopt != nil { + sockopt.Mark = d.sockopt.Mark + } + tConn, err := internet.DialSystem(ctx, net.DestinationFromAddr(conn.RemoteAddr()), sockopt) + if err != nil { + return err + } + defer tConn.Close() + + writer = &buf.SequentialWriter{Writer: tConn} + tReader := buf.NewPacketReader(tConn) + requestCount++ + tproxyRequest = func() error { + defer func() { + if atomic.AddInt32(&requestCount, -1) == 0 { + timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + } + }() + if err := buf.Copy(tReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport request (TPROXY conn)").Base(err) + } + return nil + } + } + } + + responseDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) + + if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport response").Base(err) + } + return nil + } + + if err := task.Run(ctx, task.OnSuccess(func() error { + return task.Run(ctx, requestDone, tproxyRequest) + }, task.Close(link.Writer)), responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} diff --git a/proxy/dokodemo/errors.generated.go b/proxy/dokodemo/errors.generated.go new file mode 100644 index 00000000..d570d667 --- /dev/null +++ b/proxy/dokodemo/errors.generated.go @@ -0,0 +1,9 @@ +package dokodemo + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/freedom/config.go b/proxy/freedom/config.go new file mode 100644 index 00000000..61cc6ad5 --- /dev/null +++ b/proxy/freedom/config.go @@ -0,0 +1,5 @@ +package freedom + +func (c *Config) useIP() bool { + return c.DomainStrategy == Config_USE_IP || c.DomainStrategy == Config_USE_IP4 || c.DomainStrategy == Config_USE_IP6 +} diff --git a/proxy/freedom/config.pb.go b/proxy/freedom/config.pb.go new file mode 100644 index 00000000..f929118d --- /dev/null +++ b/proxy/freedom/config.pb.go @@ -0,0 +1,324 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/freedom/config.proto + +package freedom + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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_DomainStrategy int32 + +const ( + Config_AS_IS Config_DomainStrategy = 0 + Config_USE_IP Config_DomainStrategy = 1 + Config_USE_IP4 Config_DomainStrategy = 2 + Config_USE_IP6 Config_DomainStrategy = 3 +) + +// Enum value maps for Config_DomainStrategy. +var ( + Config_DomainStrategy_name = map[int32]string{ + 0: "AS_IS", + 1: "USE_IP", + 2: "USE_IP4", + 3: "USE_IP6", + } + Config_DomainStrategy_value = map[string]int32{ + "AS_IS": 0, + "USE_IP": 1, + "USE_IP4": 2, + "USE_IP6": 3, + } +) + +func (x Config_DomainStrategy) Enum() *Config_DomainStrategy { + p := new(Config_DomainStrategy) + *p = x + return p +} + +func (x Config_DomainStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Config_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_freedom_config_proto_enumTypes[0].Descriptor() +} + +func (Config_DomainStrategy) Type() protoreflect.EnumType { + return &file_proxy_freedom_config_proto_enumTypes[0] +} + +func (x Config_DomainStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Config_DomainStrategy.Descriptor instead. +func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{1, 0} +} + +type DestinationOverride struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` +} + +func (x *DestinationOverride) Reset() { + *x = DestinationOverride{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_freedom_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DestinationOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DestinationOverride) ProtoMessage() {} + +func (x *DestinationOverride) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_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 DestinationOverride.ProtoReflect.Descriptor instead. +func (*DestinationOverride) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{0} +} + +func (x *DestinationOverride) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.freedom.Config_DomainStrategy" json:"domain_strategy,omitempty"` + // Deprecated: Do not use. + Timeout uint32 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"` + DestinationOverride *DestinationOverride `protobuf:"bytes,3,opt,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` + UserLevel uint32 `protobuf:"varint,4,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_freedom_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_proxy_freedom_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_proxy_freedom_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetDomainStrategy() Config_DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return Config_AS_IS +} + +// Deprecated: Do not use. +func (x *Config) GetTimeout() uint32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *Config) GetDestinationOverride() *DestinationOverride { + if x != nil { + return x.DestinationOverride + } + return nil +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +var File_proxy_freedom_config_proto protoreflect.FileDescriptor + +var file_proxy_freedom_config_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, 0x2f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, + 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x53, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0xb8, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x52, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, + 0x6d, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x1c, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, 0x74, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x5a, 0x0a, 0x14, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x13, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x22, 0x41, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x53, 0x5f, 0x49, 0x53, 0x10, 0x00, 0x12, 0x0a, 0x0a, + 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, + 0x5f, 0x49, 0x50, 0x34, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, + 0x36, 0x10, 0x03, 0x42, 0x5b, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, 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, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2f, 0x66, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, 0xaa, 0x02, 0x12, 0x58, 0x72, + 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x46, 0x72, 0x65, 0x65, 0x64, 0x6f, 0x6d, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_freedom_config_proto_rawDescOnce sync.Once + file_proxy_freedom_config_proto_rawDescData = file_proxy_freedom_config_proto_rawDesc +) + +func file_proxy_freedom_config_proto_rawDescGZIP() []byte { + file_proxy_freedom_config_proto_rawDescOnce.Do(func() { + file_proxy_freedom_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_freedom_config_proto_rawDescData) + }) + return file_proxy_freedom_config_proto_rawDescData +} + +var file_proxy_freedom_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_freedom_config_proto_goTypes = []interface{}{ + (Config_DomainStrategy)(0), // 0: xray.proxy.freedom.Config.DomainStrategy + (*DestinationOverride)(nil), // 1: xray.proxy.freedom.DestinationOverride + (*Config)(nil), // 2: xray.proxy.freedom.Config + (*protocol.ServerEndpoint)(nil), // 3: xray.common.protocol.ServerEndpoint +} +var file_proxy_freedom_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint + 0, // 1: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.proxy.freedom.Config.DomainStrategy + 1, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride + 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_proxy_freedom_config_proto_init() } +func file_proxy_freedom_config_proto_init() { + if File_proxy_freedom_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_freedom_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DestinationOverride); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_freedom_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_proxy_freedom_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_freedom_config_proto_goTypes, + DependencyIndexes: file_proxy_freedom_config_proto_depIdxs, + EnumInfos: file_proxy_freedom_config_proto_enumTypes, + MessageInfos: file_proxy_freedom_config_proto_msgTypes, + }.Build() + File_proxy_freedom_config_proto = out.File + file_proxy_freedom_config_proto_rawDesc = nil + file_proxy_freedom_config_proto_goTypes = nil + file_proxy_freedom_config_proto_depIdxs = nil +} diff --git a/proxy/freedom/config.proto b/proxy/freedom/config.proto new file mode 100644 index 00000000..eb0de1ad --- /dev/null +++ b/proxy/freedom/config.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package xray.proxy.freedom; +option csharp_namespace = "Xray.Proxy.Freedom"; +option go_package = "github.com/xtls/xray-core/v1/proxy/freedom"; +option java_package = "com.xray.proxy.freedom"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message DestinationOverride { + xray.common.protocol.ServerEndpoint server = 1; +} + +message Config { + enum DomainStrategy { + AS_IS = 0; + USE_IP = 1; + USE_IP4 = 2; + USE_IP6 = 3; + } + DomainStrategy domain_strategy = 1; + uint32 timeout = 2 [deprecated = true]; + DestinationOverride destination_override = 3; + uint32 user_level = 4; +} diff --git a/proxy/freedom/errors.generated.go b/proxy/freedom/errors.generated.go new file mode 100644 index 00000000..f6c1cb83 --- /dev/null +++ b/proxy/freedom/errors.generated.go @@ -0,0 +1,9 @@ +package freedom + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go new file mode 100644 index 00000000..fb7a8ff0 --- /dev/null +++ b/proxy/freedom/freedom.go @@ -0,0 +1,184 @@ +// +build !confonly + +package freedom + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/dns" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + h := new(Handler) + if err := core.RequireFeatures(ctx, func(pm policy.Manager, d dns.Client) error { + return h.Init(config.(*Config), pm, d) + }); err != nil { + return nil, err + } + return h, nil + })) +} + +// Handler handles Freedom connections. +type Handler struct { + policyManager policy.Manager + dns dns.Client + config *Config +} + +// Init initializes the Handler with necessary parameters. +func (h *Handler) Init(config *Config, pm policy.Manager, d dns.Client) error { + h.config = config + h.policyManager = pm + h.dns = d + + return nil +} + +func (h *Handler) policy() policy.Session { + p := h.policyManager.ForLevel(h.config.UserLevel) + if h.config.Timeout > 0 && h.config.UserLevel == 0 { + p.Timeouts.ConnectionIdle = time.Duration(h.config.Timeout) * time.Second + } + return p +} + +func (h *Handler) resolveIP(ctx context.Context, domain string, localAddr net.Address) net.Address { + var lookupFunc func(string) ([]net.IP, error) = h.dns.LookupIP + + if h.config.DomainStrategy == Config_USE_IP4 || (localAddr != nil && localAddr.Family().IsIPv4()) { + if lookupIPv4, ok := h.dns.(dns.IPv4Lookup); ok { + lookupFunc = lookupIPv4.LookupIPv4 + } + } else if h.config.DomainStrategy == Config_USE_IP6 || (localAddr != nil && localAddr.Family().IsIPv6()) { + if lookupIPv6, ok := h.dns.(dns.IPv6Lookup); ok { + lookupFunc = lookupIPv6.LookupIPv6 + } + } + + ips, err := lookupFunc(domain) + if err != nil { + newError("failed to get IP address for domain ", domain).Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + if len(ips) == 0 { + return nil + } + return net.IPAddress(ips[dice.Roll(len(ips))]) +} + +func isValidAddress(addr *net.IPOrDomain) bool { + if addr == nil { + return false + } + + a := addr.AsAddress() + return a != net.AnyIP +} + +// Process implements proxy.Outbound. +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified.") + } + destination := outbound.Target + if h.config.DestinationOverride != nil { + server := h.config.DestinationOverride.Server + if isValidAddress(server.Address) { + destination.Address = server.Address.AsAddress() + } + if server.Port != 0 { + destination.Port = net.Port(server.Port) + } + } + newError("opening connection to ", destination).WriteToLog(session.ExportIDToError(ctx)) + + input := link.Reader + output := link.Writer + + var conn internet.Connection + err := retry.ExponentialBackoff(5, 100).On(func() error { + dialDest := destination + if h.config.useIP() && dialDest.Address.Family().IsDomain() { + ip := h.resolveIP(ctx, dialDest.Address.Domain(), dialer.Address()) + if ip != nil { + dialDest = net.Destination{ + Network: dialDest.Network, + Address: ip, + Port: dialDest.Port, + } + newError("dialing to to ", dialDest).WriteToLog(session.ExportIDToError(ctx)) + } + } + + rawConn, err := dialer.Dial(ctx, dialDest) + if err != nil { + return err + } + conn = rawConn + return nil + }) + if err != nil { + return newError("failed to open connection to ", destination).Base(err) + } + defer conn.Close() + + plcy := h.policy() + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, plcy.Timeouts.ConnectionIdle) + + requestDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + + var writer buf.Writer + if destination.Network == net.Network_TCP { + writer = buf.NewWriter(conn) + } else { + writer = &buf.SequentialWriter{Writer: conn} + } + + if err := buf.Copy(input, writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to process request").Base(err) + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) + + var reader buf.Reader + if destination.Network == net.Network_TCP { + reader = buf.NewReader(conn) + } else { + reader = buf.NewPacketReader(conn) + } + if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil { + return newError("failed to process response").Base(err) + } + + return nil + } + + if err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} diff --git a/proxy/http/client.go b/proxy/http/client.go new file mode 100644 index 00000000..0010a330 --- /dev/null +++ b/proxy/http/client.go @@ -0,0 +1,309 @@ +// +build !confonly + +package http + +import ( + "bufio" + "context" + "encoding/base64" + "io" + "net/http" + "net/url" + "sync" + + "golang.org/x/net/http2" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/bytespool" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +type Client struct { + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +type h2Conn struct { + rawConn net.Conn + h2Conn *http2.ClientConn +} + +var ( + cachedH2Mutex sync.Mutex + cachedH2Conns map[net.Destination]h2Conn +) + +// NewClient create a new http client based on the given config. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Server { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to get server spec").Base(err) + } + serverList.AddServer(s) + } + if serverList.Size() == 0 { + return nil, newError("0 target server") + } + + v := core.MustFromContext(ctx) + return &Client{ + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + }, nil +} + +// Process implements proxy.Outbound.Process. We first create a socket tunnel via HTTP CONNECT method, then redirect all inbound traffic to that tunnel. +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified.") + } + target := outbound.Target + targetAddr := target.NetAddr() + + if target.Network == net.Network_UDP { + return newError("UDP is not supported by HTTP outbound") + } + + var user *protocol.MemoryUser + var conn internet.Connection + + mbuf, _ := link.Reader.ReadMultiBuffer() + len := mbuf.Len() + firstPayload := bytespool.Alloc(len) + mbuf, _ = buf.SplitBytes(mbuf, firstPayload) + firstPayload = firstPayload[:len] + + buf.ReleaseMulti(mbuf) + defer bytespool.Free(firstPayload) + + if err := retry.ExponentialBackoff(5, 100).On(func() error { + server := c.serverPicker.PickServer() + dest := server.Destination() + user = server.PickUser() + + netConn, err := setUpHTTPTunnel(ctx, dest, targetAddr, user, dialer, firstPayload) + if netConn != nil { + if _, ok := netConn.(*http2Conn); !ok { + if _, err := netConn.Write(firstPayload); err != nil { + netConn.Close() + return err + } + } + conn = internet.Connection(netConn) + } + return err + }); err != nil { + return newError("failed to find an available destination").Base(err) + } + + defer func() { + if err := conn.Close(); err != nil { + newError("failed to closed connection").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + }() + + p := c.policyManager.ForLevel(0) + if user != nil { + p = c.policyManager.ForLevel(user.Level) + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, p.Timeouts.ConnectionIdle) + + requestFunc := func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc := func() error { + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + + var responseDonePost = task.OnSuccess(responseFunc, task.Close(link.Writer)) + if err := task.Run(ctx, requestFunc, responseDonePost); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +// setUpHTTPTunnel will create a socket tunnel via HTTP CONNECT method +func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, user *protocol.MemoryUser, dialer internet.Dialer, firstPayload []byte) (net.Conn, error) { + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Host: target}, + Header: make(http.Header), + Host: target, + } + + if user != nil && user.Account != nil { + account := user.Account.(*Account) + auth := account.GetUsername() + ":" + account.GetPassword() + req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + } + + connectHTTP1 := func(rawConn net.Conn) (net.Conn, error) { + req.Header.Set("Proxy-Connection", "Keep-Alive") + + err := req.Write(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) + if err != nil { + rawConn.Close() + return nil, err + } + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, newError("Proxy responded with non 200 code: " + resp.Status) + } + return rawConn, nil + } + + connectHTTP2 := func(rawConn net.Conn, h2clientConn *http2.ClientConn) (net.Conn, error) { + pr, pw := io.Pipe() + req.Body = pr + + var pErr error + var wg sync.WaitGroup + wg.Add(1) + + go func() { + _, pErr = pw.Write(firstPayload) + wg.Done() + }() + + resp, err := h2clientConn.RoundTrip(req) + if err != nil { + rawConn.Close() + return nil, err + } + + wg.Wait() + if pErr != nil { + rawConn.Close() + return nil, pErr + } + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, newError("Proxy responded with non 200 code: " + resp.Status) + } + return newHTTP2Conn(rawConn, pw, resp.Body), nil + } + + cachedH2Mutex.Lock() + cachedConn, cachedConnFound := cachedH2Conns[dest] + cachedH2Mutex.Unlock() + + if cachedConnFound { + rc, cc := cachedConn.rawConn, cachedConn.h2Conn + if cc.CanTakeNewRequest() { + proxyConn, err := connectHTTP2(rc, cc) + if err != nil { + return nil, err + } + + return proxyConn, nil + } + } + + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return nil, err + } + + iConn := rawConn + if statConn, ok := iConn.(*internet.StatCouterConnection); ok { + iConn = statConn.Connection + } + + nextProto := "" + if tlsConn, ok := iConn.(*tls.Conn); ok { + if err := tlsConn.Handshake(); err != nil { + rawConn.Close() + return nil, err + } + nextProto = tlsConn.ConnectionState().NegotiatedProtocol + } + + switch nextProto { + case "", "http/1.1": + return connectHTTP1(rawConn) + case "h2": + t := http2.Transport{} + h2clientConn, err := t.NewClientConn(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + proxyConn, err := connectHTTP2(rawConn, h2clientConn) + if err != nil { + rawConn.Close() + return nil, err + } + + cachedH2Mutex.Lock() + if cachedH2Conns == nil { + cachedH2Conns = make(map[net.Destination]h2Conn) + } + + cachedH2Conns[dest] = h2Conn{ + rawConn: rawConn, + h2Conn: h2clientConn, + } + cachedH2Mutex.Unlock() + + return proxyConn, err + default: + return nil, newError("negotiated unsupported application layer protocol: " + nextProto) + } +} + +func newHTTP2Conn(c net.Conn, pipedReqBody *io.PipeWriter, respBody io.ReadCloser) net.Conn { + return &http2Conn{Conn: c, in: pipedReqBody, out: respBody} +} + +type http2Conn struct { + net.Conn + in *io.PipeWriter + out io.ReadCloser +} + +func (h *http2Conn) Read(p []byte) (n int, err error) { + return h.out.Read(p) +} + +func (h *http2Conn) Write(p []byte) (n int, err error) { + return h.in.Write(p) +} + +func (h *http2Conn) Close() error { + h.in.Close() + return h.out.Close() +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/http/config.go b/proxy/http/config.go new file mode 100644 index 00000000..e12bd43c --- /dev/null +++ b/proxy/http/config.go @@ -0,0 +1,28 @@ +package http + +import ( + "github.com/xtls/xray-core/v1/common/protocol" +) + +func (a *Account) Equals(another protocol.Account) bool { + if account, ok := another.(*Account); ok { + return a.Username == account.Username + } + return false +} + +func (a *Account) AsAccount() (protocol.Account, error) { + return a, nil +} + +func (sc *ServerConfig) HasAccount(username, password string) bool { + if sc.Accounts == nil { + return false + } + + p, found := sc.Accounts[username] + if !found { + return false + } + return p == password +} diff --git a/proxy/http/config.pb.go b/proxy/http/config.pb.go new file mode 100644 index 00000000..79966d11 --- /dev/null +++ b/proxy/http/config.pb.go @@ -0,0 +1,339 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/http/config.proto + +package http + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_http_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// Config for HTTP proxy server. +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Deprecated: Do not use. + Timeout uint32 `protobuf:"varint,1,opt,name=timeout,proto3" json:"timeout,omitempty"` + Accounts map[string]string `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + AllowTransparent bool `protobuf:"varint,3,opt,name=allow_transparent,json=allowTransparent,proto3" json:"allow_transparent,omitempty"` + UserLevel uint32 `protobuf:"varint,4,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_http_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{1} +} + +// Deprecated: Do not use. +func (x *ServerConfig) GetTimeout() uint32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *ServerConfig) GetAccounts() map[string]string { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *ServerConfig) GetAllowTransparent() bool { + if x != nil { + return x.AllowTransparent + } + return false +} + +func (x *ServerConfig) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +// ClientConfig is the protobuf config for HTTP proxy client. +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Sever is a list of HTTP server addresses. + Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_http_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_http_config_proto protoreflect.FileDescriptor + +var file_proxy_http_config_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x41, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x22, 0xfe, 0x01, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x1c, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, + 0x47, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 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, 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, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, + 0x52, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x68, 0x74, 0x74, + 0x70, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x48, + 0x74, 0x74, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_http_config_proto_rawDescOnce sync.Once + file_proxy_http_config_proto_rawDescData = file_proxy_http_config_proto_rawDesc +) + +func file_proxy_http_config_proto_rawDescGZIP() []byte { + file_proxy_http_config_proto_rawDescOnce.Do(func() { + file_proxy_http_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_http_config_proto_rawDescData) + }) + return file_proxy_http_config_proto_rawDescData +} + +var file_proxy_http_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_http_config_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: xray.proxy.http.Account + (*ServerConfig)(nil), // 1: xray.proxy.http.ServerConfig + (*ClientConfig)(nil), // 2: xray.proxy.http.ClientConfig + nil, // 3: xray.proxy.http.ServerConfig.AccountsEntry + (*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint +} +var file_proxy_http_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.http.ServerConfig.accounts:type_name -> xray.proxy.http.ServerConfig.AccountsEntry + 4, // 1: xray.proxy.http.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_proxy_http_config_proto_init() } +func file_proxy_http_config_proto_init() { + if File_proxy_http_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_http_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_http_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_http_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); 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_proxy_http_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_http_config_proto_goTypes, + DependencyIndexes: file_proxy_http_config_proto_depIdxs, + MessageInfos: file_proxy_http_config_proto_msgTypes, + }.Build() + File_proxy_http_config_proto = out.File + file_proxy_http_config_proto_rawDesc = nil + file_proxy_http_config_proto_goTypes = nil + file_proxy_http_config_proto_depIdxs = nil +} diff --git a/proxy/http/config.proto b/proxy/http/config.proto new file mode 100644 index 00000000..212e7227 --- /dev/null +++ b/proxy/http/config.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package xray.proxy.http; +option csharp_namespace = "Xray.Proxy.Http"; +option go_package = "github.com/xtls/xray-core/v1/proxy/http"; +option java_package = "com.xray.proxy.http"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Account { + string username = 1; + string password = 2; +} + +// Config for HTTP proxy server. +message ServerConfig { + uint32 timeout = 1 [deprecated = true]; + map accounts = 2; + bool allow_transparent = 3; + uint32 user_level = 4; +} + +// ClientConfig is the protobuf config for HTTP proxy client. +message ClientConfig { + // Sever is a list of HTTP server addresses. + repeated xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/proxy/http/errors.generated.go b/proxy/http/errors.generated.go new file mode 100644 index 00000000..b07b3fec --- /dev/null +++ b/proxy/http/errors.generated.go @@ -0,0 +1,9 @@ +package http + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/http/http.go b/proxy/http/http.go new file mode 100644 index 00000000..9dc78532 --- /dev/null +++ b/proxy/http/http.go @@ -0,0 +1,3 @@ +package http + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/proxy/http/server.go b/proxy/http/server.go new file mode 100644 index 00000000..acdc1665 --- /dev/null +++ b/proxy/http/server.go @@ -0,0 +1,329 @@ +// +build !confonly + +package http + +import ( + "bufio" + "context" + "encoding/base64" + "io" + "net/http" + "strings" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/log" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + http_proto "github.com/xtls/xray-core/v1/common/protocol/http" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Server is an HTTP proxy server. +type Server struct { + config *ServerConfig + policyManager policy.Manager +} + +// NewServer creates a new HTTP inbound handler. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return s, nil +} + +func (s *Server) policy() policy.Session { + config := s.config + p := s.policyManager.ForLevel(config.UserLevel) + if config.Timeout > 0 && config.UserLevel == 0 { + p.Timeouts.ConnectionIdle = time.Duration(config.Timeout) * time.Second + } + return p +} + +// Network implements proxy.Inbound. +func (*Server) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +func isTimeout(err error) bool { + nerr, ok := errors.Cause(err).(net.Error) + return ok && nerr.Timeout() +} + +func parseBasicAuth(auth string) (username, password string, ok bool) { + const prefix = "Basic " + if !strings.HasPrefix(auth, prefix) { + return + } + c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) + if err != nil { + return + } + cs := string(c) + s := strings.IndexByte(cs, ':') + if s < 0 { + return + } + return cs[:s], cs[s+1:], true +} + +type readerOnly struct { + io.Reader +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + if inbound != nil { + inbound.User = &protocol.MemoryUser{ + Level: s.config.UserLevel, + } + } + + reader := bufio.NewReaderSize(readerOnly{conn}, buf.Size) + +Start: + if err := conn.SetReadDeadline(time.Now().Add(s.policy().Timeouts.Handshake)); err != nil { + newError("failed to set read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + request, err := http.ReadRequest(reader) + if err != nil { + trace := newError("failed to read http request").Base(err) + if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) { + trace.AtWarning() + } + return trace + } + + if len(s.config.Accounts) > 0 { + user, pass, ok := parseBasicAuth(request.Header.Get("Proxy-Authorization")) + if !ok || !s.config.HasAccount(user, pass) { + return common.Error2(conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n"))) + } + if inbound != nil { + inbound.User.Email = user + } + } + + newError("request to Method [", request.Method, "] Host [", request.Host, "] with URL [", request.URL, "]").WriteToLog(session.ExportIDToError(ctx)) + if err := conn.SetReadDeadline(time.Time{}); err != nil { + newError("failed to clear read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + defaultPort := net.Port(80) + if strings.EqualFold(request.URL.Scheme, "https") { + defaultPort = net.Port(443) + } + host := request.Host + if host == "" { + host = request.URL.Host + } + dest, err := http_proto.ParseHost(host, defaultPort) + if err != nil { + return newError("malformed proxy host: ", host).AtWarning().Base(err) + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: request.URL, + Status: log.AccessAccepted, + Reason: "", + }) + + if strings.EqualFold(request.Method, "CONNECT") { + return s.handleConnect(ctx, request, reader, conn, dest, dispatcher) + } + + keepAlive := (strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive") + + err = s.handlePlainHTTP(ctx, request, conn, dest, dispatcher) + if err == errWaitAnother { + if keepAlive { + goto Start + } + err = nil + } + + return err +} + +func (s *Server) handleConnect(ctx context.Context, _ *http.Request, reader *bufio.Reader, conn internet.Connection, dest net.Destination, dispatcher routing.Dispatcher) error { + _, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) + if err != nil { + return newError("failed to write back OK response").Base(err) + } + + plcy := s.policy() + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, plcy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, plcy.Buffer) + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + if reader.Buffered() > 0 { + payload, err := buf.ReadFrom(io.LimitReader(reader, int64(reader.Buffered()))) + if err != nil { + return err + } + if err := link.Writer.WriteMultiBuffer(payload); err != nil { + return err + } + reader = nil + } + + requestDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + + responseDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) + + v2writer := buf.NewWriter(conn) + if err := buf.Copy(link.Reader, v2writer, buf.UpdateActivity(timer)); err != nil { + return err + } + + return nil + } + + var closeWriter = task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, closeWriter, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} + +var errWaitAnother = newError("keep alive") + +func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, writer io.Writer, dest net.Destination, dispatcher routing.Dispatcher) error { + if !s.config.AllowTransparent && request.URL.Host == "" { + // RFC 2068 (HTTP/1.1) requires URL to be absolute URL in HTTP proxy. + response := &http.Response{ + Status: "Bad Request", + StatusCode: 400, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + response.Header.Set("Proxy-Connection", "close") + response.Header.Set("Connection", "close") + return response.Write(writer) + } + + if len(request.URL.Host) > 0 { + request.Host = request.URL.Host + } + http_proto.RemoveHopByHopHeaders(request.Header) + + // Prevent UA from being set to golang's default ones + if request.Header.Get("User-Agent") == "" { + request.Header.Set("User-Agent", "") + } + + content := &session.Content{ + Protocol: "http/1.1", + } + + content.SetAttribute(":method", strings.ToUpper(request.Method)) + content.SetAttribute(":path", request.URL.Path) + for key := range request.Header { + value := request.Header.Get(key) + content.SetAttribute(strings.ToLower(key), value) + } + + ctx = session.ContextWithContent(ctx, content) + + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + // Plain HTTP request is not a stream. The request always finishes before response. Hense request has to be closed later. + defer common.Close(link.Writer) + var result error = errWaitAnother + + requestDone := func() error { + request.Header.Set("Connection", "close") + + requestWriter := buf.NewBufferedWriter(link.Writer) + common.Must(requestWriter.SetBuffered(false)) + if err := request.Write(requestWriter); err != nil { + return newError("failed to write whole request").Base(err).AtWarning() + } + return nil + } + + responseDone := func() error { + responseReader := bufio.NewReaderSize(&buf.BufferedReader{Reader: link.Reader}, buf.Size) + response, err := http.ReadResponse(responseReader, request) + if err == nil { + http_proto.RemoveHopByHopHeaders(response.Header) + if response.ContentLength >= 0 { + response.Header.Set("Proxy-Connection", "keep-alive") + response.Header.Set("Connection", "keep-alive") + response.Header.Set("Keep-Alive", "timeout=4") + response.Close = false + } else { + response.Close = true + result = nil + } + } else { + newError("failed to read response from ", request.Host).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + response = &http.Response{ + Status: "Service Unavailable", + StatusCode: 503, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + response.Header.Set("Connection", "close") + response.Header.Set("Proxy-Connection", "close") + } + if err := response.Write(writer); err != nil { + return newError("failed to write response").Base(err).AtWarning() + } + return nil + } + + if err := task.Run(ctx, requestDone, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return result +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/proxy/mtproto/auth.go b/proxy/mtproto/auth.go new file mode 100644 index 00000000..1261d199 --- /dev/null +++ b/proxy/mtproto/auth.go @@ -0,0 +1,150 @@ +package mtproto + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "io" + "sync" + + "github.com/xtls/xray-core/v1/common" +) + +const ( + HeaderSize = 64 +) + +type SessionContext struct { + ConnectionType [4]byte + DataCenterID uint16 +} + +func DefaultSessionContext() SessionContext { + return SessionContext{ + ConnectionType: [4]byte{0xef, 0xef, 0xef, 0xef}, + DataCenterID: 0, + } +} + +type contextKey int32 + +const ( + sessionContextKey contextKey = iota +) + +func ContextWithSessionContext(ctx context.Context, c SessionContext) context.Context { + return context.WithValue(ctx, sessionContextKey, c) +} + +func SessionContextFromContext(ctx context.Context) SessionContext { + if c := ctx.Value(sessionContextKey); c != nil { + return c.(SessionContext) + } + return DefaultSessionContext() +} + +type Authentication struct { + Header [HeaderSize]byte + DecodingKey [32]byte + EncodingKey [32]byte + DecodingNonce [16]byte + EncodingNonce [16]byte +} + +func (a *Authentication) DataCenterID() uint16 { + x := ((int16(a.Header[61]) << 8) | int16(a.Header[60])) + if x < 0 { + x = -x + } + return uint16(x) - 1 +} + +func (a *Authentication) ConnectionType() [4]byte { + var x [4]byte + copy(x[:], a.Header[56:60]) + return x +} + +func (a *Authentication) ApplySecret(b []byte) { + a.DecodingKey = sha256.Sum256(append(a.DecodingKey[:], b...)) + a.EncodingKey = sha256.Sum256(append(a.EncodingKey[:], b...)) +} + +func generateRandomBytes(random []byte, connType [4]byte) { + for { + common.Must2(rand.Read(random)) + + if random[0] == 0xef { + continue + } + + val := (uint32(random[3]) << 24) | (uint32(random[2]) << 16) | (uint32(random[1]) << 8) | uint32(random[0]) + if val == 0x44414548 || val == 0x54534f50 || val == 0x20544547 || val == 0x4954504f || val == 0xeeeeeeee { + continue + } + + if (uint32(random[7])<<24)|(uint32(random[6])<<16)|(uint32(random[5])<<8)|uint32(random[4]) == 0x00000000 { + continue + } + + copy(random[56:60], connType[:]) + + return + } +} + +func NewAuthentication(sc SessionContext) *Authentication { + auth := getAuthenticationObject() + random := auth.Header[:] + generateRandomBytes(random, sc.ConnectionType) + copy(auth.EncodingKey[:], random[8:]) + copy(auth.EncodingNonce[:], random[8+32:]) + keyivInverse := Inverse(random[8 : 8+32+16]) + copy(auth.DecodingKey[:], keyivInverse) + copy(auth.DecodingNonce[:], keyivInverse[32:]) + return auth +} + +func ReadAuthentication(reader io.Reader) (*Authentication, error) { + auth := getAuthenticationObject() + + if _, err := io.ReadFull(reader, auth.Header[:]); err != nil { + putAuthenticationObject(auth) + return nil, err + } + + copy(auth.DecodingKey[:], auth.Header[8:]) + copy(auth.DecodingNonce[:], auth.Header[8+32:]) + keyivInverse := Inverse(auth.Header[8 : 8+32+16]) + copy(auth.EncodingKey[:], keyivInverse) + copy(auth.EncodingNonce[:], keyivInverse[32:]) + + return auth, nil +} + +// Inverse returns a new byte array. It is a sequence of bytes when the input is read from end to beginning.Inverse +// Visible for testing only. +func Inverse(b []byte) []byte { + lenb := len(b) + b2 := make([]byte, lenb) + for i, v := range b { + b2[lenb-i-1] = v + } + return b2 +} + +var ( + authPool = sync.Pool{ + New: func() interface{} { + return new(Authentication) + }, + } +) + +func getAuthenticationObject() *Authentication { + return authPool.Get().(*Authentication) +} + +func putAuthenticationObject(auth *Authentication) { + authPool.Put(auth) +} diff --git a/proxy/mtproto/auth_test.go b/proxy/mtproto/auth_test.go new file mode 100644 index 00000000..3bdff593 --- /dev/null +++ b/proxy/mtproto/auth_test.go @@ -0,0 +1,53 @@ +package mtproto_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/proxy/mtproto" +) + +func TestInverse(t *testing.T) { + const size = 64 + b := make([]byte, 64) + for b[0] == b[size-1] { + common.Must2(rand.Read(b)) + } + + bi := Inverse(b) + if b[0] == bi[0] { + t.Fatal("seems bytes are not inversed: ", b[0], "vs", bi[0]) + } + + bii := Inverse(bi) + if r := cmp.Diff(bii, b); r != "" { + t.Fatal(r) + } +} + +func TestAuthenticationReadWrite(t *testing.T) { + a := NewAuthentication(DefaultSessionContext()) + b := bytes.NewReader(a.Header[:]) + a2, err := ReadAuthentication(b) + common.Must(err) + + if r := cmp.Diff(a.EncodingKey[:], a2.DecodingKey[:]); r != "" { + t.Error("decoding key: ", r) + } + + if r := cmp.Diff(a.EncodingNonce[:], a2.DecodingNonce[:]); r != "" { + t.Error("decoding nonce: ", r) + } + + if r := cmp.Diff(a.DecodingKey[:], a2.EncodingKey[:]); r != "" { + t.Error("encoding key: ", r) + } + + if r := cmp.Diff(a.DecodingNonce[:], a2.EncodingNonce[:]); r != "" { + t.Error("encoding nonce: ", r) + } +} diff --git a/proxy/mtproto/client.go b/proxy/mtproto/client.go new file mode 100644 index 00000000..6c2f4439 --- /dev/null +++ b/proxy/mtproto/client.go @@ -0,0 +1,77 @@ +package mtproto + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "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/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +type Client struct { +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + return &Client{}, nil +} + +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("unknown destination.") + } + dest := outbound.Target + if dest.Network != net.Network_TCP { + return newError("not TCP traffic", dest) + } + + conn, err := dialer.Dial(ctx, dest) + if err != nil { + return newError("failed to dial to ", dest).Base(err).AtWarning() + } + defer conn.Close() + + sc := SessionContextFromContext(ctx) + auth := NewAuthentication(sc) + defer putAuthenticationObject(auth) + + request := func() error { + encryptor := crypto.NewAesCTRStream(auth.EncodingKey[:], auth.EncodingNonce[:]) + + var header [HeaderSize]byte + encryptor.XORKeyStream(header[:], auth.Header[:]) + copy(header[:56], auth.Header[:]) + + if _, err := conn.Write(header[:]); err != nil { + return newError("failed to write auth header").Base(err) + } + + connWriter := buf.NewWriter(crypto.NewCryptionWriter(encryptor, conn)) + return buf.Copy(link.Reader, connWriter) + } + + response := func() error { + decryptor := crypto.NewAesCTRStream(auth.DecodingKey[:], auth.DecodingNonce[:]) + + connReader := buf.NewReader(crypto.NewCryptionReader(decryptor, conn)) + return buf.Copy(connReader, link.Writer) + } + + var responseDoneAndCloseWriter = task.OnSuccess(response, task.Close(link.Writer)) + if err := task.Run(ctx, request, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/mtproto/config.go b/proxy/mtproto/config.go new file mode 100644 index 00000000..016efcde --- /dev/null +++ b/proxy/mtproto/config.go @@ -0,0 +1,24 @@ +package mtproto + +import ( + "github.com/xtls/xray-core/v1/common/protocol" +) + +func (a *Account) Equals(another protocol.Account) bool { + aa, ok := another.(*Account) + if !ok { + return false + } + + if len(a.Secret) != len(aa.Secret) { + return false + } + + for i, v := range a.Secret { + if v != aa.Secret[i] { + return false + } + } + + return true +} diff --git a/proxy/mtproto/config.pb.go b/proxy/mtproto/config.pb.go new file mode 100644 index 00000000..268dc44c --- /dev/null +++ b/proxy/mtproto/config.pb.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/mtproto/config.proto + +package mtproto + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Secret []byte `protobuf:"bytes,1,opt,name=secret,proto3" json:"secret,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_mtproto_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_mtproto_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_mtproto_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetSecret() []byte { + if x != nil { + return x.Secret + } + return nil +} + +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // User is a list of users that allowed to connect to this inbound. + // Although this is a repeated field, only the first user is effective for + // now. + User []*protocol.User `protobuf:"bytes,1,rep,name=user,proto3" json:"user,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_mtproto_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_mtproto_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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_mtproto_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetUser() []*protocol.User { + if x != nil { + return x.User + } + return nil +} + +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_mtproto_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_mtproto_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_mtproto_config_proto_rawDescGZIP(), []int{2} +} + +var File_proxy_mtproto_config_proto protoreflect.FileDescriptor + +var file_proxy_mtproto_config_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x6d, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x6d, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x1a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x21, 0x0a, 0x07, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, + 0x3e, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, + 0x0e, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, + 0x5b, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x6d, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, + 0x6d, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xaa, 0x02, 0x12, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x4d, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_mtproto_config_proto_rawDescOnce sync.Once + file_proxy_mtproto_config_proto_rawDescData = file_proxy_mtproto_config_proto_rawDesc +) + +func file_proxy_mtproto_config_proto_rawDescGZIP() []byte { + file_proxy_mtproto_config_proto_rawDescOnce.Do(func() { + file_proxy_mtproto_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_mtproto_config_proto_rawDescData) + }) + return file_proxy_mtproto_config_proto_rawDescData +} + +var file_proxy_mtproto_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_mtproto_config_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: xray.proxy.mtproto.Account + (*ServerConfig)(nil), // 1: xray.proxy.mtproto.ServerConfig + (*ClientConfig)(nil), // 2: xray.proxy.mtproto.ClientConfig + (*protocol.User)(nil), // 3: xray.common.protocol.User +} +var file_proxy_mtproto_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.mtproto.ServerConfig.user:type_name -> xray.common.protocol.User + 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_proxy_mtproto_config_proto_init() } +func file_proxy_mtproto_config_proto_init() { + if File_proxy_mtproto_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_mtproto_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_mtproto_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_mtproto_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); 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_proxy_mtproto_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_mtproto_config_proto_goTypes, + DependencyIndexes: file_proxy_mtproto_config_proto_depIdxs, + MessageInfos: file_proxy_mtproto_config_proto_msgTypes, + }.Build() + File_proxy_mtproto_config_proto = out.File + file_proxy_mtproto_config_proto_rawDesc = nil + file_proxy_mtproto_config_proto_goTypes = nil + file_proxy_mtproto_config_proto_depIdxs = nil +} diff --git a/proxy/mtproto/config.proto b/proxy/mtproto/config.proto new file mode 100644 index 00000000..20be7569 --- /dev/null +++ b/proxy/mtproto/config.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package xray.proxy.mtproto; +option csharp_namespace = "Xray.Proxy.Mtproto"; +option go_package = "github.com/xtls/xray-core/v1/proxy/mtproto"; +option java_package = "com.xray.proxy.mtproto"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; + +message Account { + bytes secret = 1; +} + +message ServerConfig { + // User is a list of users that allowed to connect to this inbound. + // Although this is a repeated field, only the first user is effective for + // now. + repeated xray.common.protocol.User user = 1; +} + +message ClientConfig {} diff --git a/proxy/mtproto/errors.generated.go b/proxy/mtproto/errors.generated.go new file mode 100644 index 00000000..5dd70350 --- /dev/null +++ b/proxy/mtproto/errors.generated.go @@ -0,0 +1,9 @@ +package mtproto + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/mtproto/mtproto.go b/proxy/mtproto/mtproto.go new file mode 100644 index 00000000..2103186d --- /dev/null +++ b/proxy/mtproto/mtproto.go @@ -0,0 +1,3 @@ +package mtproto + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/proxy/mtproto/server.go b/proxy/mtproto/server.go new file mode 100644 index 00000000..6e4d3ddc --- /dev/null +++ b/proxy/mtproto/server.go @@ -0,0 +1,162 @@ +// +build !confonly + +package mtproto + +import ( + "bytes" + "context" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "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/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport/internet" +) + +var ( + dcList = []net.Address{ + net.ParseAddress("149.154.175.50"), + net.ParseAddress("149.154.167.51"), + net.ParseAddress("149.154.175.100"), + net.ParseAddress("149.154.167.91"), + net.ParseAddress("149.154.171.5"), + } +) + +type Server struct { + user *protocol.User + account *Account + policy policy.Manager +} + +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + if len(config.User) == 0 { + return nil, newError("no user configured.") + } + + user := config.User[0] + rawAccount, err := config.User[0].GetTypedAccount() + if err != nil { + return nil, newError("invalid account").Base(err) + } + account, ok := rawAccount.(*Account) + if !ok { + return nil, newError("not a MTProto account") + } + + v := core.MustFromContext(ctx) + + return &Server{ + user: user, + account: account, + policy: v.GetFeature(policy.ManagerType()).(policy.Manager), + }, nil +} + +func (s *Server) Network() []net.Network { + return []net.Network{net.Network_TCP} +} + +var ctype1 = []byte{0xef, 0xef, 0xef, 0xef} +var ctype2 = []byte{0xee, 0xee, 0xee, 0xee} + +func isValidConnectionType(c [4]byte) bool { + if bytes.Equal(c[:], ctype1) { + return true + } + if bytes.Equal(c[:], ctype2) { + return true + } + return false +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + sPolicy := s.policy.ForLevel(s.user.Level) + + if err := conn.SetDeadline(time.Now().Add(sPolicy.Timeouts.Handshake)); err != nil { + newError("failed to set deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + auth, err := ReadAuthentication(conn) + if err != nil { + return newError("failed to read authentication header").Base(err) + } + defer putAuthenticationObject(auth) + + if err := conn.SetDeadline(time.Time{}); err != nil { + newError("failed to clear deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + auth.ApplySecret(s.account.Secret) + + decryptor := crypto.NewAesCTRStream(auth.DecodingKey[:], auth.DecodingNonce[:]) + decryptor.XORKeyStream(auth.Header[:], auth.Header[:]) + + ct := auth.ConnectionType() + if !isValidConnectionType(ct) { + return newError("invalid connection type: ", ct) + } + + dcID := auth.DataCenterID() + if dcID >= uint16(len(dcList)) { + return newError("invalid datacenter id: ", dcID) + } + + dest := net.Destination{ + Network: net.Network_TCP, + Address: dcList[dcID], + Port: net.Port(443), + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sPolicy.Buffer) + + sc := SessionContext{ + ConnectionType: ct, + DataCenterID: dcID, + } + ctx = ContextWithSessionContext(ctx, sc) + + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return newError("failed to dispatch request to: ", dest).Base(err) + } + + request := func() error { + defer timer.SetTimeout(sPolicy.Timeouts.DownlinkOnly) + + reader := buf.NewReader(crypto.NewCryptionReader(decryptor, conn)) + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + + response := func() error { + defer timer.SetTimeout(sPolicy.Timeouts.UplinkOnly) + + encryptor := crypto.NewAesCTRStream(auth.EncodingKey[:], auth.EncodingNonce[:]) + writer := buf.NewWriter(crypto.NewCryptionWriter(encryptor, conn)) + return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)) + } + + var responseDoneAndCloseWriter = task.OnSuccess(response, task.Close(link.Writer)) + if err := task.Run(ctx, request, responseDoneAndCloseWriter); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 00000000..59b95a5a --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,48 @@ +// Package proxy contains all proxies used by Xray. +// +// To implement an inbound or outbound proxy, one needs to do the following: +// 1. Implement the interface(s) below. +// 2. Register a config creator through common.RegisterConfig. +package proxy + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// An Inbound processes inbound connections. +type Inbound interface { + // Network returns a list of networks that this inbound supports. Connections with not-supported networks will not be passed into Process(). + Network() []net.Network + + // Process processes a connection of given network. If necessary, the Inbound can dispatch the connection to an Outbound. + Process(context.Context, net.Network, internet.Connection, routing.Dispatcher) error +} + +// An Outbound process outbound connections. +type Outbound interface { + // Process processes the given connection. The given dialer may be used to dial a system outbound connection. + Process(context.Context, *transport.Link, internet.Dialer) error +} + +// UserManager is the interface for Inbounds and Outbounds that can manage their users. +type UserManager interface { + // AddUser adds a new user. + AddUser(context.Context, *protocol.MemoryUser) error + + // RemoveUser removes a user by email. + RemoveUser(context.Context, string) error +} + +type GetInbound interface { + GetInbound() Inbound +} + +type GetOutbound interface { + GetOutbound() Outbound +} diff --git a/proxy/shadowsocks/client.go b/proxy/shadowsocks/client.go new file mode 100644 index 00000000..94a984ab --- /dev/null +++ b/proxy/shadowsocks/client.go @@ -0,0 +1,182 @@ +// +build !confonly + +package shadowsocks + +import ( + "context" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Client is a inbound handler for Shadowsocks protocol +type Client struct { + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// NewClient create a new Shadowsocks client. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Server { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to parse server spec").Base(err) + } + serverList.AddServer(s) + } + if serverList.Size() == 0 { + return nil, newError("0 server") + } + + v := core.MustFromContext(ctx) + client := &Client{ + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +// Process implements OutboundHandler.Process(). +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified") + } + destination := outbound.Target + network := destination.Network + + var server *protocol.ServerSpec + var conn internet.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + server = c.serverPicker.PickServer() + dest := server.Destination() + dest.Network = network + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return err + } + conn = rawConn + + return nil + }) + if err != nil { + return newError("failed to find an available destination").AtWarning().Base(err) + } + newError("tunneling request to ", destination, " via ", server.Destination()).WriteToLog(session.ExportIDToError(ctx)) + + defer conn.Close() + + request := &protocol.RequestHeader{ + Version: Version, + Address: destination.Address, + Port: destination.Port, + } + if destination.Network == net.Network_TCP { + request.Command = protocol.RequestCommandTCP + } else { + request.Command = protocol.RequestCommandUDP + } + + user := server.PickUser() + _, ok := user.Account.(*MemoryAccount) + if !ok { + return newError("user account is not valid") + } + request.User = user + + sessionPolicy := c.policyManager.ForLevel(user.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + if request.Command == protocol.RequestCommandTCP { + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + bodyWriter, err := WriteTCPRequest(request, bufferedWriter) + if err != nil { + return newError("failed to write request").Base(err) + } + + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)) + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + responseReader, err := ReadTCPResponse(user, conn) + if err != nil { + return err + } + + return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer)) + } + + var responseDoneAndCloseWriter = task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil + } + + if request.Command == protocol.RequestCommandUDP { + writer := &buf.SequentialWriter{Writer: &UDPWriter{ + Writer: conn, + Request: request, + }} + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all UDP request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &UDPReader{ + Reader: conn, + User: user, + } + + if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all UDP response").Base(err) + } + return nil + } + + var responseDoneAndCloseWriter = task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/shadowsocks/config.go b/proxy/shadowsocks/config.go new file mode 100644 index 00000000..5c21541f --- /dev/null +++ b/proxy/shadowsocks/config.go @@ -0,0 +1,309 @@ +package shadowsocks + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/sha1" + "io" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "github.com/xtls/xray-core/v1/common/protocol" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct { + Cipher Cipher + Key []byte +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return bytes.Equal(a.Key, account.Key) + } + return false +} + +func createAesGcm(key []byte) cipher.AEAD { + block, err := aes.NewCipher(key) + common.Must(err) + gcm, err := cipher.NewGCM(block) + common.Must(err) + return gcm +} + +func createChacha20Poly1305(key []byte) cipher.AEAD { + chacha20, err := chacha20poly1305.New(key) + common.Must(err) + return chacha20 +} + +func (a *Account) getCipher() (Cipher, error) { + switch a.CipherType { + case CipherType_AES_128_CFB: + return &AesCfb{KeyBytes: 16}, nil + case CipherType_AES_256_CFB: + return &AesCfb{KeyBytes: 32}, nil + case CipherType_CHACHA20: + return &ChaCha20{IVBytes: 8}, nil + case CipherType_CHACHA20_IETF: + return &ChaCha20{IVBytes: 12}, nil + case CipherType_AES_128_GCM: + return &AEADCipher{ + KeyBytes: 16, + IVBytes: 16, + AEADAuthCreator: createAesGcm, + }, nil + case CipherType_AES_256_GCM: + return &AEADCipher{ + KeyBytes: 32, + IVBytes: 32, + AEADAuthCreator: createAesGcm, + }, nil + case CipherType_CHACHA20_POLY1305: + return &AEADCipher{ + KeyBytes: 32, + IVBytes: 32, + AEADAuthCreator: createChacha20Poly1305, + }, nil + case CipherType_NONE: + return NoneCipher{}, nil + default: + return nil, newError("Unsupported cipher.") + } +} + +// AsAccount implements protocol.AsAccount. +func (a *Account) AsAccount() (protocol.Account, error) { + cipher, err := a.getCipher() + if err != nil { + return nil, newError("failed to get cipher").Base(err) + } + return &MemoryAccount{ + Cipher: cipher, + Key: passwordToCipherKey([]byte(a.Password), cipher.KeySize()), + }, nil +} + +// Cipher is an interface for all Shadowsocks ciphers. +type Cipher interface { + KeySize() int32 + IVSize() int32 + NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) + NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) + IsAEAD() bool + EncodePacket(key []byte, b *buf.Buffer) error + DecodePacket(key []byte, b *buf.Buffer) error +} + +// AesCfb represents all AES-CFB ciphers. +type AesCfb struct { + KeyBytes int32 +} + +func (*AesCfb) IsAEAD() bool { + return false +} + +func (v *AesCfb) KeySize() int32 { + return v.KeyBytes +} + +func (v *AesCfb) IVSize() int32 { + return 16 +} + +func (v *AesCfb) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + stream := crypto.NewAesEncryptionStream(key, iv) + return &buf.SequentialWriter{Writer: crypto.NewCryptionWriter(stream, writer)}, nil +} + +func (v *AesCfb) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + stream := crypto.NewAesDecryptionStream(key, iv) + return &buf.SingleReader{ + Reader: crypto.NewCryptionReader(stream, reader), + }, nil +} + +func (v *AesCfb) EncodePacket(key []byte, b *buf.Buffer) error { + iv := b.BytesTo(v.IVSize()) + stream := crypto.NewAesEncryptionStream(key, iv) + stream.XORKeyStream(b.BytesFrom(v.IVSize()), b.BytesFrom(v.IVSize())) + return nil +} + +func (v *AesCfb) DecodePacket(key []byte, b *buf.Buffer) error { + if b.Len() <= v.IVSize() { + return newError("insufficient data: ", b.Len()) + } + iv := b.BytesTo(v.IVSize()) + stream := crypto.NewAesDecryptionStream(key, iv) + stream.XORKeyStream(b.BytesFrom(v.IVSize()), b.BytesFrom(v.IVSize())) + b.Advance(v.IVSize()) + return nil +} + +type AEADCipher struct { + KeyBytes int32 + IVBytes int32 + AEADAuthCreator func(key []byte) cipher.AEAD +} + +func (*AEADCipher) IsAEAD() bool { + return true +} + +func (c *AEADCipher) KeySize() int32 { + return c.KeyBytes +} + +func (c *AEADCipher) IVSize() int32 { + return c.IVBytes +} + +func (c *AEADCipher) createAuthenticator(key []byte, iv []byte) *crypto.AEADAuthenticator { + nonce := crypto.GenerateInitialAEADNonce() + subkey := make([]byte, c.KeyBytes) + hkdfSHA1(key, iv, subkey) + return &crypto.AEADAuthenticator{ + AEAD: c.AEADAuthCreator(subkey), + NonceGenerator: nonce, + } +} + +func (c *AEADCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + auth := c.createAuthenticator(key, iv) + return crypto.NewAuthenticationWriter(auth, &crypto.AEADChunkSizeParser{ + Auth: auth, + }, writer, protocol.TransferTypeStream, nil), nil +} + +func (c *AEADCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + auth := c.createAuthenticator(key, iv) + return crypto.NewAuthenticationReader(auth, &crypto.AEADChunkSizeParser{ + Auth: auth, + }, reader, protocol.TransferTypeStream, nil), nil +} + +func (c *AEADCipher) EncodePacket(key []byte, b *buf.Buffer) error { + ivLen := c.IVSize() + payloadLen := b.Len() + auth := c.createAuthenticator(key, b.BytesTo(ivLen)) + + b.Extend(int32(auth.Overhead())) + _, err := auth.Seal(b.BytesTo(ivLen), b.BytesRange(ivLen, payloadLen)) + return err +} + +func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error { + if b.Len() <= c.IVSize() { + return newError("insufficient data: ", b.Len()) + } + ivLen := c.IVSize() + payloadLen := b.Len() + auth := c.createAuthenticator(key, b.BytesTo(ivLen)) + + bbb, err := auth.Open(b.BytesTo(ivLen), b.BytesRange(ivLen, payloadLen)) + if err != nil { + return err + } + b.Resize(ivLen, int32(len(bbb))) + return nil +} + +type ChaCha20 struct { + IVBytes int32 +} + +func (*ChaCha20) IsAEAD() bool { + return false +} + +func (v *ChaCha20) KeySize() int32 { + return 32 +} + +func (v *ChaCha20) IVSize() int32 { + return v.IVBytes +} + +func (v *ChaCha20) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + stream := crypto.NewChaCha20Stream(key, iv) + return &buf.SequentialWriter{Writer: crypto.NewCryptionWriter(stream, writer)}, nil +} + +func (v *ChaCha20) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + stream := crypto.NewChaCha20Stream(key, iv) + return &buf.SingleReader{Reader: crypto.NewCryptionReader(stream, reader)}, nil +} + +func (v *ChaCha20) EncodePacket(key []byte, b *buf.Buffer) error { + iv := b.BytesTo(v.IVSize()) + stream := crypto.NewChaCha20Stream(key, iv) + stream.XORKeyStream(b.BytesFrom(v.IVSize()), b.BytesFrom(v.IVSize())) + return nil +} + +func (v *ChaCha20) DecodePacket(key []byte, b *buf.Buffer) error { + if b.Len() <= v.IVSize() { + return newError("insufficient data: ", b.Len()) + } + iv := b.BytesTo(v.IVSize()) + stream := crypto.NewChaCha20Stream(key, iv) + stream.XORKeyStream(b.BytesFrom(v.IVSize()), b.BytesFrom(v.IVSize())) + b.Advance(v.IVSize()) + return nil +} + +type NoneCipher struct{} + +func (NoneCipher) KeySize() int32 { return 0 } +func (NoneCipher) IVSize() int32 { return 0 } +func (NoneCipher) IsAEAD() bool { + return true // to avoid OTA +} + +func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + return buf.NewReader(reader), nil +} + +func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + return buf.NewWriter(writer), nil +} + +func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error { + return nil +} + +func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error { + return nil +} + +func passwordToCipherKey(password []byte, keySize int32) []byte { + key := make([]byte, 0, keySize) + + md5Sum := md5.Sum(password) + key = append(key, md5Sum[:]...) + + for int32(len(key)) < keySize { + md5Hash := md5.New() + common.Must2(md5Hash.Write(md5Sum[:])) + common.Must2(md5Hash.Write(password)) + md5Hash.Sum(md5Sum[:0]) + + key = append(key, md5Sum[:]...) + } + return key +} + +func hkdfSHA1(secret, salt, outkey []byte) { + r := hkdf.New(sha1.New, secret, salt, []byte("ss-subkey")) + common.Must2(io.ReadFull(r, outkey)) +} diff --git a/proxy/shadowsocks/config.pb.go b/proxy/shadowsocks/config.pb.go new file mode 100644 index 00000000..b32f92bc --- /dev/null +++ b/proxy/shadowsocks/config.pb.go @@ -0,0 +1,417 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/shadowsocks/config.proto + +package shadowsocks + +import ( + proto "github.com/golang/protobuf/proto" + net "github.com/xtls/xray-core/v1/common/net" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 CipherType int32 + +const ( + CipherType_UNKNOWN CipherType = 0 + CipherType_AES_128_CFB CipherType = 1 + CipherType_AES_256_CFB CipherType = 2 + CipherType_CHACHA20 CipherType = 3 + CipherType_CHACHA20_IETF CipherType = 4 + CipherType_AES_128_GCM CipherType = 5 + CipherType_AES_256_GCM CipherType = 6 + CipherType_CHACHA20_POLY1305 CipherType = 7 + CipherType_NONE CipherType = 8 +) + +// Enum value maps for CipherType. +var ( + CipherType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "AES_128_CFB", + 2: "AES_256_CFB", + 3: "CHACHA20", + 4: "CHACHA20_IETF", + 5: "AES_128_GCM", + 6: "AES_256_GCM", + 7: "CHACHA20_POLY1305", + 8: "NONE", + } + CipherType_value = map[string]int32{ + "UNKNOWN": 0, + "AES_128_CFB": 1, + "AES_256_CFB": 2, + "CHACHA20": 3, + "CHACHA20_IETF": 4, + "AES_128_GCM": 5, + "AES_256_GCM": 6, + "CHACHA20_POLY1305": 7, + "NONE": 8, + } +) + +func (x CipherType) Enum() *CipherType { + p := new(CipherType) + *p = x + return p +} + +func (x CipherType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CipherType) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_shadowsocks_config_proto_enumTypes[0].Descriptor() +} + +func (CipherType) Type() protoreflect.EnumType { + return &file_proxy_shadowsocks_config_proto_enumTypes[0] +} + +func (x CipherType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CipherType.Descriptor instead. +func (CipherType) EnumDescriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{0} +} + +type Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + CipherType CipherType `protobuf:"varint,2,opt,name=cipher_type,json=cipherType,proto3,enum=xray.proxy.shadowsocks.CipherType" json:"cipher_type,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Account) GetCipherType() CipherType { + if x != nil { + return x.CipherType + } + return CipherType_UNKNOWN +} + +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UdpEnabled specified whether or not to enable UDP for Shadowsocks. + // Deprecated. Use 'network' field. + // + // Deprecated: Do not use. + UdpEnabled bool `protobuf:"varint,1,opt,name=udp_enabled,json=udpEnabled,proto3" json:"udp_enabled,omitempty"` + User *protocol.User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` + Network []net.Network `protobuf:"varint,3,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{1} +} + +// Deprecated: Do not use. +func (x *ServerConfig) GetUdpEnabled() bool { + if x != nil { + return x.UdpEnabled + } + return false +} + +func (x *ServerConfig) GetUser() *protocol.User { + if x != nil { + return x.User + } + return nil +} + +func (x *ServerConfig) GetNetwork() []net.Network { + if x != nil { + return x.Network + } + return nil +} + +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_shadowsocks_config_proto protoreflect.FileDescriptor + +var file_proxy_shadowsocks_config_proto_rawDesc = []byte{ + 0x0a, 0x1e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, + 0x63, 0x6b, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x16, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, + 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, 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, 0x1a, 0x1a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x21, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x6a, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x43, 0x0a, 0x0b, 0x63, 0x69, 0x70, 0x68, + 0x65, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, + 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x22, 0x97, 0x01, + 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, + 0x0a, 0x0b, 0x75, 0x64, 0x70, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x75, 0x64, 0x70, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x03, + 0x20, 0x03, 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, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x2a, 0x9f, 0x01, 0x0a, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x43, 0x46, 0x42, + 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x43, 0x46, + 0x42, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x10, + 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x5f, 0x49, 0x45, + 0x54, 0x46, 0x10, 0x04, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, + 0x47, 0x43, 0x4d, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, + 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x06, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, + 0x32, 0x30, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x31, 0x33, 0x30, 0x35, 0x10, 0x07, 0x12, 0x08, 0x0a, + 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x08, 0x42, 0x67, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, + 0x73, 0x6f, 0x63, 0x6b, 0x73, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, + 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0xaa, 0x02, 0x16, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_shadowsocks_config_proto_rawDescOnce sync.Once + file_proxy_shadowsocks_config_proto_rawDescData = file_proxy_shadowsocks_config_proto_rawDesc +) + +func file_proxy_shadowsocks_config_proto_rawDescGZIP() []byte { + file_proxy_shadowsocks_config_proto_rawDescOnce.Do(func() { + file_proxy_shadowsocks_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_shadowsocks_config_proto_rawDescData) + }) + return file_proxy_shadowsocks_config_proto_rawDescData +} + +var file_proxy_shadowsocks_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_shadowsocks_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_shadowsocks_config_proto_goTypes = []interface{}{ + (CipherType)(0), // 0: xray.proxy.shadowsocks.CipherType + (*Account)(nil), // 1: xray.proxy.shadowsocks.Account + (*ServerConfig)(nil), // 2: xray.proxy.shadowsocks.ServerConfig + (*ClientConfig)(nil), // 3: xray.proxy.shadowsocks.ClientConfig + (*protocol.User)(nil), // 4: xray.common.protocol.User + (net.Network)(0), // 5: xray.common.net.Network + (*protocol.ServerEndpoint)(nil), // 6: xray.common.protocol.ServerEndpoint +} +var file_proxy_shadowsocks_config_proto_depIdxs = []int32{ + 0, // 0: xray.proxy.shadowsocks.Account.cipher_type:type_name -> xray.proxy.shadowsocks.CipherType + 4, // 1: xray.proxy.shadowsocks.ServerConfig.user:type_name -> xray.common.protocol.User + 5, // 2: xray.proxy.shadowsocks.ServerConfig.network:type_name -> xray.common.net.Network + 6, // 3: xray.proxy.shadowsocks.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proxy_shadowsocks_config_proto_init() } +func file_proxy_shadowsocks_config_proto_init() { + if File_proxy_shadowsocks_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_shadowsocks_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_shadowsocks_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_shadowsocks_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); 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_proxy_shadowsocks_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_shadowsocks_config_proto_goTypes, + DependencyIndexes: file_proxy_shadowsocks_config_proto_depIdxs, + EnumInfos: file_proxy_shadowsocks_config_proto_enumTypes, + MessageInfos: file_proxy_shadowsocks_config_proto_msgTypes, + }.Build() + File_proxy_shadowsocks_config_proto = out.File + file_proxy_shadowsocks_config_proto_rawDesc = nil + file_proxy_shadowsocks_config_proto_goTypes = nil + file_proxy_shadowsocks_config_proto_depIdxs = nil +} diff --git a/proxy/shadowsocks/config.proto b/proxy/shadowsocks/config.proto new file mode 100644 index 00000000..baab7c02 --- /dev/null +++ b/proxy/shadowsocks/config.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package xray.proxy.shadowsocks; +option csharp_namespace = "Xray.Proxy.Shadowsocks"; +option go_package = "github.com/xtls/xray-core/v1/proxy/shadowsocks"; +option java_package = "com.xray.proxy.shadowsocks"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/protocol/user.proto"; +import "common/protocol/server_spec.proto"; + +message Account { + string password = 1; + CipherType cipher_type = 2; +} + +enum CipherType { + UNKNOWN = 0; + AES_128_CFB = 1; + AES_256_CFB = 2; + CHACHA20 = 3; + CHACHA20_IETF = 4; + AES_128_GCM = 5; + AES_256_GCM = 6; + CHACHA20_POLY1305 = 7; + NONE = 8; +} + +message ServerConfig { + // UdpEnabled specified whether or not to enable UDP for Shadowsocks. + // Deprecated. Use 'network' field. + bool udp_enabled = 1 [deprecated = true]; + xray.common.protocol.User user = 2; + repeated xray.common.net.Network network = 3; +} + +message ClientConfig { + repeated xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/proxy/shadowsocks/config_test.go b/proxy/shadowsocks/config_test.go new file mode 100644 index 00000000..49d99d05 --- /dev/null +++ b/proxy/shadowsocks/config_test.go @@ -0,0 +1,39 @@ +package shadowsocks_test + +import ( + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/proxy/shadowsocks" +) + +func TestAEADCipherUDP(t *testing.T) { + rawAccount := &shadowsocks.Account{ + CipherType: shadowsocks.CipherType_AES_128_GCM, + Password: "test", + } + account, err := rawAccount.AsAccount() + common.Must(err) + + cipher := account.(*shadowsocks.MemoryAccount).Cipher + + key := make([]byte, cipher.KeySize()) + common.Must2(rand.Read(key)) + + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + b1 := buf.New() + common.Must2(b1.ReadFullFrom(rand.Reader, cipher.IVSize())) + common.Must2(b1.Write(payload)) + common.Must(cipher.EncodePacket(key, b1)) + + common.Must(cipher.DecodePacket(key, b1)) + if diff := cmp.Diff(b1.Bytes(), payload); diff != "" { + t.Error(diff) + } +} diff --git a/proxy/shadowsocks/errors.generated.go b/proxy/shadowsocks/errors.generated.go new file mode 100644 index 00000000..8b1b33d9 --- /dev/null +++ b/proxy/shadowsocks/errors.generated.go @@ -0,0 +1,9 @@ +package shadowsocks + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/shadowsocks/protocol.go b/proxy/shadowsocks/protocol.go new file mode 100644 index 00000000..8322b52e --- /dev/null +++ b/proxy/shadowsocks/protocol.go @@ -0,0 +1,257 @@ +// +build !confonly + +package shadowsocks + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "hash/crc32" + "io" + "io/ioutil" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" +) + +const ( + Version = 1 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), + protocol.WithAddressTypeParser(func(b byte) byte { + return b & 0x0F + }), +) + +// ReadTCPSession reads a Shadowsocks TCP session from the given reader, returns its header and remaining parts. +func ReadTCPSession(user *protocol.MemoryUser, reader io.Reader) (*protocol.RequestHeader, buf.Reader, error) { + account := user.Account.(*MemoryAccount) + + hashkdf := hmac.New(sha256.New, []byte("SSBSKDF")) + hashkdf.Write(account.Key) + + behaviorSeed := crc32.ChecksumIEEE(hashkdf.Sum(nil)) + + behaviorRand := dice.NewDeterministicDice(int64(behaviorSeed)) + BaseDrainSize := behaviorRand.Roll(3266) + RandDrainMax := behaviorRand.Roll(64) + 1 + RandDrainRolled := dice.Roll(RandDrainMax) + DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled + readSizeRemain := DrainSize + + buffer := buf.New() + defer buffer.Release() + + ivLen := account.Cipher.IVSize() + var iv []byte + if ivLen > 0 { + if _, err := buffer.ReadFullFrom(reader, ivLen); err != nil { + readSizeRemain -= int(buffer.Len()) + DrainConnN(reader, readSizeRemain) + return nil, nil, newError("failed to read IV").Base(err) + } + + iv = append([]byte(nil), buffer.BytesTo(ivLen)...) + } + + r, err := account.Cipher.NewDecryptionReader(account.Key, iv, reader) + if err != nil { + readSizeRemain -= int(buffer.Len()) + DrainConnN(reader, readSizeRemain) + return nil, nil, newError("failed to initialize decoding stream").Base(err).AtError() + } + br := &buf.BufferedReader{Reader: r} + + request := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandTCP, + } + + readSizeRemain -= int(buffer.Len()) + buffer.Clear() + + addr, port, err := addrParser.ReadAddressPort(buffer, br) + if err != nil { + readSizeRemain -= int(buffer.Len()) + DrainConnN(reader, readSizeRemain) + return nil, nil, newError("failed to read address").Base(err) + } + + request.Address = addr + request.Port = port + + if request.Address == nil { + readSizeRemain -= int(buffer.Len()) + DrainConnN(reader, readSizeRemain) + return nil, nil, newError("invalid remote address.") + } + + return request, br, nil +} + +func DrainConnN(reader io.Reader, n int) error { + _, err := io.CopyN(ioutil.Discard, reader, int64(n)) + return err +} + +// WriteTCPRequest writes Shadowsocks request into the given writer, and returns a writer for body. +func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + common.Must2(rand.Read(iv)) + if err := buf.WriteAllBytes(writer, iv); err != nil { + return nil, newError("failed to write IV") + } + } + + w, err := account.Cipher.NewEncryptionWriter(account.Key, iv, writer) + if err != nil { + return nil, newError("failed to create encoding stream").Base(err).AtError() + } + + header := buf.New() + + if err := addrParser.WriteAddressPort(header, request.Address, request.Port); err != nil { + return nil, newError("failed to write address").Base(err) + } + + if err := w.WriteMultiBuffer(buf.MultiBuffer{header}); err != nil { + return nil, newError("failed to write header").Base(err) + } + + return w, nil +} + +func ReadTCPResponse(user *protocol.MemoryUser, reader io.Reader) (buf.Reader, error) { + account := user.Account.(*MemoryAccount) + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + if _, err := io.ReadFull(reader, iv); err != nil { + return nil, newError("failed to read IV").Base(err) + } + } + + return account.Cipher.NewDecryptionReader(account.Key, iv, reader) +} + +func WriteTCPResponse(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + common.Must2(rand.Read(iv)) + if err := buf.WriteAllBytes(writer, iv); err != nil { + return nil, newError("failed to write IV.").Base(err) + } + } + + return account.Cipher.NewEncryptionWriter(account.Key, iv, writer) +} + +func EncodeUDPPacket(request *protocol.RequestHeader, payload []byte) (*buf.Buffer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + buffer := buf.New() + ivLen := account.Cipher.IVSize() + if ivLen > 0 { + common.Must2(buffer.ReadFullFrom(rand.Reader, ivLen)) + } + + if err := addrParser.WriteAddressPort(buffer, request.Address, request.Port); err != nil { + return nil, newError("failed to write address").Base(err) + } + + buffer.Write(payload) + + if err := account.Cipher.EncodePacket(account.Key, buffer); err != nil { + return nil, newError("failed to encrypt UDP payload").Base(err) + } + + return buffer, nil +} + +func DecodeUDPPacket(user *protocol.MemoryUser, payload *buf.Buffer) (*protocol.RequestHeader, *buf.Buffer, error) { + account := user.Account.(*MemoryAccount) + + var iv []byte + if !account.Cipher.IsAEAD() && account.Cipher.IVSize() > 0 { + // Keep track of IV as it gets removed from payload in DecodePacket. + iv = make([]byte, account.Cipher.IVSize()) + copy(iv, payload.BytesTo(account.Cipher.IVSize())) + } + + if err := account.Cipher.DecodePacket(account.Key, payload); err != nil { + return nil, nil, newError("failed to decrypt UDP payload").Base(err) + } + + request := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandUDP, + } + + payload.SetByte(0, payload.Byte(0)&0x0F) + + addr, port, err := addrParser.ReadAddressPort(nil, payload) + if err != nil { + return nil, nil, newError("failed to parse address").Base(err) + } + + request.Address = addr + request.Port = port + + return request, payload, nil +} + +type UDPReader struct { + Reader io.Reader + User *protocol.MemoryUser +} + +func (v *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + buffer := buf.New() + _, err := buffer.ReadFrom(v.Reader) + if err != nil { + buffer.Release() + return nil, err + } + _, payload, err := DecodeUDPPacket(v.User, buffer) + if err != nil { + buffer.Release() + return nil, err + } + return buf.MultiBuffer{payload}, nil +} + +type UDPWriter struct { + Writer io.Writer + Request *protocol.RequestHeader +} + +// Write implements io.Writer. +func (w *UDPWriter) Write(payload []byte) (int, error) { + packet, err := EncodeUDPPacket(w.Request, payload) + if err != nil { + return 0, err + } + _, err = w.Writer.Write(packet.Bytes()) + packet.Release() + return len(payload), err +} diff --git a/proxy/shadowsocks/protocol_test.go b/proxy/shadowsocks/protocol_test.go new file mode 100644 index 00000000..43fa331b --- /dev/null +++ b/proxy/shadowsocks/protocol_test.go @@ -0,0 +1,186 @@ +package shadowsocks_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + . "github.com/xtls/xray-core/v1/proxy/shadowsocks" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestUDPEncoding(t *testing.T) { + request := &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandUDP, + Address: net.LocalHostIP, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "shadowsocks-password", + CipherType: CipherType_AES_128_CFB, + }), + }, + } + + data := buf.New() + common.Must2(data.WriteString("test string")) + encodedData, err := EncodeUDPPacket(request, data.Bytes()) + common.Must(err) + + decodedRequest, decodedData, err := DecodeUDPPacket(request.User, encodedData) + common.Must(err) + + if r := cmp.Diff(decodedData.Bytes(), data.Bytes()); r != "" { + t.Error("data: ", r) + } + + if r := cmp.Diff(decodedRequest, request); r != "" { + t.Error("request: ", r) + } +} + +func TestTCPRequest(t *testing.T) { + cases := []struct { + request *protocol.RequestHeader + payload []byte + }{ + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.LocalHostIP, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "tcp-password", + CipherType: CipherType_CHACHA20, + }), + }, + }, + payload: []byte("test string"), + }, + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.LocalHostIPv6, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + CipherType: CipherType_AES_256_CFB, + }), + }, + }, + payload: []byte("test string"), + }, + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("example.com"), + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + CipherType: CipherType_CHACHA20_IETF, + }), + }, + }, + payload: []byte("test string"), + }, + } + + runTest := func(request *protocol.RequestHeader, payload []byte) { + data := buf.New() + common.Must2(data.Write(payload)) + + cache := buf.New() + defer cache.Release() + + writer, err := WriteTCPRequest(request, cache) + common.Must(err) + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + decodedRequest, reader, err := ReadTCPSession(request.User, cache) + common.Must(err) + if r := cmp.Diff(decodedRequest, request); r != "" { + t.Error("request: ", r) + } + + decodedData, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedData[0].Bytes(), payload); r != "" { + t.Error("data: ", r) + } + } + + for _, test := range cases { + runTest(test.request, test.payload) + } +} + +func TestUDPReaderWriter(t *testing.T) { + user := &protocol.MemoryUser{ + Account: toAccount(&Account{ + Password: "test-password", + CipherType: CipherType_CHACHA20_IETF, + }), + } + cache := buf.New() + defer cache.Release() + + writer := &buf.SequentialWriter{Writer: &UDPWriter{ + Writer: cache, + Request: &protocol.RequestHeader{ + Version: Version, + Address: net.DomainAddress("example.com"), + Port: 123, + User: user, + }, + }} + + reader := &UDPReader{ + Reader: cache, + User: user, + } + + { + b := buf.New() + common.Must2(b.WriteString("test payload")) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + payload, err := reader.ReadMultiBuffer() + common.Must(err) + if payload[0].String() != "test payload" { + t.Error("unexpected output: ", payload[0].String()) + } + } + + { + b := buf.New() + common.Must2(b.WriteString("test payload 2")) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + payload, err := reader.ReadMultiBuffer() + common.Must(err) + if payload[0].String() != "test payload 2" { + t.Error("unexpected output: ", payload[0].String()) + } + } +} diff --git a/proxy/shadowsocks/server.go b/proxy/shadowsocks/server.go new file mode 100644 index 00000000..b16fcf3e --- /dev/null +++ b/proxy/shadowsocks/server.go @@ -0,0 +1,239 @@ +// +build !confonly + +package shadowsocks + +import ( + "context" + "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" + 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" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/udp" +) + +type Server struct { + config *ServerConfig + user *protocol.MemoryUser + policyManager policy.Manager +} + +// NewServer create a new Shadowsocks server. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + if config.GetUser() == nil { + return nil, newError("user is not specified") + } + + mUser, err := config.User.ToMemoryUser() + if err != nil { + return nil, newError("failed to parse user account").Base(err) + } + + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + user: mUser, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return s, nil +} + +func (s *Server) Network() []net.Network { + list := s.config.Network + if len(list) == 0 { + list = append(list, net.Network_TCP) + } + if s.config.UdpEnabled { + list = append(list, net.Network_UDP) + } + return list +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + switch network { + case net.Network_TCP: + return s.handleConnection(ctx, conn, dispatcher) + case net.Network_UDP: + return s.handlerUDPPayload(ctx, conn, dispatcher) + default: + return newError("unknown network: ", network) + } +} + +func (s *Server) handlerUDPPayload(ctx context.Context, conn internet.Connection, dispatcher routing.Dispatcher) error { + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + request := protocol.RequestHeaderFromContext(ctx) + if request == nil { + return + } + + payload := packet.Payload + data, err := EncodeUDPPacket(request, payload.Bytes()) + payload.Release() + if err != nil { + newError("failed to encode UDP packet").Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + return + } + defer data.Release() + + conn.Write(data.Bytes()) + }) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = s.user + + reader := buf.NewPacketReader(conn) + for { + mpayload, err := reader.ReadMultiBuffer() + if err != nil { + break + } + + for _, payload := range mpayload { + request, data, err := DecodeUDPPacket(s.user, payload) + if err != nil { + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + newError("dropping invalid UDP packet from: ", inbound.Source).Base(err).WriteToLog(session.ExportIDToError(ctx)) + log.Record(&log.AccessMessage{ + From: inbound.Source, + To: "", + Status: log.AccessRejected, + Reason: err, + }) + } + payload.Release() + continue + } + + currentPacketCtx := ctx + dest := request.Destination() + if inbound.Source.IsValid() { + currentPacketCtx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: dest, + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } + newError("tunnelling request to ", dest).WriteToLog(session.ExportIDToError(currentPacketCtx)) + + currentPacketCtx = protocol.ContextWithRequestHeader(currentPacketCtx, request) + udpServer.Dispatch(currentPacketCtx, dest, data) + } + } + + return nil +} + +func (s *Server) handleConnection(ctx context.Context, conn internet.Connection, dispatcher routing.Dispatcher) error { + sessionPolicy := s.policyManager.ForLevel(s.user.Level) + conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)) + + bufferedReader := buf.BufferedReader{Reader: buf.NewReader(conn)} + request, bodyReader, err := ReadTCPSession(s.user, &bufferedReader) + if err != nil { + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + return newError("failed to create request from: ", conn.RemoteAddr()).Base(err) + } + conn.SetReadDeadline(time.Time{}) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = s.user + + dest := request.Destination() + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: dest, + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + newError("tunnelling request to ", dest).WriteToLog(session.ExportIDToError(ctx)) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + responseWriter, err := WriteTCPResponse(request, bufferedWriter) + if err != nil { + return newError("failed to write response").Base(err) + } + + { + payload, err := link.Reader.ReadMultiBuffer() + if err != nil { + return err + } + if err := responseWriter.WriteMultiBuffer(payload); err != nil { + return err + } + } + + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(link.Reader, responseWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all TCP response").Base(err) + } + + return nil + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + if err := buf.Copy(bodyReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all TCP request").Base(err) + } + + return nil + } + + var requestDoneAndCloseWriter = task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/proxy/shadowsocks/shadowsocks.go b/proxy/shadowsocks/shadowsocks.go new file mode 100644 index 00000000..5214a24c --- /dev/null +++ b/proxy/shadowsocks/shadowsocks.go @@ -0,0 +1,8 @@ +// Package shadowsocks provides compatible functionality to Shadowsocks. +// +// Shadowsocks client and server are implemented as outbound and inbound respectively in Xray's term. +// +// R.I.P Shadowsocks +package shadowsocks + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/proxy/socks/client.go b/proxy/socks/client.go new file mode 100644 index 00000000..5266dbd0 --- /dev/null +++ b/proxy/socks/client.go @@ -0,0 +1,154 @@ +// +build !confonly + +package socks + +import ( + "context" + "time" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Client is a Socks5 client. +type Client struct { + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// NewClient create a new Socks5 client based on the given config. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Server { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to get server spec").Base(err) + } + serverList.AddServer(s) + } + if serverList.Size() == 0 { + return nil, newError("0 target server") + } + + v := core.MustFromContext(ctx) + return &Client{ + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + }, nil +} + +// Process implements proxy.Outbound.Process. +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified.") + } + destination := outbound.Target + + var server *protocol.ServerSpec + var conn internet.Connection + + if err := retry.ExponentialBackoff(5, 100).On(func() error { + server = c.serverPicker.PickServer() + dest := server.Destination() + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return err + } + conn = rawConn + + return nil + }); err != nil { + return newError("failed to find an available destination").Base(err) + } + + defer func() { + if err := conn.Close(); err != nil { + newError("failed to closed connection").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + }() + + p := c.policyManager.ForLevel(0) + + request := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandTCP, + Address: destination.Address, + Port: destination.Port, + } + if destination.Network == net.Network_UDP { + request.Command = protocol.RequestCommandUDP + } + + user := server.PickUser() + if user != nil { + request.User = user + p = c.policyManager.ForLevel(user.Level) + } + + if err := conn.SetDeadline(time.Now().Add(p.Timeouts.Handshake)); err != nil { + newError("failed to set deadline for handshake").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + udpRequest, err := ClientHandshake(request, conn, conn) + if err != nil { + return newError("failed to establish connection to server").AtWarning().Base(err) + } + + if err := conn.SetDeadline(time.Time{}); err != nil { + newError("failed to clear deadline after handshake").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, p.Timeouts.ConnectionIdle) + + var requestFunc func() error + var responseFunc func() error + if request.Command == protocol.RequestCommandTCP { + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc = func() error { + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + } else if request.Command == protocol.RequestCommandUDP { + udpConn, err := dialer.Dial(ctx, udpRequest.Destination()) + if err != nil { + return newError("failed to create UDP connection").Base(err) + } + defer udpConn.Close() + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, &buf.SequentialWriter{Writer: NewUDPWriter(request, udpConn)}, buf.UpdateActivity(timer)) + } + responseFunc = func() error { + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + reader := &UDPReader{reader: udpConn} + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + } + + var responseDonePost = task.OnSuccess(responseFunc, task.Close(link.Writer)) + if err := task.Run(ctx, requestFunc, responseDonePost); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/socks/config.go b/proxy/socks/config.go new file mode 100644 index 00000000..17074630 --- /dev/null +++ b/proxy/socks/config.go @@ -0,0 +1,27 @@ +// +build !confonly + +package socks + +import "github.com/xtls/xray-core/v1/common/protocol" + +func (a *Account) Equals(another protocol.Account) bool { + if account, ok := another.(*Account); ok { + return a.Username == account.Username + } + return false +} + +func (a *Account) AsAccount() (protocol.Account, error) { + return a, nil +} + +func (c *ServerConfig) HasAccount(username, password string) bool { + if c.Accounts == nil { + return false + } + storedPassed, found := c.Accounts[username] + if !found { + return false + } + return storedPassed == password +} diff --git a/proxy/socks/config.pb.go b/proxy/socks/config.pb.go new file mode 100644 index 00000000..c6c27150 --- /dev/null +++ b/proxy/socks/config.pb.go @@ -0,0 +1,423 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/socks/config.proto + +package socks + +import ( + proto "github.com/golang/protobuf/proto" + net "github.com/xtls/xray-core/v1/common/net" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 + +// AuthType is the authentication type of Socks proxy. +type AuthType int32 + +const ( + // NO_AUTH is for anounymous authentication. + AuthType_NO_AUTH AuthType = 0 + // PASSWORD is for username/password authentication. + AuthType_PASSWORD AuthType = 1 +) + +// Enum value maps for AuthType. +var ( + AuthType_name = map[int32]string{ + 0: "NO_AUTH", + 1: "PASSWORD", + } + AuthType_value = map[string]int32{ + "NO_AUTH": 0, + "PASSWORD": 1, + } +) + +func (x AuthType) Enum() *AuthType { + p := new(AuthType) + *p = x + return p +} + +func (x AuthType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AuthType) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_socks_config_proto_enumTypes[0].Descriptor() +} + +func (AuthType) Type() protoreflect.EnumType { + return &file_proxy_socks_config_proto_enumTypes[0] +} + +func (x AuthType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AuthType.Descriptor instead. +func (AuthType) EnumDescriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{0} +} + +// Account represents a Socks account. +type Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_socks_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// ServerConfig is the protobuf config for Socks server. +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AuthType AuthType `protobuf:"varint,1,opt,name=auth_type,json=authType,proto3,enum=xray.proxy.socks.AuthType" json:"auth_type,omitempty"` + Accounts map[string]string `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Address *net.IPOrDomain `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + UdpEnabled bool `protobuf:"varint,4,opt,name=udp_enabled,json=udpEnabled,proto3" json:"udp_enabled,omitempty"` + // Deprecated: Do not use. + Timeout uint32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"` + UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_socks_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetAuthType() AuthType { + if x != nil { + return x.AuthType + } + return AuthType_NO_AUTH +} + +func (x *ServerConfig) GetAccounts() map[string]string { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *ServerConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ServerConfig) GetUdpEnabled() bool { + if x != nil { + return x.UdpEnabled + } + return false +} + +// Deprecated: Do not use. +func (x *ServerConfig) GetTimeout() uint32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *ServerConfig) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +// ClientConfig is the protobuf config for Socks client. +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Sever is a list of Socks server addresses. + Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_socks_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_socks_config_proto protoreflect.FileDescriptor + +var file_proxy_socks_config_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0x2f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x6f, 0x63, 0x6b, 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, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, + 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x41, 0x0a, 0x07, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xe3, 0x02, 0x0a, + 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x37, 0x0a, + 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x6f, + 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x61, 0x75, + 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x48, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x35, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 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, 0x07, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x64, 0x70, 0x5f, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x64, + 0x70, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, + 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 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, 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, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2a, 0x25, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, + 0x4e, 0x4f, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x41, 0x53, + 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x01, 0x42, 0x55, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x6f, 0x63, 0x6b, 0x73, 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, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, + 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x53, 0x6f, 0x63, 0x6b, 0x73, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_socks_config_proto_rawDescOnce sync.Once + file_proxy_socks_config_proto_rawDescData = file_proxy_socks_config_proto_rawDesc +) + +func file_proxy_socks_config_proto_rawDescGZIP() []byte { + file_proxy_socks_config_proto_rawDescOnce.Do(func() { + file_proxy_socks_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_socks_config_proto_rawDescData) + }) + return file_proxy_socks_config_proto_rawDescData +} + +var file_proxy_socks_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_socks_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_socks_config_proto_goTypes = []interface{}{ + (AuthType)(0), // 0: xray.proxy.socks.AuthType + (*Account)(nil), // 1: xray.proxy.socks.Account + (*ServerConfig)(nil), // 2: xray.proxy.socks.ServerConfig + (*ClientConfig)(nil), // 3: xray.proxy.socks.ClientConfig + nil, // 4: xray.proxy.socks.ServerConfig.AccountsEntry + (*net.IPOrDomain)(nil), // 5: xray.common.net.IPOrDomain + (*protocol.ServerEndpoint)(nil), // 6: xray.common.protocol.ServerEndpoint +} +var file_proxy_socks_config_proto_depIdxs = []int32{ + 0, // 0: xray.proxy.socks.ServerConfig.auth_type:type_name -> xray.proxy.socks.AuthType + 4, // 1: xray.proxy.socks.ServerConfig.accounts:type_name -> xray.proxy.socks.ServerConfig.AccountsEntry + 5, // 2: xray.proxy.socks.ServerConfig.address:type_name -> xray.common.net.IPOrDomain + 6, // 3: xray.proxy.socks.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proxy_socks_config_proto_init() } +func file_proxy_socks_config_proto_init() { + if File_proxy_socks_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_socks_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_socks_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_socks_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); 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_proxy_socks_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_socks_config_proto_goTypes, + DependencyIndexes: file_proxy_socks_config_proto_depIdxs, + EnumInfos: file_proxy_socks_config_proto_enumTypes, + MessageInfos: file_proxy_socks_config_proto_msgTypes, + }.Build() + File_proxy_socks_config_proto = out.File + file_proxy_socks_config_proto_rawDesc = nil + file_proxy_socks_config_proto_goTypes = nil + file_proxy_socks_config_proto_depIdxs = nil +} diff --git a/proxy/socks/config.proto b/proxy/socks/config.proto new file mode 100644 index 00000000..ff1c1385 --- /dev/null +++ b/proxy/socks/config.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package xray.proxy.socks; +option csharp_namespace = "Xray.Proxy.Socks"; +option go_package = "github.com/xtls/xray-core/v1/proxy/socks"; +option java_package = "com.xray.proxy.socks"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/protocol/server_spec.proto"; + +// Account represents a Socks account. +message Account { + string username = 1; + string password = 2; +} + +// AuthType is the authentication type of Socks proxy. +enum AuthType { + // NO_AUTH is for anounymous authentication. + NO_AUTH = 0; + // PASSWORD is for username/password authentication. + PASSWORD = 1; +} + +// ServerConfig is the protobuf config for Socks server. +message ServerConfig { + AuthType auth_type = 1; + map accounts = 2; + xray.common.net.IPOrDomain address = 3; + bool udp_enabled = 4; + uint32 timeout = 5 [deprecated = true]; + uint32 user_level = 6; +} + +// ClientConfig is the protobuf config for Socks client. +message ClientConfig { + // Sever is a list of Socks server addresses. + repeated xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/proxy/socks/errors.generated.go b/proxy/socks/errors.generated.go new file mode 100644 index 00000000..023bf9e0 --- /dev/null +++ b/proxy/socks/errors.generated.go @@ -0,0 +1,9 @@ +package socks + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go new file mode 100644 index 00000000..20b91dd0 --- /dev/null +++ b/proxy/socks/protocol.go @@ -0,0 +1,490 @@ +// +build !confonly + +package socks + +import ( + "encoding/binary" + "io" + + "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/protocol" +) + +const ( + socks5Version = 0x05 + socks4Version = 0x04 + + cmdTCPConnect = 0x01 + cmdTCPBind = 0x02 + cmdUDPPort = 0x03 + cmdTorResolve = 0xF0 + cmdTorResolvePTR = 0xF1 + + socks4RequestGranted = 90 + socks4RequestRejected = 91 + + authNotRequired = 0x00 + // authGssAPI = 0x01 + authPassword = 0x02 + authNoMatchingMethod = 0xFF + + statusSuccess = 0x00 + statusCmdNotSupport = 0x07 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), +) + +type ServerSession struct { + config *ServerConfig + port net.Port +} + +func (s *ServerSession) handshake4(cmd byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + if s.config.AuthType == AuthType_PASSWORD { + writeSocks4Response(writer, socks4RequestRejected, net.AnyIP, net.Port(0)) + return nil, newError("socks 4 is not allowed when auth is required.") + } + + var port net.Port + var address net.Address + + { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 6); err != nil { + buffer.Release() + return nil, newError("insufficient header").Base(err) + } + port = net.PortFromBytes(buffer.BytesRange(0, 2)) + address = net.IPAddress(buffer.BytesRange(2, 6)) + buffer.Release() + } + + if _, err := ReadUntilNull(reader); /* user id */ err != nil { + return nil, err + } + if address.IP()[0] == 0x00 { + domain, err := ReadUntilNull(reader) + if err != nil { + return nil, newError("failed to read domain for socks 4a").Base(err) + } + address = net.DomainAddress(domain) + } + + switch cmd { + case cmdTCPConnect: + request := &protocol.RequestHeader{ + Command: protocol.RequestCommandTCP, + Address: address, + Port: port, + Version: socks4Version, + } + if err := writeSocks4Response(writer, socks4RequestGranted, net.AnyIP, net.Port(0)); err != nil { + return nil, err + } + return request, nil + default: + writeSocks4Response(writer, socks4RequestRejected, net.AnyIP, net.Port(0)) + return nil, newError("unsupported command: ", cmd) + } +} + +func (s *ServerSession) auth5(nMethod byte, reader io.Reader, writer io.Writer) (username string, err error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err = buffer.ReadFullFrom(reader, int32(nMethod)); err != nil { + return "", newError("failed to read auth methods").Base(err) + } + + var expectedAuth byte = authNotRequired + if s.config.AuthType == AuthType_PASSWORD { + expectedAuth = authPassword + } + + if !hasAuthMethod(expectedAuth, buffer.BytesRange(0, int32(nMethod))) { + writeSocks5AuthenticationResponse(writer, socks5Version, authNoMatchingMethod) + return "", newError("no matching auth method") + } + + if err := writeSocks5AuthenticationResponse(writer, socks5Version, expectedAuth); err != nil { + return "", newError("failed to write auth response").Base(err) + } + + if expectedAuth == authPassword { + username, password, err := ReadUsernamePassword(reader) + if err != nil { + return "", newError("failed to read username and password for authentication").Base(err) + } + + if !s.config.HasAccount(username, password) { + writeSocks5AuthenticationResponse(writer, 0x01, 0xFF) + return "", newError("invalid username or password") + } + + if err := writeSocks5AuthenticationResponse(writer, 0x01, 0x00); err != nil { + return "", newError("failed to write auth response").Base(err) + } + return username, nil + } + + return "", nil +} + +func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + var ( + username string + err error + ) + if username, err = s.auth5(nMethod, reader, writer); err != nil { + return nil, err + } + + var cmd byte + { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 3); err != nil { + buffer.Release() + return nil, newError("failed to read request").Base(err) + } + cmd = buffer.Byte(1) + buffer.Release() + } + + request := new(protocol.RequestHeader) + if username != "" { + request.User = &protocol.MemoryUser{Email: username} + } + switch cmd { + case cmdTCPConnect, cmdTorResolve, cmdTorResolvePTR: + // We don't have a solution for Tor case now. Simply treat it as connect command. + request.Command = protocol.RequestCommandTCP + case cmdUDPPort: + if !s.config.UdpEnabled { + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, newError("UDP is not enabled.") + } + request.Command = protocol.RequestCommandUDP + case cmdTCPBind: + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, newError("TCP bind is not supported.") + default: + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, newError("unknown command ", cmd) + } + + request.Version = socks5Version + + addr, port, err := addrParser.ReadAddressPort(nil, reader) + if err != nil { + return nil, newError("failed to read address").Base(err) + } + request.Address = addr + request.Port = port + + responseAddress := net.AnyIP + responsePort := net.Port(1717) + if request.Command == protocol.RequestCommandUDP { + addr := s.config.Address.AsAddress() + if addr == nil { + addr = net.LocalHostIP + } + responseAddress = addr + responsePort = s.port + } + if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { + return nil, err + } + + return request, nil +} + +// Handshake performs a Socks4/4a/5 handshake. +func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 2); err != nil { + buffer.Release() + return nil, newError("insufficient header").Base(err) + } + + version := buffer.Byte(0) + cmd := buffer.Byte(1) + buffer.Release() + + switch version { + case socks4Version: + return s.handshake4(cmd, reader, writer) + case socks5Version: + return s.handshake5(cmd, reader, writer) + default: + return nil, newError("unknown Socks version: ", version) + } +} + +// ReadUsernamePassword reads Socks 5 username/password message from the given reader. +// +----+------+----------+------+----------+ +// |VER | ULEN | UNAME | PLEN | PASSWD | +// +----+------+----------+------+----------+ +// | 1 | 1 | 1 to 255 | 1 | 1 to 255 | +// +----+------+----------+------+----------+ +func ReadUsernamePassword(reader io.Reader) (string, string, error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(reader, 2); err != nil { + return "", "", err + } + nUsername := int32(buffer.Byte(1)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, nUsername); err != nil { + return "", "", err + } + username := buffer.String() + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return "", "", err + } + nPassword := int32(buffer.Byte(0)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, nPassword); err != nil { + return "", "", err + } + password := buffer.String() + return username, password, nil +} + +// ReadUntilNull reads content from given reader, until a null (0x00) byte. +func ReadUntilNull(reader io.Reader) (string, error) { + b := buf.StackNew() + defer b.Release() + + for { + _, err := b.ReadFullFrom(reader, 1) + if err != nil { + return "", err + } + if b.Byte(b.Len()-1) == 0x00 { + b.Resize(0, b.Len()-1) + return b.String(), nil + } + if b.IsFull() { + return "", newError("buffer overrun") + } + } +} + +func hasAuthMethod(expectedAuth byte, authCandidates []byte) bool { + for _, a := range authCandidates { + if a == expectedAuth { + return true + } + } + return false +} + +func writeSocks5AuthenticationResponse(writer io.Writer, version byte, auth byte) error { + return buf.WriteAllBytes(writer, []byte{version, auth}) +} + +func writeSocks5Response(writer io.Writer, errCode byte, address net.Address, port net.Port) error { + buffer := buf.New() + defer buffer.Release() + + common.Must2(buffer.Write([]byte{socks5Version, errCode, 0x00 /* reserved */})) + if err := addrParser.WriteAddressPort(buffer, address, port); err != nil { + return err + } + + return buf.WriteAllBytes(writer, buffer.Bytes()) +} + +func writeSocks4Response(writer io.Writer, errCode byte, address net.Address, port net.Port) error { + buffer := buf.StackNew() + defer buffer.Release() + + common.Must(buffer.WriteByte(0x00)) + common.Must(buffer.WriteByte(errCode)) + portBytes := buffer.Extend(2) + binary.BigEndian.PutUint16(portBytes, port.Value()) + common.Must2(buffer.Write(address.IP())) + return buf.WriteAllBytes(writer, buffer.Bytes()) +} + +func DecodeUDPPacket(packet *buf.Buffer) (*protocol.RequestHeader, error) { + if packet.Len() < 5 { + return nil, newError("insufficient length of packet.") + } + request := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandUDP, + } + + // packet[0] and packet[1] are reserved + if packet.Byte(2) != 0 /* fragments */ { + return nil, newError("discarding fragmented payload.") + } + + packet.Advance(3) + + addr, port, err := addrParser.ReadAddressPort(nil, packet) + if err != nil { + return nil, newError("failed to read UDP header").Base(err) + } + request.Address = addr + request.Port = port + return request, nil +} + +func EncodeUDPPacket(request *protocol.RequestHeader, data []byte) (*buf.Buffer, error) { + b := buf.New() + common.Must2(b.Write([]byte{0, 0, 0 /* Fragment */})) + if err := addrParser.WriteAddressPort(b, request.Address, request.Port); err != nil { + b.Release() + return nil, err + } + common.Must2(b.Write(data)) + return b, nil +} + +type UDPReader struct { + reader io.Reader +} + +func NewUDPReader(reader io.Reader) *UDPReader { + return &UDPReader{reader: reader} +} + +func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + if _, err := b.ReadFrom(r.reader); err != nil { + return nil, err + } + if _, err := DecodeUDPPacket(b); err != nil { + return nil, err + } + return buf.MultiBuffer{b}, nil +} + +type UDPWriter struct { + request *protocol.RequestHeader + writer io.Writer +} + +func NewUDPWriter(request *protocol.RequestHeader, writer io.Writer) *UDPWriter { + return &UDPWriter{ + request: request, + writer: writer, + } +} + +// Write implements io.Writer. +func (w *UDPWriter) Write(b []byte) (int, error) { + eb, err := EncodeUDPPacket(w.request, b) + if err != nil { + return 0, err + } + defer eb.Release() + if _, err := w.writer.Write(eb.Bytes()); err != nil { + return 0, err + } + return len(b), nil +} + +func ClientHandshake(request *protocol.RequestHeader, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + authByte := byte(authNotRequired) + if request.User != nil { + authByte = byte(authPassword) + } + + b := buf.New() + defer b.Release() + + common.Must2(b.Write([]byte{socks5Version, 0x01, authByte})) + if authByte == authPassword { + account := request.User.Account.(*Account) + + common.Must(b.WriteByte(0x01)) + common.Must(b.WriteByte(byte(len(account.Username)))) + common.Must2(b.WriteString(account.Username)) + common.Must(b.WriteByte(byte(len(account.Password)))) + common.Must2(b.WriteString(account.Password)) + } + + if err := buf.WriteAllBytes(writer, b.Bytes()); err != nil { + return nil, err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return nil, err + } + + if b.Byte(0) != socks5Version { + return nil, newError("unexpected server version: ", b.Byte(0)).AtWarning() + } + if b.Byte(1) != authByte { + return nil, newError("auth method not supported.").AtWarning() + } + + if authByte == authPassword { + b.Clear() + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return nil, err + } + if b.Byte(1) != 0x00 { + return nil, newError("server rejects account: ", b.Byte(1)) + } + } + + b.Clear() + + command := byte(cmdTCPConnect) + if request.Command == protocol.RequestCommandUDP { + command = byte(cmdUDPPort) + } + common.Must2(b.Write([]byte{socks5Version, command, 0x00 /* reserved */})) + if err := addrParser.WriteAddressPort(b, request.Address, request.Port); err != nil { + return nil, err + } + + if err := buf.WriteAllBytes(writer, b.Bytes()); err != nil { + return nil, err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 3); err != nil { + return nil, err + } + + resp := b.Byte(1) + if resp != 0x00 { + return nil, newError("server rejects request: ", resp) + } + + b.Clear() + + address, port, err := addrParser.ReadAddressPort(b, reader) + if err != nil { + return nil, err + } + + if request.Command == protocol.RequestCommandUDP { + udpRequest := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandUDP, + Address: address, + Port: port, + } + return udpRequest, nil + } + + return nil, nil +} diff --git a/proxy/socks/protocol_test.go b/proxy/socks/protocol_test.go new file mode 100644 index 00000000..861d7a0b --- /dev/null +++ b/proxy/socks/protocol_test.go @@ -0,0 +1,124 @@ +package socks_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + . "github.com/xtls/xray-core/v1/proxy/socks" +) + +func TestUDPEncoding(t *testing.T) { + b := buf.New() + + request := &protocol.RequestHeader{ + Address: net.IPAddress([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}), + Port: 1024, + } + writer := &buf.SequentialWriter{Writer: NewUDPWriter(request, b)} + + content := []byte{'a'} + payload := buf.New() + payload.Write(content) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{payload})) + + reader := NewUDPReader(b) + + decodedPayload, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedPayload[0].Bytes(), content); r != "" { + t.Error(r) + } +} + +func TestReadUsernamePassword(t *testing.T) { + testCases := []struct { + Input []byte + Username string + Password string + Error bool + }{ + { + Input: []byte{0x05, 0x01, 'a', 0x02, 'b', 'c'}, + Username: "a", + Password: "bc", + }, + { + Input: []byte{0x05, 0x18, 'a', 0x02, 'b', 'c'}, + Error: true, + }, + } + + for _, testCase := range testCases { + reader := bytes.NewReader(testCase.Input) + username, password, err := ReadUsernamePassword(reader) + if testCase.Error { + if err == nil { + t.Error("for input: ", testCase.Input, " expect error, but actually nil") + } + } else { + if err != nil { + t.Error("for input: ", testCase.Input, " expect no error, but actually ", err.Error()) + } + if testCase.Username != username { + t.Error("for input: ", testCase.Input, " expect username ", testCase.Username, " but actually ", username) + } + if testCase.Password != password { + t.Error("for input: ", testCase.Input, " expect passowrd ", testCase.Password, " but actually ", password) + } + } + } +} + +func TestReadUntilNull(t *testing.T) { + testCases := []struct { + Input []byte + Output string + Error bool + }{ + { + Input: []byte{'a', 'b', 0x00}, + Output: "ab", + }, + { + Input: []byte{'a'}, + Error: true, + }, + } + + for _, testCase := range testCases { + reader := bytes.NewReader(testCase.Input) + value, err := ReadUntilNull(reader) + if testCase.Error { + if err == nil { + t.Error("for input: ", testCase.Input, " expect error, but actually nil") + } + } else { + if err != nil { + t.Error("for input: ", testCase.Input, " expect no error, but actually ", err.Error()) + } + if testCase.Output != value { + t.Error("for input: ", testCase.Input, " expect output ", testCase.Output, " but actually ", value) + } + } + } +} + +func BenchmarkReadUsernamePassword(b *testing.B) { + input := []byte{0x05, 0x01, 'a', 0x02, 'b', 'c'} + buffer := buf.New() + buffer.Write(input) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := ReadUsernamePassword(buffer) + common.Must(err) + buffer.Clear() + buffer.Extend(int32(len(input))) + } +} diff --git a/proxy/socks/server.go b/proxy/socks/server.go new file mode 100644 index 00000000..8633146e --- /dev/null +++ b/proxy/socks/server.go @@ -0,0 +1,253 @@ +// +build !confonly + +package socks + +import ( + "context" + "io" + "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" + 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" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/udp" +) + +// Server is a SOCKS 5 proxy server +type Server struct { + config *ServerConfig + policyManager policy.Manager +} + +// NewServer creates a new Server object. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return s, nil +} + +func (s *Server) policy() policy.Session { + config := s.config + p := s.policyManager.ForLevel(config.UserLevel) + if config.Timeout > 0 { + features.PrintDeprecatedFeatureWarning("Socks timeout") + } + if config.Timeout > 0 && config.UserLevel == 0 { + p.Timeouts.ConnectionIdle = time.Duration(config.Timeout) * time.Second + } + return p +} + +// Network implements proxy.Inbound. +func (s *Server) Network() []net.Network { + list := []net.Network{net.Network_TCP} + if s.config.UdpEnabled { + list = append(list, net.Network_UDP) + } + return list +} + +// Process implements proxy.Inbound. +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + if inbound := session.InboundFromContext(ctx); inbound != nil { + inbound.User = &protocol.MemoryUser{ + Level: s.config.UserLevel, + } + } + + switch network { + case net.Network_TCP: + return s.processTCP(ctx, conn, dispatcher) + case net.Network_UDP: + return s.handleUDPPayload(ctx, conn, dispatcher) + default: + return newError("unknown network: ", network) + } +} + +func (s *Server) processTCP(ctx context.Context, conn internet.Connection, dispatcher routing.Dispatcher) error { + plcy := s.policy() + if err := conn.SetReadDeadline(time.Now().Add(plcy.Timeouts.Handshake)); err != nil { + newError("failed to set deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + inbound := session.InboundFromContext(ctx) + if inbound == nil || !inbound.Gateway.IsValid() { + return newError("inbound gateway not specified") + } + + svrSession := &ServerSession{ + config: s.config, + port: inbound.Gateway.Port, + } + + reader := &buf.BufferedReader{Reader: buf.NewReader(conn)} + request, err := svrSession.Handshake(reader, conn) + if err != nil { + if inbound != nil && inbound.Source.IsValid() { + log.Record(&log.AccessMessage{ + From: inbound.Source, + To: "", + Status: log.AccessRejected, + Reason: err, + }) + } + return newError("failed to read request").Base(err) + } + if request.User != nil { + inbound.User.Email = request.User.Email + } + + if err := conn.SetReadDeadline(time.Time{}); err != nil { + newError("failed to clear deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + if request.Command == protocol.RequestCommandTCP { + dest := request.Destination() + newError("TCP Connect request to ", dest).WriteToLog(session.ExportIDToError(ctx)) + if inbound != nil && inbound.Source.IsValid() { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: dest, + Status: log.AccessAccepted, + Reason: "", + }) + } + + return s.transport(ctx, reader, conn, dest, dispatcher) + } + + if request.Command == protocol.RequestCommandUDP { + return s.handleUDP(conn) + } + + return nil +} + +func (*Server) handleUDP(c io.Reader) error { + // The TCP connection closes after this method returns. We need to wait until + // the client closes it. + return common.Error2(io.Copy(buf.DiscardBytes, c)) +} + +func (s *Server) transport(ctx context.Context, reader io.Reader, writer io.Writer, dest net.Destination, dispatcher routing.Dispatcher) error { + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, s.policy().Timeouts.ConnectionIdle) + + plcy := s.policy() + ctx = policy.ContextWithBufferPolicy(ctx, plcy.Buffer) + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + requestDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + if err := buf.Copy(buf.NewReader(reader), link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all TCP request").Base(err) + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) + + v2writer := buf.NewWriter(writer) + if err := buf.Copy(link.Reader, v2writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transport all TCP response").Base(err) + } + + return nil + } + + var requestDonePost = task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} + +func (s *Server) handleUDPPayload(ctx context.Context, conn internet.Connection, dispatcher routing.Dispatcher) error { + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + payload := packet.Payload + newError("writing back UDP response with ", payload.Len(), " bytes").AtDebug().WriteToLog(session.ExportIDToError(ctx)) + + request := protocol.RequestHeaderFromContext(ctx) + if request == nil { + return + } + udpMessage, err := EncodeUDPPacket(request, payload.Bytes()) + payload.Release() + + defer udpMessage.Release() + if err != nil { + newError("failed to write UDP response").AtWarning().Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + conn.Write(udpMessage.Bytes()) + }) + + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + newError("client UDP connection from ", inbound.Source).WriteToLog(session.ExportIDToError(ctx)) + } + + reader := buf.NewPacketReader(conn) + for { + mpayload, err := reader.ReadMultiBuffer() + if err != nil { + return err + } + + for _, payload := range mpayload { + request, err := DecodeUDPPacket(payload) + + if err != nil { + newError("failed to parse UDP request").Base(err).WriteToLog(session.ExportIDToError(ctx)) + payload.Release() + continue + } + + if payload.IsEmpty() { + payload.Release() + continue + } + currentPacketCtx := ctx + newError("send packet to ", request.Destination(), " with ", payload.Len(), " bytes").AtDebug().WriteToLog(session.ExportIDToError(ctx)) + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + currentPacketCtx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: request.Destination(), + Status: log.AccessAccepted, + Reason: "", + }) + } + + currentPacketCtx = protocol.ContextWithRequestHeader(currentPacketCtx, request) + udpServer.Dispatch(currentPacketCtx, request.Destination(), payload) + } + } +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/proxy/socks/socks.go b/proxy/socks/socks.go new file mode 100644 index 00000000..f268b02e --- /dev/null +++ b/proxy/socks/socks.go @@ -0,0 +1,4 @@ +// Package socks provides implements of Socks protocol 4, 4a and 5. +package socks + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/proxy/trojan/client.go b/proxy/trojan/client.go new file mode 100644 index 00000000..f7f2b0e5 --- /dev/null +++ b/proxy/trojan/client.go @@ -0,0 +1,212 @@ +// +build !confonly + +package trojan + +import ( + "context" + "syscall" + "time" + + "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/platform" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + core "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/transport" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +// Client is a inbound handler for trojan protocol +type Client struct { + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// NewClient create a new trojan client. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Server { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to parse server spec").Base(err) + } + serverList.AddServer(s) + } + if serverList.Size() == 0 { + return nil, newError("0 server") + } + + v := core.MustFromContext(ctx) + client := &Client{ + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +// Process implements OutboundHandler.Process(). +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified") + } + destination := outbound.Target + network := destination.Network + + var server *protocol.ServerSpec + var conn internet.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + server = c.serverPicker.PickServer() + rawConn, err := dialer.Dial(ctx, server.Destination()) + if err != nil { + return err + } + + conn = rawConn + return nil + }) + if err != nil { + return newError("failed to find an available destination").AtWarning().Base(err) + } + newError("tunneling request to ", destination, " via ", server.Destination()).WriteToLog(session.ExportIDToError(ctx)) + + defer conn.Close() + + user := server.PickUser() + account, ok := user.Account.(*MemoryAccount) + if !ok { + return newError("user account is not valid") + } + + iConn := conn + statConn, ok := iConn.(*internet.StatCouterConnection) + if ok { + iConn = statConn.Connection + } + + var rawConn syscall.RawConn + + connWriter := &ConnWriter{} + allowUDP443 := false + switch account.Flow { + case XRO + "-udp443", XRD + "-udp443": + allowUDP443 = true + account.Flow = account.Flow[:16] + fallthrough + case XRO, XRD: + if destination.Address.Family().IsDomain() && destination.Address.Domain() == muxCoolAddress { + return newError(account.Flow + " doesn't support Mux").AtWarning() + } + if destination.Network == net.Network_UDP { + if !allowUDP443 && destination.Port == 443 { + return newError(account.Flow + " stopped UDP/443").AtInfo() + } + } else { // enable XTLS only if making TCP request + if xtlsConn, ok := iConn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = trojanXTLSShow + connWriter.Flow = account.Flow + if account.Flow == XRD { + xtlsConn.DirectMode = true + } + if sc, ok := xtlsConn.Connection.(syscall.Conn); ok { + rawConn, _ = sc.SyscallConn() + } + } else { + return newError(`failed to use ` + account.Flow + `, maybe "security" is not "xtls"`).AtWarning() + } + } + case "": + if _, ok := iConn.(*xtls.Conn); ok { + panic(`To avoid misunderstanding, you must fill in Trojan "flow" when using XTLS.`) + } + default: + return newError("unsupported flow " + account.Flow).AtWarning() + } + + sessionPolicy := c.policyManager.ForLevel(user.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + var bodyWriter buf.Writer + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + connWriter.Writer = bufferWriter + connWriter.Target = destination + connWriter.Account = account + + if destination.Network == net.Network_UDP { + bodyWriter = &PacketWriter{Writer: connWriter, Target: destination} + } else { + bodyWriter = connWriter + } + + // write some request payload to buffer + if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return newError("failed to write A request payload").Base(err).AtWarning() + } + + // Flush; bufferWriter.WriteMultiBufer now is bufferWriter.writer.WriteMultiBuffer + if err = bufferWriter.SetBuffered(false); err != nil { + return newError("failed to flush payload").Base(err).AtWarning() + } + + if err = buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer request payload").Base(err).AtInfo() + } + + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + var reader buf.Reader + if network == net.Network_UDP { + reader = &PacketReader{ + Reader: conn, + } + } else { + reader = buf.NewReader(conn) + } + if rawConn != nil { + var counter stats.Counter + if statConn != nil { + counter = statConn.ReadCounter + } + return ReadV(reader, link.Writer, timer, iConn.(*xtls.Conn), rawConn, counter) + } + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + + var responseDoneAndCloseWriter = task.OnSuccess(getResponse, task.Close(link.Writer)) + if err := task.Run(ctx, postRequest, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + xtlsShow := platform.NewEnvFlag("xray.trojan.xtls.show").GetValue(func() string { return defaultFlagValue }) + if xtlsShow == "true" { + trojanXTLSShow = true + } +} diff --git a/proxy/trojan/config.go b/proxy/trojan/config.go new file mode 100644 index 00000000..05c4e50e --- /dev/null +++ b/proxy/trojan/config.go @@ -0,0 +1,52 @@ +package trojan + +import ( + "crypto/sha256" + "encoding/hex" + fmt "fmt" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct { + Password string + Key []byte + Flow string +} + +// AsAccount implements protocol.AsAccount. +func (a *Account) AsAccount() (protocol.Account, error) { + password := a.GetPassword() + key := hexSha224(password) + return &MemoryAccount{ + Password: password, + Key: key, + Flow: a.Flow, + }, nil +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return a.Password == account.Password + } + return false +} + +func hexSha224(password string) []byte { + buf := make([]byte, 56) + hash := sha256.New224() + common.Must2(hash.Write([]byte(password))) + hex.Encode(buf, hash.Sum(nil)) + return buf +} + +func hexString(data []byte) string { + str := "" + for _, v := range data { + str += fmt.Sprintf("%02x", v) + } + return str +} diff --git a/proxy/trojan/config.pb.go b/proxy/trojan/config.pb.go new file mode 100644 index 00000000..85b8b7ad --- /dev/null +++ b/proxy/trojan/config.pb.go @@ -0,0 +1,412 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/trojan/config.proto + +package trojan + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_trojan_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Account) GetFlow() string { + if x != nil { + return x.Flow + } + return "" +} + +type Fallback struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Alpn string `protobuf:"bytes,1,opt,name=alpn,proto3" json:"alpn,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Dest string `protobuf:"bytes,4,opt,name=dest,proto3" json:"dest,omitempty"` + Xver uint64 `protobuf:"varint,5,opt,name=xver,proto3" json:"xver,omitempty"` +} + +func (x *Fallback) Reset() { + *x = Fallback{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_trojan_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Fallback) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Fallback) ProtoMessage() {} + +func (x *Fallback) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_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 Fallback.ProtoReflect.Descriptor instead. +func (*Fallback) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Fallback) GetAlpn() string { + if x != nil { + return x.Alpn + } + return "" +} + +func (x *Fallback) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Fallback) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Fallback) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Fallback) GetXver() uint64 { + if x != nil { + return x.Xver + } + return 0 +} + +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_trojan_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + Fallbacks []*Fallback `protobuf:"bytes,3,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_trojan_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ServerConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +func (x *ServerConfig) GetFallbacks() []*Fallback { + if x != nil { + return x.Fallbacks + } + return nil +} + +var File_proxy_trojan_config_proto protoreflect.FileDescriptor + +var file_proxy_trojan_config_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x1a, 0x1a, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x39, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x6e, 0x0a, 0x08, 0x46, 0x61, 0x6c, 0x6c, + 0x62, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x6c, 0x70, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x61, 0x6c, 0x70, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x7b, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x39, 0x0a, 0x09, 0x66, 0x61, 0x6c, 0x6c, + 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, + 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x73, 0x42, 0x58, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x50, 0x01, 0x5a, 0x29, + 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, 0x70, 0x72, 0x6f, + 0x78, 0x79, 0x2f, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x54, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_trojan_config_proto_rawDescOnce sync.Once + file_proxy_trojan_config_proto_rawDescData = file_proxy_trojan_config_proto_rawDesc +) + +func file_proxy_trojan_config_proto_rawDescGZIP() []byte { + file_proxy_trojan_config_proto_rawDescOnce.Do(func() { + file_proxy_trojan_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_trojan_config_proto_rawDescData) + }) + return file_proxy_trojan_config_proto_rawDescData +} + +var file_proxy_trojan_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_trojan_config_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: xray.proxy.trojan.Account + (*Fallback)(nil), // 1: xray.proxy.trojan.Fallback + (*ClientConfig)(nil), // 2: xray.proxy.trojan.ClientConfig + (*ServerConfig)(nil), // 3: xray.proxy.trojan.ServerConfig + (*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint + (*protocol.User)(nil), // 5: xray.common.protocol.User +} +var file_proxy_trojan_config_proto_depIdxs = []int32{ + 4, // 0: xray.proxy.trojan.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 5, // 1: xray.proxy.trojan.ServerConfig.users:type_name -> xray.common.protocol.User + 1, // 2: xray.proxy.trojan.ServerConfig.fallbacks:type_name -> xray.proxy.trojan.Fallback + 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_proxy_trojan_config_proto_init() } +func file_proxy_trojan_config_proto_init() { + if File_proxy_trojan_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_trojan_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_trojan_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Fallback); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_trojan_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_trojan_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerConfig); 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_proxy_trojan_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_trojan_config_proto_goTypes, + DependencyIndexes: file_proxy_trojan_config_proto_depIdxs, + MessageInfos: file_proxy_trojan_config_proto_msgTypes, + }.Build() + File_proxy_trojan_config_proto = out.File + file_proxy_trojan_config_proto_rawDesc = nil + file_proxy_trojan_config_proto_goTypes = nil + file_proxy_trojan_config_proto_depIdxs = nil +} diff --git a/proxy/trojan/config.proto b/proxy/trojan/config.proto new file mode 100644 index 00000000..1fb865a8 --- /dev/null +++ b/proxy/trojan/config.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package xray.proxy.trojan; +option csharp_namespace = "Xray.Proxy.Trojan"; +option go_package = "github.com/xtls/xray-core/v1/proxy/trojan"; +option java_package = "com.xray.proxy.trojan"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; +import "common/protocol/server_spec.proto"; + +message Account { + string password = 1; + string flow = 2; +} + +message Fallback { + string alpn = 1; + string path = 2; + string type = 3; + string dest = 4; + uint64 xver = 5; +} + +message ClientConfig { + repeated xray.common.protocol.ServerEndpoint server = 1; +} + +message ServerConfig { + repeated xray.common.protocol.User users = 1; + repeated Fallback fallbacks = 3; +} diff --git a/proxy/trojan/errors.generated.go b/proxy/trojan/errors.generated.go new file mode 100644 index 00000000..10d02653 --- /dev/null +++ b/proxy/trojan/errors.generated.go @@ -0,0 +1,9 @@ +package trojan + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/trojan/protocol.go b/proxy/trojan/protocol.go new file mode 100644 index 00000000..ff8e83eb --- /dev/null +++ b/proxy/trojan/protocol.go @@ -0,0 +1,341 @@ +package trojan + +import ( + "encoding/binary" + fmt "fmt" + "io" + "syscall" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/features/stats" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + crlf = []byte{'\r', '\n'} + + addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), + ) + + trojanXTLSShow = false +) + +const ( + maxLength = 8192 + // XRD is constant for XTLS direct mode + XRD = "xtls-rprx-direct" + // XRO is constant for XTLS origin mode + XRO = "xtls-rprx-origin" + + commandTCP byte = 1 + commandUDP byte = 3 + + // for XTLS + commandXRD byte = 0xf0 // XTLS direct mode + commandXRO byte = 0xf1 // XTLS origin mode +) + +// ConnWriter is TCP Connection Writer Wrapper for trojan protocol +type ConnWriter struct { + io.Writer + Target net.Destination + Account *MemoryAccount + Flow string + headerSent bool +} + +// Write implements io.Writer +func (c *ConnWriter) Write(p []byte) (n int, err error) { + if !c.headerSent { + if err := c.writeHeader(); err != nil { + return 0, newError("failed to write request header").Base(err) + } + } + + return c.Writer.Write(p) +} + +// WriteMultiBuffer implements buf.Writer +func (c *ConnWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + for _, b := range mb { + if !b.IsEmpty() { + if _, err := c.Write(b.Bytes()); err != nil { + return err + } + } + } + + return nil +} + +func (c *ConnWriter) writeHeader() error { + buffer := buf.StackNew() + defer buffer.Release() + + command := commandTCP + if c.Target.Network == net.Network_UDP { + command = commandUDP + } else if c.Flow == XRO { + command = commandXRO + } else if c.Flow == XRD { + command = commandXRD + } + + if _, err := buffer.Write(c.Account.Key); err != nil { + return err + } + if _, err := buffer.Write(crlf); err != nil { + return err + } + if err := buffer.WriteByte(command); err != nil { + return err + } + if err := addrParser.WriteAddressPort(&buffer, c.Target.Address, c.Target.Port); err != nil { + return err + } + if _, err := buffer.Write(crlf); err != nil { + return err + } + + _, err := c.Writer.Write(buffer.Bytes()) + if err == nil { + c.headerSent = true + } + + return err +} + +// PacketWriter UDP Connection Writer Wrapper for trojan protocol +type PacketWriter struct { + io.Writer + Target net.Destination +} + +// WriteMultiBuffer implements buf.Writer +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + b := make([]byte, maxLength) + for !mb.IsEmpty() { + var length int + mb, length = buf.SplitBytes(mb, b) + if _, err := w.writePacket(b[:length], w.Target); err != nil { + buf.ReleaseMulti(mb) + return err + } + } + + return nil +} + +// WriteMultiBufferWithMetadata writes udp packet with destination specified +func (w *PacketWriter) WriteMultiBufferWithMetadata(mb buf.MultiBuffer, dest net.Destination) error { + b := make([]byte, maxLength) + for !mb.IsEmpty() { + var length int + mb, length = buf.SplitBytes(mb, b) + if _, err := w.writePacket(b[:length], dest); err != nil { + buf.ReleaseMulti(mb) + return err + } + } + + return nil +} + +func (w *PacketWriter) writePacket(payload []byte, dest net.Destination) (int, error) { + buffer := buf.StackNew() + defer buffer.Release() + + length := len(payload) + lengthBuf := [2]byte{} + binary.BigEndian.PutUint16(lengthBuf[:], uint16(length)) + if err := addrParser.WriteAddressPort(&buffer, dest.Address, dest.Port); err != nil { + return 0, err + } + if _, err := buffer.Write(lengthBuf[:]); err != nil { + return 0, err + } + if _, err := buffer.Write(crlf); err != nil { + return 0, err + } + if _, err := buffer.Write(payload); err != nil { + return 0, err + } + _, err := w.Write(buffer.Bytes()) + if err != nil { + return 0, err + } + + return length, nil +} + +// ConnReader is TCP Connection Reader Wrapper for trojan protocol +type ConnReader struct { + io.Reader + Target net.Destination + Flow string + headerParsed bool +} + +// ParseHeader parses the trojan protocol header +func (c *ConnReader) ParseHeader() error { + var crlf [2]byte + var command [1]byte + var hash [56]byte + if _, err := io.ReadFull(c.Reader, hash[:]); err != nil { + return newError("failed to read user hash").Base(err) + } + + if _, err := io.ReadFull(c.Reader, crlf[:]); err != nil { + return newError("failed to read crlf").Base(err) + } + + if _, err := io.ReadFull(c.Reader, command[:]); err != nil { + return newError("failed to read command").Base(err) + } + + network := net.Network_TCP + if command[0] == commandUDP { + network = net.Network_UDP + } else if command[0] == commandXRO { + c.Flow = XRO + } else if command[0] == commandXRD { + c.Flow = XRD + } + + addr, port, err := addrParser.ReadAddressPort(nil, c.Reader) + if err != nil { + return newError("failed to read address and port").Base(err) + } + c.Target = net.Destination{Network: network, Address: addr, Port: port} + + if _, err := io.ReadFull(c.Reader, crlf[:]); err != nil { + return newError("failed to read crlf").Base(err) + } + + c.headerParsed = true + return nil +} + +// Read implements io.Reader +func (c *ConnReader) Read(p []byte) (int, error) { + if !c.headerParsed { + if err := c.ParseHeader(); err != nil { + return 0, err + } + } + + return c.Reader.Read(p) +} + +// ReadMultiBuffer implements buf.Reader +func (c *ConnReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + _, err := b.ReadFrom(c) + return buf.MultiBuffer{b}, err +} + +// PacketPayload combines udp payload and destination +type PacketPayload struct { + Target net.Destination + Buffer buf.MultiBuffer +} + +// PacketReader is UDP Connection Reader Wrapper for trojan protocol +type PacketReader struct { + io.Reader +} + +// ReadMultiBuffer implements buf.Reader +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + p, err := r.ReadMultiBufferWithMetadata() + if p != nil { + return p.Buffer, err + } + return nil, err +} + +// ReadMultiBufferWithMetadata reads udp packet with destination +func (r *PacketReader) ReadMultiBufferWithMetadata() (*PacketPayload, error) { + addr, port, err := addrParser.ReadAddressPort(nil, r) + if err != nil { + return nil, newError("failed to read address and port").Base(err) + } + + var lengthBuf [2]byte + if _, err := io.ReadFull(r, lengthBuf[:]); err != nil { + return nil, newError("failed to read payload length").Base(err) + } + + remain := int(binary.BigEndian.Uint16(lengthBuf[:])) + if remain > maxLength { + return nil, newError("oversize payload") + } + + var crlf [2]byte + if _, err := io.ReadFull(r, crlf[:]); err != nil { + return nil, newError("failed to read crlf").Base(err) + } + + dest := net.UDPDestination(addr, port) + var mb buf.MultiBuffer + for remain > 0 { + length := buf.Size + if remain < length { + length = remain + } + + b := buf.New() + mb = append(mb, b) + n, err := b.ReadFullFrom(r, int32(length)) + if err != nil { + buf.ReleaseMulti(mb) + return nil, newError("failed to read payload").Base(err) + } + + remain -= int(n) + } + + return &PacketPayload{Target: dest, Buffer: mb}, nil +} + +func ReadV(reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, conn *xtls.Conn, rawConn syscall.RawConn, counter stats.Counter) error { + err := func() error { + var ct stats.Counter + for { + if conn.DirectIn { + conn.DirectIn = false + reader = buf.NewReadVReader(conn.Connection, rawConn) + ct = counter + if conn.SHOW { + fmt.Println(conn.MARK, "ReadV") + } + } + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + if ct != nil { + ct.Add(int64(buffer.Len())) + } + timer.Update() + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return werr + } + } + if err != nil { + return err + } + } + }() + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil +} diff --git a/proxy/trojan/protocol_test.go b/proxy/trojan/protocol_test.go new file mode 100644 index 00000000..da23035b --- /dev/null +++ b/proxy/trojan/protocol_test.go @@ -0,0 +1,91 @@ +package trojan_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "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/protocol" + . "github.com/xtls/xray-core/v1/proxy/trojan" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestTCPRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + }), + } + payload := []byte("test string") + data := buf.New() + common.Must2(data.Write(payload)) + + buffer := buf.New() + defer buffer.Release() + + destination := net.Destination{Network: net.Network_TCP, Address: net.LocalHostIP, Port: 1234} + writer := &ConnWriter{Writer: buffer, Target: destination, Account: user.Account.(*MemoryAccount)} + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + reader := &ConnReader{Reader: buffer} + common.Must(reader.ParseHeader()) + + if r := cmp.Diff(reader.Target, destination); r != "" { + t.Error("destination: ", r) + } + + decodedData, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedData[0].Bytes(), payload); r != "" { + t.Error("data: ", r) + } +} + +func TestUDPRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + }), + } + payload := []byte("test string") + data := buf.New() + common.Must2(data.Write(payload)) + + buffer := buf.New() + defer buffer.Release() + + destination := net.Destination{Network: net.Network_UDP, Address: net.LocalHostIP, Port: 1234} + writer := &PacketWriter{Writer: &ConnWriter{Writer: buffer, Target: destination, Account: user.Account.(*MemoryAccount)}, Target: destination} + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + connReader := &ConnReader{Reader: buffer} + common.Must(connReader.ParseHeader()) + + packetReader := &PacketReader{Reader: connReader} + p, err := packetReader.ReadMultiBufferWithMetadata() + common.Must(err) + + if p.Buffer.IsEmpty() { + t.Error("no request data") + } + + if r := cmp.Diff(p.Target, destination); r != "" { + t.Error("destination: ", r) + } + + mb, decoded := buf.SplitFirst(p.Buffer) + buf.ReleaseMulti(mb) + + if r := cmp.Diff(decoded.Bytes(), payload); r != "" { + t.Error("data: ", r) + } +} diff --git a/proxy/trojan/server.go b/proxy/trojan/server.go new file mode 100644 index 00000000..a0e857b0 --- /dev/null +++ b/proxy/trojan/server.go @@ -0,0 +1,489 @@ +// +build !confonly + +package trojan + +import ( + "context" + "crypto/tls" + "io" + "strconv" + "syscall" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/log" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/platform" + "github.com/xtls/xray-core/v1/common/protocol" + udp_proto "github.com/xtls/xray-core/v1/common/protocol/udp" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/features/stats" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/udp" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + xtlsShow := platform.NewEnvFlag("xray.trojan.xtls.show").GetValue(func() string { return defaultFlagValue }) + if xtlsShow == "true" { + trojanXTLSShow = true + } +} + +// Server is an inbound connection handler that handles messages in trojan protocol. +type Server struct { + policyManager policy.Manager + validator *Validator + fallbacks map[string]map[string]*Fallback // or nil +} + +// NewServer creates a new trojan inbound handler. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + validator := new(Validator) + for _, user := range config.Users { + u, err := user.ToMemoryUser() + if err != nil { + return nil, newError("failed to get trojan user").Base(err).AtError() + } + + if err := validator.Add(u); err != nil { + return nil, newError("failed to add user").Base(err).AtError() + } + } + + v := core.MustFromContext(ctx) + server := &Server{ + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + validator: validator, + } + + if config.Fallbacks != nil { + server.fallbacks = make(map[string]map[string]*Fallback) + for _, fb := range config.Fallbacks { + if server.fallbacks[fb.Alpn] == nil { + server.fallbacks[fb.Alpn] = make(map[string]*Fallback) + } + server.fallbacks[fb.Alpn][fb.Path] = fb + } + if server.fallbacks[""] != nil { + for alpn, pfb := range server.fallbacks { + if alpn != "" { // && alpn != "h2" { + for path, fb := range server.fallbacks[""] { + if pfb[path] == nil { + pfb[path] = fb + } + } + } + } + } + } + + return server, nil +} + +// AddUser implements proxy.UserManager.AddUser(). +func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return s.validator.Add(u) +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (s *Server) RemoveUser(ctx context.Context, e string) error { + return s.validator.Del(e) +} + +// Network implements proxy.Inbound.Network(). +func (s *Server) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +// Process implements proxy.Inbound.Process(). +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + sid := session.ExportIDToError(ctx) + + iConn := conn + statConn, ok := iConn.(*internet.StatCouterConnection) + if ok { + iConn = statConn.Connection + } + + sessionPolicy := s.policyManager.ForLevel(0) + if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + first := buf.New() + defer first.Release() + + firstLen, err := first.ReadFrom(conn) + if err != nil { + return newError("failed to read first request").Base(err) + } + newError("firstLen = ", firstLen).AtInfo().WriteToLog(sid) + + bufferedReader := &buf.BufferedReader{ + Reader: buf.NewReader(conn), + Buffer: buf.MultiBuffer{first}, + } + + var user *protocol.MemoryUser + + apfb := s.fallbacks + isfb := apfb != nil + + shouldFallback := false + if firstLen < 58 || first.Byte(56) != '\r' { + // invalid protocol + err = newError("not trojan protocol") + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + + shouldFallback = true + } else { + user = s.validator.Get(hexString(first.BytesTo(56))) + if user == nil { + // invalid user, let's fallback + err = newError("not a valid user") + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + + shouldFallback = true + } + } + + if isfb && shouldFallback { + return s.fallback(ctx, sid, err, sessionPolicy, conn, iConn, apfb, first, firstLen, bufferedReader) + } else if shouldFallback { + return newError("invalid protocol or invalid user") + } + + clientReader := &ConnReader{Reader: bufferedReader} + if err := clientReader.ParseHeader(); err != nil { + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + return newError("failed to create request from: ", conn.RemoteAddr()).Base(err) + } + + destination := clientReader.Target + if err := conn.SetReadDeadline(time.Time{}); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = user + sessionPolicy = s.policyManager.ForLevel(user.Level) + + if destination.Network == net.Network_UDP { // handle udp request + return s.handleUDPPayload(ctx, &PacketReader{Reader: clientReader}, &PacketWriter{Writer: conn}, dispatcher) + } + + // handle tcp request + account, ok := user.Account.(*MemoryAccount) + if !ok { + return newError("user account is not valid") + } + + var rawConn syscall.RawConn + + switch clientReader.Flow { + case XRO, XRD: + if account.Flow == clientReader.Flow { + if destination.Address.Family().IsDomain() && destination.Address.Domain() == muxCoolAddress { + return newError(clientReader.Flow + " doesn't support Mux").AtWarning() + } + if xtlsConn, ok := iConn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = trojanXTLSShow + if clientReader.Flow == XRD { + xtlsConn.DirectMode = true + if sc, ok := xtlsConn.Connection.(syscall.Conn); ok { + rawConn, _ = sc.SyscallConn() + } + } + } else { + return newError(`failed to use ` + clientReader.Flow + `, maybe "security" is not "xtls"`).AtWarning() + } + } else { + return newError("unable to use ", clientReader.Flow).AtWarning() + } + case "": + default: + return newError("unsupported flow " + account.Flow).AtWarning() + } + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: destination, + Status: log.AccessAccepted, + Reason: "", + Email: user.Email, + }) + + newError("received request for ", destination).WriteToLog(sid) + return s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher, iConn, rawConn, statConn) +} + +func (s *Server) handleUDPPayload(ctx context.Context, clientReader *PacketReader, clientWriter *PacketWriter, dispatcher routing.Dispatcher) error { + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + common.Must(clientWriter.WriteMultiBufferWithMetadata(buf.MultiBuffer{packet.Payload}, packet.Source)) + }) + + inbound := session.InboundFromContext(ctx) + user := inbound.User + + for { + select { + case <-ctx.Done(): + return nil + default: + p, err := clientReader.ReadMultiBufferWithMetadata() + if err != nil { + if errors.Cause(err) != io.EOF { + return newError("unexpected EOF").Base(err) + } + return nil + } + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: p.Target, + Status: log.AccessAccepted, + Reason: "", + Email: user.Email, + }) + newError("tunnelling request to ", p.Target).WriteToLog(session.ExportIDToError(ctx)) + + for _, b := range p.Buffer { + udpServer.Dispatch(ctx, p.Target, b) + } + } + } +} + +func (s *Server) handleConnection(ctx context.Context, sessionPolicy policy.Session, + destination net.Destination, + clientReader buf.Reader, + clientWriter buf.Writer, dispatcher routing.Dispatcher, iConn internet.Connection, rawConn syscall.RawConn, statConn *internet.StatCouterConnection) error { + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return newError("failed to dispatch request to ", destination).Base(err) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + var err error + if rawConn != nil { + var counter stats.Counter + if statConn != nil { + counter = statConn.ReadCounter + } + err = ReadV(clientReader, link.Writer, timer, iConn.(*xtls.Conn), rawConn, counter) + } else { + err = buf.Copy(clientReader, link.Writer, buf.UpdateActivity(timer)) + } + if err != nil { + return newError("failed to transfer request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + if err := buf.Copy(link.Reader, clientWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to write response").Base(err) + } + return nil + } + + var requestDonePost = task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Must(common.Interrupt(link.Reader)) + common.Must(common.Interrupt(link.Writer)) + return newError("connection ends").Base(err) + } + + return nil +} + +func (s *Server) fallback(ctx context.Context, sid errors.ExportOption, err error, sessionPolicy policy.Session, connection internet.Connection, iConn internet.Connection, apfb map[string]map[string]*Fallback, first *buf.Buffer, firstLen int64, reader buf.Reader) error { + if err := connection.SetReadDeadline(time.Time{}); err != nil { + newError("unable to set back read deadline").Base(err).AtWarning().WriteToLog(sid) + } + newError("fallback starts").Base(err).AtInfo().WriteToLog(sid) + + alpn := "" + if len(apfb) > 1 || apfb[""] == nil { + if tlsConn, ok := iConn.(*tls.Conn); ok { + alpn = tlsConn.ConnectionState().NegotiatedProtocol + newError("realAlpn = " + alpn).AtInfo().WriteToLog(sid) + } else if xtlsConn, ok := iConn.(*xtls.Conn); ok { + alpn = xtlsConn.ConnectionState().NegotiatedProtocol + newError("realAlpn = " + alpn).AtInfo().WriteToLog(sid) + } + if apfb[alpn] == nil { + alpn = "" + } + } + pfb := apfb[alpn] + if pfb == nil { + return newError(`failed to find the default "alpn" config`).AtWarning() + } + + path := "" + if len(pfb) > 1 || pfb[""] == nil { + if firstLen >= 18 && first.Byte(4) != '*' { // not h2c + firstBytes := first.Bytes() + for i := 4; i <= 8; i++ { // 5 -> 9 + if firstBytes[i] == '/' && firstBytes[i-1] == ' ' { + search := len(firstBytes) + if search > 64 { + search = 64 // up to about 60 + } + for j := i + 1; j < search; j++ { + k := firstBytes[j] + if k == '\r' || k == '\n' { // avoid logging \r or \n + break + } + if k == ' ' { + path = string(firstBytes[i:j]) + newError("realPath = " + path).AtInfo().WriteToLog(sid) + if pfb[path] == nil { + path = "" + } + break + } + } + break + } + } + } + } + fb := pfb[path] + if fb == nil { + return newError(`failed to find the default "path" config`).AtWarning() + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + var conn net.Conn + if err := retry.ExponentialBackoff(5, 100).On(func() error { + var dialer net.Dialer + conn, err = dialer.DialContext(ctx, fb.Type, fb.Dest) + if err != nil { + return err + } + return nil + }); err != nil { + return newError("failed to dial to " + fb.Dest).Base(err).AtWarning() + } + defer conn.Close() + + serverReader := buf.NewReader(conn) + serverWriter := buf.NewWriter(conn) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + if fb.Xver != 0 { + remoteAddr, remotePort, err := net.SplitHostPort(connection.RemoteAddr().String()) + if err != nil { + return err + } + localAddr, localPort, err := net.SplitHostPort(connection.LocalAddr().String()) + if err != nil { + return err + } + ipv4 := true + for i := 0; i < len(remoteAddr); i++ { + if remoteAddr[i] == ':' { + ipv4 = false + break + } + } + pro := buf.New() + defer pro.Release() + switch fb.Xver { + case 1: + if ipv4 { + common.Must2(pro.Write([]byte("PROXY TCP4 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n"))) + } else { + common.Must2(pro.Write([]byte("PROXY TCP6 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n"))) + } + case 2: + common.Must2(pro.Write([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21"))) // signature + v2 + PROXY + if ipv4 { + common.Must2(pro.Write([]byte("\x11\x00\x0C"))) // AF_INET + STREAM + 12 bytes + common.Must2(pro.Write(net.ParseIP(remoteAddr).To4())) + common.Must2(pro.Write(net.ParseIP(localAddr).To4())) + } else { + common.Must2(pro.Write([]byte("\x21\x00\x24"))) // AF_INET6 + STREAM + 36 bytes + common.Must2(pro.Write(net.ParseIP(remoteAddr).To16())) + common.Must2(pro.Write(net.ParseIP(localAddr).To16())) + } + p1, _ := strconv.ParseUint(remotePort, 10, 16) + p2, _ := strconv.ParseUint(localPort, 10, 16) + common.Must2(pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)})) + } + if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil { + return newError("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning() + } + } + if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to fallback request payload").Base(err).AtInfo() + } + return nil + } + + writer := buf.NewWriter(connection) + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to deliver response payload").Base(err).AtInfo() + } + return nil + } + + if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil { + common.Must(common.Interrupt(serverReader)) + common.Must(common.Interrupt(serverWriter)) + return newError("fallback ends").Base(err).AtInfo() + } + + return nil +} diff --git a/proxy/trojan/trojan.go b/proxy/trojan/trojan.go new file mode 100644 index 00000000..4639b7d9 --- /dev/null +++ b/proxy/trojan/trojan.go @@ -0,0 +1,5 @@ +package trojan + +const ( + muxCoolAddress = "v1.mux.cool" +) diff --git a/proxy/trojan/validator.go b/proxy/trojan/validator.go new file mode 100644 index 00000000..7ea503e1 --- /dev/null +++ b/proxy/trojan/validator.go @@ -0,0 +1,53 @@ +// +build !confonly + +package trojan + +import ( + "strings" + "sync" + + "github.com/xtls/xray-core/v1/common/protocol" +) + +// Validator stores valid trojan users. +type Validator struct { + // Considering email's usage here, map + sync.Mutex/RWMutex may have better performance. + email sync.Map + users sync.Map +} + +// Add a trojan user, Email must be empty or unique. +func (v *Validator) Add(u *protocol.MemoryUser) error { + if u.Email != "" { + _, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u) + if loaded { + return newError("User ", u.Email, " already exists.") + } + } + v.users.Store(hexString(u.Account.(*MemoryAccount).Key), u) + return nil +} + +// Del a trojan user with a non-empty Email. +func (v *Validator) Del(e string) error { + if e == "" { + return newError("Email must not be empty.") + } + le := strings.ToLower(e) + u, _ := v.email.Load(le) + if u == nil { + return newError("User ", e, " not found.") + } + v.email.Delete(le) + v.users.Delete(hexString(u.(*protocol.MemoryUser).Account.(*MemoryAccount).Key)) + return nil +} + +// Get a trojan user with hashed key, nil if user doesn't exist. +func (v *Validator) Get(hash string) *protocol.MemoryUser { + u, _ := v.users.Load(hash) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} diff --git a/proxy/vless/account.go b/proxy/vless/account.go new file mode 100644 index 00000000..03563653 --- /dev/null +++ b/proxy/vless/account.go @@ -0,0 +1,40 @@ +// +build !confonly + +package vless + +import ( + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" +) + +// AsAccount implements protocol.Account.AsAccount(). +func (a *Account) AsAccount() (protocol.Account, error) { + id, err := uuid.ParseString(a.Id) + if err != nil { + return nil, newError("failed to parse ID").Base(err).AtError() + } + return &MemoryAccount{ + ID: protocol.NewID(id), + Flow: a.Flow, // needs parser here? + Encryption: a.Encryption, // needs parser here? + }, nil +} + +// MemoryAccount is an in-memory form of VLess account. +type MemoryAccount struct { + // ID of the account. + ID *protocol.ID + // Flow of the account. May be "xtls-rprx-direct". + Flow string + // Encryption of the account. Used for client connections, and only accepts "none" for now. + Encryption string +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(account protocol.Account) bool { + vlessAccount, ok := account.(*MemoryAccount) + if !ok { + return false + } + return a.ID.Equals(vlessAccount.ID) +} diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go new file mode 100644 index 00000000..ff629e61 --- /dev/null +++ b/proxy/vless/account.pb.go @@ -0,0 +1,174 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vless/account.proto + +package vless + +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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Flow settings. May be "xtls-rprx-direct". + Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` + // Encryption settings. Only applies to client side, and only accepts "none" for now. + Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vless_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_account_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_vless_account_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Account) GetFlow() string { + if x != nil { + return x.Flow + } + return "" +} + +func (x *Account) GetEncryption() string { + if x != nil { + return x.Encryption + } + return "" +} + +var File_proxy_vless_account_proto protoreflect.FileDescriptor + +var file_proxy_vless_account_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x4d, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, 0x0a, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x55, 0x0a, 0x14, + 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x6c, 0x65, 0x73, 0x73, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, + 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, + 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_vless_account_proto_rawDescOnce sync.Once + file_proxy_vless_account_proto_rawDescData = file_proxy_vless_account_proto_rawDesc +) + +func file_proxy_vless_account_proto_rawDescGZIP() []byte { + file_proxy_vless_account_proto_rawDescOnce.Do(func() { + file_proxy_vless_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vless_account_proto_rawDescData) + }) + return file_proxy_vless_account_proto_rawDescData +} + +var file_proxy_vless_account_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vless_account_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: xray.proxy.vless.Account +} +var file_proxy_vless_account_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_proxy_vless_account_proto_init() } +func file_proxy_vless_account_proto_init() { + if File_proxy_vless_account_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vless_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); 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_proxy_vless_account_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_account_proto_goTypes, + DependencyIndexes: file_proxy_vless_account_proto_depIdxs, + MessageInfos: file_proxy_vless_account_proto_msgTypes, + }.Build() + File_proxy_vless_account_proto = out.File + file_proxy_vless_account_proto_rawDesc = nil + file_proxy_vless_account_proto_goTypes = nil + file_proxy_vless_account_proto_depIdxs = nil +} diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto new file mode 100644 index 00000000..298bdfda --- /dev/null +++ b/proxy/vless/account.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.proxy.vless; +option csharp_namespace = "Xray.Proxy.Vless"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vless"; +option java_package = "com.xray.proxy.vless"; +option java_multiple_files = true; + +message Account { + // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". + string id = 1; + // Flow settings. May be "xtls-rprx-direct". + string flow = 2; + // Encryption settings. Only applies to client side, and only accepts "none" for now. + string encryption = 3; +} diff --git a/proxy/vless/encoding/addons.go b/proxy/vless/encoding/addons.go new file mode 100644 index 00000000..c0ee0b02 --- /dev/null +++ b/proxy/vless/encoding/addons.go @@ -0,0 +1,189 @@ +// +build !confonly + +package encoding + +import ( + "io" + + "github.com/golang/protobuf/proto" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/proxy/vless" +) + +func EncodeHeaderAddons(buffer *buf.Buffer, addons *Addons) error { + switch addons.Flow { + case vless.XRO, vless.XRD: + bytes, err := proto.Marshal(addons) + if err != nil { + return newError("failed to marshal addons protobuf value").Base(err) + } + if err := buffer.WriteByte(byte(len(bytes))); err != nil { + return newError("failed to write addons protobuf length").Base(err) + } + if _, err := buffer.Write(bytes); err != nil { + return newError("failed to write addons protobuf value").Base(err) + } + default: + if err := buffer.WriteByte(0); err != nil { + return newError("failed to write addons protobuf length").Base(err) + } + } + + return nil +} + +func DecodeHeaderAddons(buffer *buf.Buffer, reader io.Reader) (*Addons, error) { + addons := new(Addons) + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, newError("failed to read addons protobuf length").Base(err) + } + + if length := int32(buffer.Byte(0)); length != 0 { + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, length); err != nil { + return nil, newError("failed to read addons protobuf value").Base(err) + } + + if err := proto.Unmarshal(buffer.Bytes(), addons); err != nil { + return nil, newError("failed to unmarshal addons protobuf value").Base(err) + } + + // Verification. + switch addons.Flow { + default: + } + } + + return addons, nil +} + +// EncodeBodyAddons returns a Writer that auto-encrypt content written by caller. +func EncodeBodyAddons(writer io.Writer, request *protocol.RequestHeader, addons *Addons) buf.Writer { + switch addons.Flow { + default: + if request.Command == protocol.RequestCommandUDP { + return NewMultiLengthPacketWriter(writer.(buf.Writer)) + } + } + return buf.NewWriter(writer) +} + +// DecodeBodyAddons returns a Reader from which caller can fetch decrypted body. +func DecodeBodyAddons(reader io.Reader, request *protocol.RequestHeader, addons *Addons) buf.Reader { + switch addons.Flow { + default: + if request.Command == protocol.RequestCommandUDP { + return NewLengthPacketReader(reader) + } + } + return buf.NewReader(reader) +} + +func NewMultiLengthPacketWriter(writer buf.Writer) *MultiLengthPacketWriter { + return &MultiLengthPacketWriter{ + Writer: writer, + } +} + +type MultiLengthPacketWriter struct { + buf.Writer +} + +func (w *MultiLengthPacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + mb2Write := make(buf.MultiBuffer, 0, len(mb)+1) + for _, b := range mb { + length := b.Len() + if length == 0 || length+2 > buf.Size { + continue + } + eb := buf.New() + if err := eb.WriteByte(byte(length >> 8)); err != nil { + eb.Release() + continue + } + if err := eb.WriteByte(byte(length)); err != nil { + eb.Release() + continue + } + if _, err := eb.Write(b.Bytes()); err != nil { + eb.Release() + continue + } + mb2Write = append(mb2Write, eb) + } + if mb2Write.IsEmpty() { + return nil + } + return w.Writer.WriteMultiBuffer(mb2Write) +} + +func NewLengthPacketWriter(writer io.Writer) *LengthPacketWriter { + return &LengthPacketWriter{ + Writer: writer, + cache: make([]byte, 0, 65536), + } +} + +type LengthPacketWriter struct { + io.Writer + cache []byte +} + +func (w *LengthPacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + length := mb.Len() // none of mb is nil + // fmt.Println("Write", length) + if length == 0 { + return nil + } + defer func() { + w.cache = w.cache[:0] + }() + w.cache = append(w.cache, byte(length>>8), byte(length)) + for i, b := range mb { + w.cache = append(w.cache, b.Bytes()...) + b.Release() + mb[i] = nil + } + if _, err := w.Write(w.cache); err != nil { + return newError("failed to write a packet").Base(err) + } + return nil +} + +func NewLengthPacketReader(reader io.Reader) *LengthPacketReader { + return &LengthPacketReader{ + Reader: reader, + cache: make([]byte, 2), + } +} + +type LengthPacketReader struct { + io.Reader + cache []byte +} + +func (r *LengthPacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + if _, err := io.ReadFull(r.Reader, r.cache); err != nil { // maybe EOF + return nil, newError("failed to read packet length").Base(err) + } + length := int32(r.cache[0])<<8 | int32(r.cache[1]) + // fmt.Println("Read", length) + mb := make(buf.MultiBuffer, 0, length/buf.Size+1) + for length > 0 { + size := length + if size > buf.Size { + size = buf.Size + } + length -= size + b := buf.New() + if _, err := b.ReadFullFrom(r.Reader, size); err != nil { + return nil, newError("failed to read packet payload").Base(err) + } + mb = append(mb, b) + } + return mb, nil +} diff --git a/proxy/vless/encoding/addons.pb.go b/proxy/vless/encoding/addons.pb.go new file mode 100644 index 00000000..86429d5e --- /dev/null +++ b/proxy/vless/encoding/addons.pb.go @@ -0,0 +1,384 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: proxy/vless/encoding/addons.proto + +package encoding + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Addons struct { + Flow string `protobuf:"bytes,1,opt,name=Flow,proto3" json:"Flow,omitempty"` + Seed []byte `protobuf:"bytes,2,opt,name=Seed,proto3" json:"Seed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Addons) Reset() { *m = Addons{} } +func (m *Addons) String() string { return proto.CompactTextString(m) } +func (*Addons) ProtoMessage() {} +func (*Addons) Descriptor() ([]byte, []int) { + return fileDescriptor_75ab671b0ca8b1cc, []int{0} +} +func (m *Addons) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Addons) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Addons.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Addons) XXX_Merge(src proto.Message) { + xxx_messageInfo_Addons.Merge(m, src) +} +func (m *Addons) XXX_Size() int { + return m.Size() +} +func (m *Addons) XXX_DiscardUnknown() { + xxx_messageInfo_Addons.DiscardUnknown(m) +} + +var xxx_messageInfo_Addons proto.InternalMessageInfo + +func (m *Addons) GetFlow() string { + if m != nil { + return m.Flow + } + return "" +} + +func (m *Addons) GetSeed() []byte { + if m != nil { + return m.Seed + } + return nil +} + +func init() { + proto.RegisterType((*Addons)(nil), "xray.proxy.vless.encoding.Addons") +} + +func init() { proto.RegisterFile("proxy/vless/encoding/addons.proto", fileDescriptor_75ab671b0ca8b1cc) } + +var fileDescriptor_75ab671b0ca8b1cc = []byte{ + // 195 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x2c, 0x28, 0xca, 0xaf, + 0xa8, 0xd4, 0x2f, 0xcb, 0x49, 0x2d, 0x2e, 0xd6, 0x4f, 0xcd, 0x4b, 0xce, 0x4f, 0xc9, 0xcc, 0x4b, + 0xd7, 0x4f, 0x4c, 0x49, 0xc9, 0xcf, 0x2b, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, 0xac, + 0x28, 0x4a, 0xac, 0xd4, 0x03, 0xab, 0xd3, 0x03, 0xab, 0xd3, 0x83, 0xa9, 0x53, 0x32, 0xe0, 0x62, + 0x73, 0x04, 0x2b, 0x15, 0x12, 0xe2, 0x62, 0x71, 0xcb, 0xc9, 0x2f, 0x97, 0x60, 0x54, 0x60, 0xd4, + 0xe0, 0x0c, 0x02, 0xb3, 0x41, 0x62, 0xc1, 0xa9, 0xa9, 0x29, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0x3c, + 0x41, 0x60, 0xb6, 0x53, 0x03, 0xe3, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, + 0x24, 0xc7, 0x38, 0xe3, 0xb1, 0x1c, 0x03, 0x97, 0x6c, 0x72, 0x7e, 0xae, 0x1e, 0x4e, 0x3b, 0x02, + 0x18, 0xa3, 0x0c, 0xd3, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x2b, 0x4a, + 0x72, 0x8a, 0xf5, 0x41, 0x8a, 0x75, 0x93, 0xf3, 0x8b, 0x52, 0xf5, 0xcb, 0x0c, 0xf5, 0xb1, 0x79, + 0x60, 0x15, 0x93, 0x64, 0x04, 0xc8, 0xc0, 0x00, 0xb0, 0x81, 0x61, 0x60, 0x03, 0x5d, 0xa1, 0x72, + 0x49, 0x6c, 0x60, 0x6f, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xda, 0x20, 0x32, 0x3e, 0xfb, + 0x00, 0x00, 0x00, +} + +func (m *Addons) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Addons) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Addons) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Seed) > 0 { + i -= len(m.Seed) + copy(dAtA[i:], m.Seed) + i = encodeVarintAddons(dAtA, i, uint64(len(m.Seed))) + i-- + dAtA[i] = 0x12 + } + if len(m.Flow) > 0 { + i -= len(m.Flow) + copy(dAtA[i:], m.Flow) + i = encodeVarintAddons(dAtA, i, uint64(len(m.Flow))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintAddons(dAtA []byte, offset int, v uint64) int { + offset -= sovAddons(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Addons) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Flow) + if l > 0 { + n += 1 + l + sovAddons(uint64(l)) + } + l = len(m.Seed) + if l > 0 { + n += 1 + l + sovAddons(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovAddons(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozAddons(x uint64) (n int) { + return sovAddons(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Addons) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAddons + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Addons: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Addons: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Flow", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAddons + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAddons + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAddons + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Flow = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Seed", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAddons + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthAddons + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthAddons + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Seed = append(m.Seed[:0], dAtA[iNdEx:postIndex]...) + if m.Seed == nil { + m.Seed = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAddons(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthAddons + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthAddons + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipAddons(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAddons + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAddons + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAddons + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthAddons + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupAddons + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthAddons + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthAddons = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowAddons = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupAddons = fmt.Errorf("proto: unexpected end of group") +) diff --git a/proxy/vless/encoding/addons.proto b/proxy/vless/encoding/addons.proto new file mode 100644 index 00000000..b913a53f --- /dev/null +++ b/proxy/vless/encoding/addons.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.proxy.vless.encoding; +option csharp_namespace = "Xray.Proxy.Vless.Encoding"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vless/encoding"; +option java_package = "com.xray.proxy.vless.encoding"; +option java_multiple_files = true; + +message Addons { + string Flow = 1; + bytes Seed = 2; +} diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go new file mode 100644 index 00000000..7f24c1c2 --- /dev/null +++ b/proxy/vless/encoding/encoding.go @@ -0,0 +1,208 @@ +// +build !confonly + +package encoding + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "fmt" + "io" + "syscall" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/features/stats" + "github.com/xtls/xray-core/v1/proxy/vless" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +const ( + Version = byte(0) +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) + +// EncodeRequestHeader writes encoded request header into the given writer. +func EncodeRequestHeader(writer io.Writer, request *protocol.RequestHeader, requestAddons *Addons) error { + buffer := buf.StackNew() + defer buffer.Release() + + if err := buffer.WriteByte(request.Version); err != nil { + return newError("failed to write request version").Base(err) + } + + if _, err := buffer.Write(request.User.Account.(*vless.MemoryAccount).ID.Bytes()); err != nil { + return newError("failed to write request user id").Base(err) + } + + if err := EncodeHeaderAddons(&buffer, requestAddons); err != nil { + return newError("failed to encode request header addons").Base(err) + } + + if err := buffer.WriteByte(byte(request.Command)); err != nil { + return newError("failed to write request command").Base(err) + } + + if request.Command != protocol.RequestCommandMux { + if err := addrParser.WriteAddressPort(&buffer, request.Address, request.Port); err != nil { + return newError("failed to write request address and port").Base(err) + } + } + + if _, err := writer.Write(buffer.Bytes()); err != nil { + return newError("failed to write request header").Base(err) + } + + return nil +} + +// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream. +func DecodeRequestHeader(isfb bool, first *buf.Buffer, reader io.Reader, validator *vless.Validator) (*protocol.RequestHeader, *Addons, bool, error) { + buffer := buf.StackNew() + defer buffer.Release() + + request := new(protocol.RequestHeader) + + if isfb { + request.Version = first.Byte(0) + } else { + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, nil, false, newError("failed to read request version").Base(err) + } + request.Version = buffer.Byte(0) + } + + switch request.Version { + case 0: + + var id [16]byte + + if isfb { + copy(id[:], first.BytesRange(1, 17)) + } else { + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 16); err != nil { + return nil, nil, false, newError("failed to read request user id").Base(err) + } + copy(id[:], buffer.Bytes()) + } + + if request.User = validator.Get(id); request.User == nil { + return nil, nil, isfb, newError("invalid request user id") + } + + if isfb { + first.Advance(17) + } + + requestAddons, err := DecodeHeaderAddons(&buffer, reader) + if err != nil { + return nil, nil, false, newError("failed to decode request header addons").Base(err) + } + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, nil, false, newError("failed to read request command").Base(err) + } + + request.Command = protocol.RequestCommand(buffer.Byte(0)) + switch request.Command { + case protocol.RequestCommandMux: + request.Address = net.DomainAddress("v1.mux.cool") + request.Port = 0 + case protocol.RequestCommandTCP, protocol.RequestCommandUDP: + if addr, port, err := addrParser.ReadAddressPort(&buffer, reader); err == nil { + request.Address = addr + request.Port = port + } + } + if request.Address == nil { + return nil, nil, false, newError("invalid request address") + } + return request, requestAddons, false, nil + default: + return nil, nil, isfb, newError("invalid request version") + } +} + +// EncodeResponseHeader writes encoded response header into the given writer. +func EncodeResponseHeader(writer io.Writer, request *protocol.RequestHeader, responseAddons *Addons) error { + buffer := buf.StackNew() + defer buffer.Release() + + if err := buffer.WriteByte(request.Version); err != nil { + return newError("failed to write response version").Base(err) + } + + if err := EncodeHeaderAddons(&buffer, responseAddons); err != nil { + return newError("failed to encode response header addons").Base(err) + } + + if _, err := writer.Write(buffer.Bytes()); err != nil { + return newError("failed to write response header").Base(err) + } + + return nil +} + +// DecodeResponseHeader decodes and returns (if successful) a ResponseHeader from an input stream. +func DecodeResponseHeader(reader io.Reader, request *protocol.RequestHeader) (*Addons, error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, newError("failed to read response version").Base(err) + } + + if buffer.Byte(0) != request.Version { + return nil, newError("unexpected response version. Expecting ", int(request.Version), " but actually ", int(buffer.Byte(0))) + } + + responseAddons, err := DecodeHeaderAddons(&buffer, reader) + if err != nil { + return nil, newError("failed to decode response header addons").Base(err) + } + + return responseAddons, nil +} + +func ReadV(reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, conn *xtls.Conn, rawConn syscall.RawConn, counter stats.Counter) error { + err := func() error { + var ct stats.Counter + for { + if conn.DirectIn { + conn.DirectIn = false + reader = buf.NewReadVReader(conn.Connection, rawConn) + ct = counter + if conn.SHOW { + fmt.Println(conn.MARK, "ReadV") + } + } + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + if ct != nil { + ct.Add(int64(buffer.Len())) + } + timer.Update() + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return werr + } + } + if err != nil { + return err + } + } + }() + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil +} diff --git a/proxy/vless/encoding/encoding_test.go b/proxy/vless/encoding/encoding_test.go new file mode 100644 index 00000000..5e5e2812 --- /dev/null +++ b/proxy/vless/encoding/encoding_test.go @@ -0,0 +1,126 @@ +package encoding_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/proxy/vless" + . "github.com/xtls/xray-core/v1/proxy/vless/encoding" +) + +func toAccount(a *vless.Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestRequestSerialization(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.Validator) + Validator.Add(user) + + actualRequest, actualAddons, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(actualAddons, expectedAddons); r != "" { + t.Error(r) + } +} + +func TestInvalidRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommand(100), + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.Validator) + Validator.Add(user) + + _, _, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + if err == nil { + t.Error("nil error") + } +} + +func TestMuxRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandMux, + Address: net.DomainAddress("v1.mux.cool"), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.Validator) + Validator.Add(user) + + actualRequest, actualAddons, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(actualAddons, expectedAddons); r != "" { + t.Error(r) + } +} diff --git a/proxy/vless/encoding/errors.generated.go b/proxy/vless/encoding/errors.generated.go new file mode 100644 index 00000000..b12e7ece --- /dev/null +++ b/proxy/vless/encoding/errors.generated.go @@ -0,0 +1,9 @@ +package encoding + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/vless/errors.generated.go b/proxy/vless/errors.generated.go new file mode 100644 index 00000000..533d8cb8 --- /dev/null +++ b/proxy/vless/errors.generated.go @@ -0,0 +1,9 @@ +package vless + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/vless/inbound/config.go b/proxy/vless/inbound/config.go new file mode 100644 index 00000000..039be433 --- /dev/null +++ b/proxy/vless/inbound/config.go @@ -0,0 +1,3 @@ +// +build !confonly + +package inbound diff --git a/proxy/vless/inbound/config.pb.go b/proxy/vless/inbound/config.pb.go new file mode 100644 index 00000000..35527fad --- /dev/null +++ b/proxy/vless/inbound/config.pb.go @@ -0,0 +1,286 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vless/inbound/config.proto + +package inbound + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 Fallback struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Alpn string `protobuf:"bytes,1,opt,name=alpn,proto3" json:"alpn,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Dest string `protobuf:"bytes,4,opt,name=dest,proto3" json:"dest,omitempty"` + Xver uint64 `protobuf:"varint,5,opt,name=xver,proto3" json:"xver,omitempty"` +} + +func (x *Fallback) Reset() { + *x = Fallback{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vless_inbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Fallback) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Fallback) ProtoMessage() {} + +func (x *Fallback) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_inbound_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 Fallback.ProtoReflect.Descriptor instead. +func (*Fallback) Descriptor() ([]byte, []int) { + return file_proxy_vless_inbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Fallback) GetAlpn() string { + if x != nil { + return x.Alpn + } + return "" +} + +func (x *Fallback) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Fallback) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Fallback) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Fallback) GetXver() uint64 { + if x != nil { + return x.Xver + } + return 0 +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` + // Decryption settings. Only applies to server side, and only accepts "none" + // for now. + Decryption string `protobuf:"bytes,2,opt,name=decryption,proto3" json:"decryption,omitempty"` + Fallbacks []*Fallback `protobuf:"bytes,3,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vless_inbound_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_proxy_vless_inbound_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_proxy_vless_inbound_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetClients() []*protocol.User { + if x != nil { + return x.Clients + } + return nil +} + +func (x *Config) GetDecryption() string { + if x != nil { + return x.Decryption + } + return "" +} + +func (x *Config) GetFallbacks() []*Fallback { + if x != nil { + return x.Fallbacks + } + return nil +} + +var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor + +var file_proxy_vless_inbound_config_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x18, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x1a, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x08, 0x46, 0x61, 0x6c, 0x6c, + 0x62, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x6c, 0x70, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x61, 0x6c, 0x70, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0xa0, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, + 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x09, 0x66, 0x61, 0x6c, + 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, + 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, + 0x52, 0x09, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x73, 0x42, 0x6d, 0x0a, 0x1c, 0x63, + 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, + 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x30, 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, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, + 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, + 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_proxy_vless_inbound_config_proto_rawDescOnce sync.Once + file_proxy_vless_inbound_config_proto_rawDescData = file_proxy_vless_inbound_config_proto_rawDesc +) + +func file_proxy_vless_inbound_config_proto_rawDescGZIP() []byte { + file_proxy_vless_inbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vless_inbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vless_inbound_config_proto_rawDescData) + }) + return file_proxy_vless_inbound_config_proto_rawDescData +} + +var file_proxy_vless_inbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_vless_inbound_config_proto_goTypes = []interface{}{ + (*Fallback)(nil), // 0: xray.proxy.vless.inbound.Fallback + (*Config)(nil), // 1: xray.proxy.vless.inbound.Config + (*protocol.User)(nil), // 2: xray.common.protocol.User +} +var file_proxy_vless_inbound_config_proto_depIdxs = []int32{ + 2, // 0: xray.proxy.vless.inbound.Config.clients:type_name -> xray.common.protocol.User + 0, // 1: xray.proxy.vless.inbound.Config.fallbacks:type_name -> xray.proxy.vless.inbound.Fallback + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_proxy_vless_inbound_config_proto_init() } +func file_proxy_vless_inbound_config_proto_init() { + if File_proxy_vless_inbound_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vless_inbound_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Fallback); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_vless_inbound_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_proxy_vless_inbound_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_inbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vless_inbound_config_proto_depIdxs, + MessageInfos: file_proxy_vless_inbound_config_proto_msgTypes, + }.Build() + File_proxy_vless_inbound_config_proto = out.File + file_proxy_vless_inbound_config_proto_rawDesc = nil + file_proxy_vless_inbound_config_proto_goTypes = nil + file_proxy_vless_inbound_config_proto_depIdxs = nil +} diff --git a/proxy/vless/inbound/config.proto b/proxy/vless/inbound/config.proto new file mode 100644 index 00000000..664e0f09 --- /dev/null +++ b/proxy/vless/inbound/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xray.proxy.vless.inbound; +option csharp_namespace = "Xray.Proxy.Vless.Inbound"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vless/inbound"; +option java_package = "com.xray.proxy.vless.inbound"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; + +message Fallback { + string alpn = 1; + string path = 2; + string type = 3; + string dest = 4; + uint64 xver = 5; +} + +message Config { + repeated xray.common.protocol.User clients = 1; + // Decryption settings. Only applies to server side, and only accepts "none" + // for now. + string decryption = 2; + repeated Fallback fallbacks = 3; +} diff --git a/proxy/vless/inbound/errors.generated.go b/proxy/vless/inbound/errors.generated.go new file mode 100644 index 00000000..f1f4116f --- /dev/null +++ b/proxy/vless/inbound/errors.generated.go @@ -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{}) +} diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go new file mode 100644 index 00000000..00df133c --- /dev/null +++ b/proxy/vless/inbound/inbound.go @@ -0,0 +1,506 @@ +// +build !confonly + +package inbound + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "io" + "strconv" + "syscall" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/log" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/platform" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/dns" + feature_inbound "github.com/xtls/xray-core/v1/features/inbound" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/features/stats" + "github.com/xtls/xray-core/v1/proxy/vless" + "github.com/xtls/xray-core/v1/proxy/vless/encoding" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + xtls_show = false +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + var dc dns.Client + if err := core.RequireFeatures(ctx, func(d dns.Client) error { + dc = d + return nil + }); err != nil { + return nil, err + } + return New(ctx, config.(*Config), dc) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + xtlsShow := platform.NewEnvFlag("xray.vless.xtls.show").GetValue(func() string { return defaultFlagValue }) + if xtlsShow == "true" { + xtls_show = true + } +} + +// Handler is an inbound connection handler that handles messages in VLess protocol. +type Handler struct { + inboundHandlerManager feature_inbound.Manager + policyManager policy.Manager + validator *vless.Validator + dns dns.Client + fallbacks map[string]map[string]*Fallback // or nil + // regexps map[string]*regexp.Regexp // or nil +} + +// New creates a new VLess inbound handler. +func New(ctx context.Context, config *Config, dc dns.Client) (*Handler, error) { + v := core.MustFromContext(ctx) + handler := &Handler{ + inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + validator: new(vless.Validator), + dns: dc, + } + + for _, user := range config.Clients { + u, err := user.ToMemoryUser() + if err != nil { + return nil, newError("failed to get VLESS user").Base(err).AtError() + } + if err := handler.AddUser(ctx, u); err != nil { + return nil, newError("failed to initiate user").Base(err).AtError() + } + } + + if config.Fallbacks != nil { + handler.fallbacks = make(map[string]map[string]*Fallback) + // handler.regexps = make(map[string]*regexp.Regexp) + for _, fb := range config.Fallbacks { + if handler.fallbacks[fb.Alpn] == nil { + handler.fallbacks[fb.Alpn] = make(map[string]*Fallback) + } + handler.fallbacks[fb.Alpn][fb.Path] = fb + /* + if fb.Path != "" { + if r, err := regexp.Compile(fb.Path); err != nil { + return nil, newError("invalid path regexp").Base(err).AtError() + } else { + handler.regexps[fb.Path] = r + } + } + */ + } + if handler.fallbacks[""] != nil { + for alpn, pfb := range handler.fallbacks { + if alpn != "" { // && alpn != "h2" { + for path, fb := range handler.fallbacks[""] { + if pfb[path] == nil { + pfb[path] = fb + } + } + } + } + } + } + + return handler, nil +} + +// Close implements common.Closable.Close(). +func (h *Handler) Close() error { + return errors.Combine(common.Close(h.validator)) +} + +// AddUser implements proxy.UserManager.AddUser(). +func (h *Handler) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return h.validator.Add(u) +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (h *Handler) RemoveUser(ctx context.Context, e string) error { + return h.validator.Del(e) +} + +// Network implements proxy.Inbound.Network(). +func (*Handler) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +// Process implements proxy.Inbound.Process(). +func (h *Handler) Process(ctx context.Context, network net.Network, connection internet.Connection, dispatcher routing.Dispatcher) error { + sid := session.ExportIDToError(ctx) + + iConn := connection + statConn, ok := iConn.(*internet.StatCouterConnection) + if ok { + iConn = statConn.Connection + } + + sessionPolicy := h.policyManager.ForLevel(0) + if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + first := buf.New() + defer first.Release() + + firstLen, _ := first.ReadFrom(connection) + newError("firstLen = ", firstLen).AtInfo().WriteToLog(sid) + + reader := &buf.BufferedReader{ + Reader: buf.NewReader(connection), + Buffer: buf.MultiBuffer{first}, + } + + var request *protocol.RequestHeader + var requestAddons *encoding.Addons + var err error + + apfb := h.fallbacks + isfb := apfb != nil + + if isfb && firstLen < 18 { + err = newError("fallback directly") + } else { + request, requestAddons, isfb, err = encoding.DecodeRequestHeader(isfb, first, reader, h.validator) + } + + if err != nil { + if isfb { + if err := connection.SetReadDeadline(time.Time{}); err != nil { + newError("unable to set back read deadline").Base(err).AtWarning().WriteToLog(sid) + } + newError("fallback starts").Base(err).AtInfo().WriteToLog(sid) + + alpn := "" + if len(apfb) > 1 || apfb[""] == nil { + if tlsConn, ok := iConn.(*tls.Conn); ok { + alpn = tlsConn.ConnectionState().NegotiatedProtocol + newError("realAlpn = " + alpn).AtInfo().WriteToLog(sid) + } else if xtlsConn, ok := iConn.(*xtls.Conn); ok { + alpn = xtlsConn.ConnectionState().NegotiatedProtocol + newError("realAlpn = " + alpn).AtInfo().WriteToLog(sid) + } + if apfb[alpn] == nil { + alpn = "" + } + } + pfb := apfb[alpn] + if pfb == nil { + return newError(`failed to find the default "alpn" config`).AtWarning() + } + + path := "" + if len(pfb) > 1 || pfb[""] == nil { + /* + if lines := bytes.Split(firstBytes, []byte{'\r', '\n'}); len(lines) > 1 { + if s := bytes.Split(lines[0], []byte{' '}); len(s) == 3 { + if len(s[0]) < 8 && len(s[1]) > 0 && len(s[2]) == 8 { + newError("realPath = " + string(s[1])).AtInfo().WriteToLog(sid) + for _, fb := range pfb { + if fb.Path != "" && h.regexps[fb.Path].Match(s[1]) { + path = fb.Path + break + } + } + } + } + } + */ + if firstLen >= 18 && first.Byte(4) != '*' { // not h2c + firstBytes := first.Bytes() + for i := 4; i <= 8; i++ { // 5 -> 9 + if firstBytes[i] == '/' && firstBytes[i-1] == ' ' { + search := len(firstBytes) + if search > 64 { + search = 64 // up to about 60 + } + for j := i + 1; j < search; j++ { + k := firstBytes[j] + if k == '\r' || k == '\n' { // avoid logging \r or \n + break + } + if k == ' ' { + path = string(firstBytes[i:j]) + newError("realPath = " + path).AtInfo().WriteToLog(sid) + if pfb[path] == nil { + path = "" + } + break + } + } + break + } + } + } + } + fb := pfb[path] + if fb == nil { + return newError(`failed to find the default "path" config`).AtWarning() + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + var conn net.Conn + if err := retry.ExponentialBackoff(5, 100).On(func() error { + var dialer net.Dialer + conn, err = dialer.DialContext(ctx, fb.Type, fb.Dest) + if err != nil { + return err + } + return nil + }); err != nil { + return newError("failed to dial to " + fb.Dest).Base(err).AtWarning() + } + defer conn.Close() + + serverReader := buf.NewReader(conn) + serverWriter := buf.NewWriter(conn) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + if fb.Xver != 0 { + remoteAddr, remotePort, err := net.SplitHostPort(connection.RemoteAddr().String()) + if err != nil { + return err + } + localAddr, localPort, err := net.SplitHostPort(connection.LocalAddr().String()) + if err != nil { + return err + } + ipv4 := true + for i := 0; i < len(remoteAddr); i++ { + if remoteAddr[i] == ':' { + ipv4 = false + break + } + } + pro := buf.New() + defer pro.Release() + switch fb.Xver { + case 1: + if ipv4 { + pro.Write([]byte("PROXY TCP4 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n")) + } else { + pro.Write([]byte("PROXY TCP6 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n")) + } + + case 2: + pro.Write([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21")) // signature + v2 + PROXY + if ipv4 { + pro.Write([]byte("\x11\x00\x0C")) // AF_INET + STREAM + 12 bytes + pro.Write(net.ParseIP(remoteAddr).To4()) + pro.Write(net.ParseIP(localAddr).To4()) + } else { + pro.Write([]byte("\x21\x00\x24")) // AF_INET6 + STREAM + 36 bytes + pro.Write(net.ParseIP(remoteAddr).To16()) + pro.Write(net.ParseIP(localAddr).To16()) + } + p1, _ := strconv.ParseUint(remotePort, 10, 16) + p2, _ := strconv.ParseUint(localPort, 10, 16) + pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)}) + } + if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil { + return newError("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning() + } + } + if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to fallback request payload").Base(err).AtInfo() + } + return nil + } + + writer := buf.NewWriter(connection) + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to deliver response payload").Base(err).AtInfo() + } + return nil + } + + if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil { + common.Interrupt(serverReader) + common.Interrupt(serverWriter) + return newError("fallback ends").Base(err).AtInfo() + } + return nil + } + + if errors.Cause(err) != io.EOF { + log.Record(&log.AccessMessage{ + From: connection.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + err = newError("invalid request from ", connection.RemoteAddr()).Base(err).AtInfo() + } + return err + } + + if err := connection.SetReadDeadline(time.Time{}); err != nil { + newError("unable to set back read deadline").Base(err).AtWarning().WriteToLog(sid) + } + newError("received request for ", request.Destination()).AtInfo().WriteToLog(sid) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = request.User + + account := request.User.Account.(*vless.MemoryAccount) + + responseAddons := &encoding.Addons{ + // Flow: requestAddons.Flow, + } + + var rawConn syscall.RawConn + + switch requestAddons.Flow { + case vless.XRO, vless.XRD: + if account.Flow == requestAddons.Flow { + switch request.Command { + case protocol.RequestCommandMux: + return newError(requestAddons.Flow + " doesn't support Mux").AtWarning() + case protocol.RequestCommandUDP: + return newError(requestAddons.Flow + " doesn't support UDP").AtWarning() + case protocol.RequestCommandTCP: + if xtlsConn, ok := iConn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = xtls_show + xtlsConn.MARK = "XTLS" + if requestAddons.Flow == vless.XRD { + xtlsConn.DirectMode = true + if sc, ok := xtlsConn.Connection.(syscall.Conn); ok { + rawConn, _ = sc.SyscallConn() + } + } + } else { + return newError(`failed to use ` + requestAddons.Flow + `, maybe "security" is not "xtls"`).AtWarning() + } + } + } else { + return newError(account.ID.String() + " is not able to use " + requestAddons.Flow).AtWarning() + } + case "": + default: + return newError("unknown request flow " + requestAddons.Flow).AtWarning() + } + + if request.Command != protocol.RequestCommandMux { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: connection.RemoteAddr(), + To: request.Destination(), + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } + + sessionPolicy = h.policyManager.ForLevel(request.User.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + link, err := dispatcher.Dispatch(ctx, request.Destination()) + if err != nil { + return newError("failed to dispatch request to ", request.Destination()).Base(err).AtWarning() + } + + serverReader := link.Reader // .(*pipe.Reader) + serverWriter := link.Writer // .(*pipe.Writer) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + // default: clientReader := reader + clientReader := encoding.DecodeBodyAddons(reader, request, requestAddons) + + var err error + + if rawConn != nil { + var counter stats.Counter + if statConn != nil { + counter = statConn.ReadCounter + } + err = encoding.ReadV(clientReader, serverWriter, timer, iConn.(*xtls.Conn), rawConn, counter) + } else { + // from clientReader.ReadMultiBuffer to serverWriter.WriteMultiBufer + err = buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer)) + } + + if err != nil { + return newError("failed to transfer request payload").Base(err).AtInfo() + } + + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(connection)) + if err := encoding.EncodeResponseHeader(bufferWriter, request, responseAddons); err != nil { + return newError("failed to encode response header").Base(err).AtWarning() + } + + // default: clientWriter := bufferWriter + clientWriter := encoding.EncodeBodyAddons(bufferWriter, request, responseAddons) + { + multiBuffer, err := serverReader.ReadMultiBuffer() + if err != nil { + return err // ... + } + if err := clientWriter.WriteMultiBuffer(multiBuffer); err != nil { + return err // ... + } + } + + // Flush; bufferWriter.WriteMultiBufer now is bufferWriter.writer.WriteMultiBuffer + if err := bufferWriter.SetBuffered(false); err != nil { + return newError("failed to write A response payload").Base(err).AtWarning() + } + + // from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBufer + if err := buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer response payload").Base(err).AtInfo() + } + + // Indicates the end of response payload. + switch responseAddons.Flow { + default: + } + + return nil + } + + if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), getResponse); err != nil { + common.Interrupt(serverReader) + common.Interrupt(serverWriter) + return newError("connection ends").Base(err).AtInfo() + } + + return nil +} diff --git a/proxy/vless/outbound/config.go b/proxy/vless/outbound/config.go new file mode 100644 index 00000000..35bf561b --- /dev/null +++ b/proxy/vless/outbound/config.go @@ -0,0 +1,3 @@ +// +build !confonly + +package outbound diff --git a/proxy/vless/outbound/config.pb.go b/proxy/vless/outbound/config.pb.go new file mode 100644 index 00000000..fc968177 --- /dev/null +++ b/proxy/vless/outbound/config.pb.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vless/outbound/config.proto + +package outbound + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 + + Vnext []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=vnext,proto3" json:"vnext,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vless_outbound_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_proxy_vless_outbound_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_proxy_vless_outbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVnext() []*protocol.ServerEndpoint { + if x != nil { + return x.Vnext + } + return nil +} + +var File_proxy_vless_outbound_config_proto protoreflect.FileDescriptor + +var file_proxy_vless_outbound_config_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x21, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x44, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x05, 0x76, + 0x6e, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x52, 0x05, 0x76, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x70, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, + 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x31, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, + 0x6c, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x19, + 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, + 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_proxy_vless_outbound_config_proto_rawDescOnce sync.Once + file_proxy_vless_outbound_config_proto_rawDescData = file_proxy_vless_outbound_config_proto_rawDesc +) + +func file_proxy_vless_outbound_config_proto_rawDescGZIP() []byte { + file_proxy_vless_outbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vless_outbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vless_outbound_config_proto_rawDescData) + }) + return file_proxy_vless_outbound_config_proto_rawDescData +} + +var file_proxy_vless_outbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vless_outbound_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.proxy.vless.outbound.Config + (*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint +} +var file_proxy_vless_outbound_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vless.outbound.Config.vnext:type_name -> xray.common.protocol.ServerEndpoint + 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_proxy_vless_outbound_config_proto_init() } +func file_proxy_vless_outbound_config_proto_init() { + if File_proxy_vless_outbound_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vless_outbound_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_proxy_vless_outbound_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_outbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vless_outbound_config_proto_depIdxs, + MessageInfos: file_proxy_vless_outbound_config_proto_msgTypes, + }.Build() + File_proxy_vless_outbound_config_proto = out.File + file_proxy_vless_outbound_config_proto_rawDesc = nil + file_proxy_vless_outbound_config_proto_goTypes = nil + file_proxy_vless_outbound_config_proto_depIdxs = nil +} diff --git a/proxy/vless/outbound/config.proto b/proxy/vless/outbound/config.proto new file mode 100644 index 00000000..a7a8d0e1 --- /dev/null +++ b/proxy/vless/outbound/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.vless.outbound; +option csharp_namespace = "Xray.Proxy.Vless.Outbound"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vless/outbound"; +option java_package = "com.xray.proxy.vless.outbound"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Config { + repeated xray.common.protocol.ServerEndpoint vnext = 1; +} diff --git a/proxy/vless/outbound/errors.generated.go b/proxy/vless/outbound/errors.generated.go new file mode 100644 index 00000000..b6bfdd86 --- /dev/null +++ b/proxy/vless/outbound/errors.generated.go @@ -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{}) +} diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go new file mode 100644 index 00000000..937a44cc --- /dev/null +++ b/proxy/vless/outbound/outbound.go @@ -0,0 +1,240 @@ +// +build !confonly + +package outbound + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "syscall" + "time" + + "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/platform" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + core "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/vless" + "github.com/xtls/xray-core/v1/proxy/vless/encoding" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + xtls_show = false +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + xtlsShow := platform.NewEnvFlag("xray.vless.xtls.show").GetValue(func() string { return defaultFlagValue }) + if xtlsShow == "true" { + xtls_show = true + } +} + +// Handler is an outbound connection handler for VLess protocol. +type Handler struct { + serverList *protocol.ServerList + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// New creates a new VLess outbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Vnext { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to parse server spec").Base(err).AtError() + } + serverList.AddServer(s) + } + + v := core.MustFromContext(ctx) + handler := &Handler{ + serverList: serverList, + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return handler, nil +} + +// Process implements proxy.Outbound.Process(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + var rec *protocol.ServerSpec + var conn internet.Connection + + if err := retry.ExponentialBackoff(5, 200).On(func() error { + rec = h.serverPicker.PickServer() + var err error + conn, err = dialer.Dial(ctx, rec.Destination()) + if err != nil { + return err + } + return nil + }); err != nil { + return newError("failed to find an available destination").Base(err).AtWarning() + } + defer conn.Close() + + iConn := conn + statConn, ok := iConn.(*internet.StatCouterConnection) + if ok { + iConn = statConn.Connection + } + + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified").AtError() + } + + target := outbound.Target + newError("tunneling request to ", target, " via ", rec.Destination()).AtInfo().WriteToLog(session.ExportIDToError(ctx)) + + command := protocol.RequestCommandTCP + if target.Network == net.Network_UDP { + command = protocol.RequestCommandUDP + } + if target.Address.Family().IsDomain() && target.Address.Domain() == "v1.mux.cool" { + command = protocol.RequestCommandMux + } + + request := &protocol.RequestHeader{ + Version: encoding.Version, + User: rec.PickUser(), + Command: command, + Address: target.Address, + Port: target.Port, + } + + account := request.User.Account.(*vless.MemoryAccount) + + requestAddons := &encoding.Addons{ + Flow: account.Flow, + } + + var rawConn syscall.RawConn + + allowUDP443 := false + switch requestAddons.Flow { + case vless.XRO + "-udp443", vless.XRD + "-udp443": + allowUDP443 = true + requestAddons.Flow = requestAddons.Flow[:16] + fallthrough + case vless.XRO, vless.XRD: + switch request.Command { + case protocol.RequestCommandMux: + return newError(requestAddons.Flow + " doesn't support Mux").AtWarning() + case protocol.RequestCommandUDP: + if !allowUDP443 && request.Port == 443 { + return newError(requestAddons.Flow + " stopped UDP/443").AtInfo() + } + requestAddons.Flow = "" + case protocol.RequestCommandTCP: + if xtlsConn, ok := iConn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = xtls_show + xtlsConn.MARK = "XTLS" + if requestAddons.Flow == vless.XRD { + xtlsConn.DirectMode = true + if sc, ok := xtlsConn.Connection.(syscall.Conn); ok { + rawConn, _ = sc.SyscallConn() + } + } + } else { + return newError(`failed to use ` + requestAddons.Flow + `, maybe "security" is not "xtls"`).AtWarning() + } + } + default: + if _, ok := iConn.(*xtls.Conn); ok { + panic(`To avoid misunderstanding, you must fill in VLESS "flow" when using XTLS.`) + } + } + + sessionPolicy := h.policyManager.ForLevel(request.User.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + clientReader := link.Reader // .(*pipe.Reader) + clientWriter := link.Writer // .(*pipe.Writer) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + if err := encoding.EncodeRequestHeader(bufferWriter, request, requestAddons); err != nil { + return newError("failed to encode request header").Base(err).AtWarning() + } + + // default: serverWriter := bufferWriter + serverWriter := encoding.EncodeBodyAddons(bufferWriter, request, requestAddons) + if err := buf.CopyOnceTimeout(clientReader, serverWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return err // ... + } + + // Flush; bufferWriter.WriteMultiBufer now is bufferWriter.writer.WriteMultiBuffer + if err := bufferWriter.SetBuffered(false); err != nil { + return newError("failed to write A request payload").Base(err).AtWarning() + } + + // from clientReader.ReadMultiBuffer to serverWriter.WriteMultiBufer + if err := buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer request payload").Base(err).AtInfo() + } + + // Indicates the end of request payload. + switch requestAddons.Flow { + default: + } + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + responseAddons, err := encoding.DecodeResponseHeader(conn, request) + if err != nil { + return newError("failed to decode response header").Base(err).AtInfo() + } + + // default: serverReader := buf.NewReader(conn) + serverReader := encoding.DecodeBodyAddons(conn, request, responseAddons) + + if rawConn != nil { + var counter stats.Counter + if statConn != nil { + counter = statConn.ReadCounter + } + err = encoding.ReadV(serverReader, clientWriter, timer, iConn.(*xtls.Conn), rawConn, counter) + } else { + // from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBufer + err = buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer)) + } + + if err != nil { + return newError("failed to transfer response payload").Base(err).AtInfo() + } + + return nil + } + + if err := task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(clientWriter))); err != nil { + return newError("connection ends").Base(err).AtInfo() + } + + return nil +} diff --git a/proxy/vless/validator.go b/proxy/vless/validator.go new file mode 100644 index 00000000..41566fe3 --- /dev/null +++ b/proxy/vless/validator.go @@ -0,0 +1,54 @@ +// +build !confonly + +package vless + +import ( + "strings" + "sync" + + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" +) + +// Validator stores valid VLESS users. +type Validator struct { + // Considering email's usage here, map + sync.Mutex/RWMutex may have better performance. + email sync.Map + users sync.Map +} + +// Add a VLESS user, Email must be empty or unique. +func (v *Validator) Add(u *protocol.MemoryUser) error { + if u.Email != "" { + _, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u) + if loaded { + return newError("User ", u.Email, " already exists.") + } + } + v.users.Store(u.Account.(*MemoryAccount).ID.UUID(), u) + return nil +} + +// Del a VLESS user with a non-empty Email. +func (v *Validator) Del(e string) error { + if e == "" { + return newError("Email must not be empty.") + } + le := strings.ToLower(e) + u, _ := v.email.Load(le) + if u == nil { + return newError("User ", e, " not found.") + } + v.email.Delete(le) + v.users.Delete(u.(*protocol.MemoryUser).Account.(*MemoryAccount).ID.UUID()) + return nil +} + +// Get a VLESS user with UUID, nil if user doesn't exist. +func (v *Validator) Get(id uuid.UUID) *protocol.MemoryUser { + u, _ := v.users.Load(id) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} diff --git a/proxy/vless/vless.go b/proxy/vless/vless.go new file mode 100644 index 00000000..f4026804 --- /dev/null +++ b/proxy/vless/vless.go @@ -0,0 +1,13 @@ +// Package vless contains the implementation of VLess protocol and transportation. +// +// VLess contains both inbound and outbound connections. VLess inbound is usually used on servers +// together with 'freedom' to talk to final destination, while VLess outbound is usually used on +// clients with 'socks' for proxying. +package vless + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +const ( + XRO = "xtls-rprx-origin" + XRD = "xtls-rprx-direct" +) diff --git a/proxy/vmess/account.go b/proxy/vmess/account.go new file mode 100644 index 00000000..a9917e4c --- /dev/null +++ b/proxy/vmess/account.go @@ -0,0 +1,51 @@ +// +build !confonly + +package vmess + +import ( + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" +) + +// MemoryAccount is an in-memory form of VMess account. +type MemoryAccount struct { + // ID is the main ID of the account. + ID *protocol.ID + // AlterIDs are the alternative IDs of the account. + AlterIDs []*protocol.ID + // Security type of the account. Used for client connections. + Security protocol.SecurityType +} + +// AnyValidID returns an ID that is either the main ID or one of the alternative IDs if any. +func (a *MemoryAccount) AnyValidID() *protocol.ID { + if len(a.AlterIDs) == 0 { + return a.ID + } + return a.AlterIDs[dice.Roll(len(a.AlterIDs))] +} + +// Equals implements protocol.Account. +func (a *MemoryAccount) Equals(account protocol.Account) bool { + vmessAccount, ok := account.(*MemoryAccount) + if !ok { + return false + } + // TODO: handle AlterIds difference + return a.ID.Equals(vmessAccount.ID) +} + +// AsAccount implements protocol.Account. +func (a *Account) AsAccount() (protocol.Account, error) { + id, err := uuid.ParseString(a.Id) + if err != nil { + return nil, newError("failed to parse ID").Base(err).AtError() + } + protoID := protocol.NewID(id) + return &MemoryAccount{ + ID: protoID, + AlterIDs: protocol.NewAlterIDs(protoID, uint16(a.AlterId)), + Security: a.SecuritySettings.GetSecurityType(), + }, nil +} diff --git a/proxy/vmess/account.pb.go b/proxy/vmess/account.pb.go new file mode 100644 index 00000000..0772f339 --- /dev/null +++ b/proxy/vmess/account.pb.go @@ -0,0 +1,195 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vmess/account.proto + +package vmess + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID of the account, in the form of a UUID, e.g., + // "66ad4540-b58c-4ad2-9926-ea63445a9b57". + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Number of alternative IDs. Client and server must share the same number. + AlterId uint32 `protobuf:"varint,2,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"` + // Security settings. Only applies to client side. + SecuritySettings *protocol.SecurityConfig `protobuf:"bytes,3,opt,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + // Define tests enabled for this account + TestsEnabled string `protobuf:"bytes,4,opt,name=tests_enabled,json=testsEnabled,proto3" json:"tests_enabled,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vmess_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_account_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_vmess_account_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Account) GetAlterId() uint32 { + if x != nil { + return x.AlterId + } + return 0 +} + +func (x *Account) GetSecuritySettings() *protocol.SecurityConfig { + if x != nil { + return x.SecuritySettings + } + return nil +} + +func (x *Account) GetTestsEnabled() string { + if x != nil { + return x.TestsEnabled + } + return "" +} + +var File_proxy_vmess_account_proto protoreflect.FileDescriptor + +var file_proxy_vmess_account_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x1a, 0x1d, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xac, 0x01, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x6c, 0x74, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x6c, 0x74, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x11, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, + 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x53, 0x65, + 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x73, 0x74, 0x73, 0x5f, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, + 0x65, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x55, 0x0a, 0x14, 0x63, + 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, + 0x65, 0x73, 0x73, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0xaa, + 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6d, 0x65, + 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_vmess_account_proto_rawDescOnce sync.Once + file_proxy_vmess_account_proto_rawDescData = file_proxy_vmess_account_proto_rawDesc +) + +func file_proxy_vmess_account_proto_rawDescGZIP() []byte { + file_proxy_vmess_account_proto_rawDescOnce.Do(func() { + file_proxy_vmess_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_account_proto_rawDescData) + }) + return file_proxy_vmess_account_proto_rawDescData +} + +var file_proxy_vmess_account_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vmess_account_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: xray.proxy.vmess.Account + (*protocol.SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig +} +var file_proxy_vmess_account_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vmess.Account.security_settings:type_name -> xray.common.protocol.SecurityConfig + 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_proxy_vmess_account_proto_init() } +func file_proxy_vmess_account_proto_init() { + if File_proxy_vmess_account_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vmess_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); 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_proxy_vmess_account_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_account_proto_goTypes, + DependencyIndexes: file_proxy_vmess_account_proto_depIdxs, + MessageInfos: file_proxy_vmess_account_proto_msgTypes, + }.Build() + File_proxy_vmess_account_proto = out.File + file_proxy_vmess_account_proto_rawDesc = nil + file_proxy_vmess_account_proto_goTypes = nil + file_proxy_vmess_account_proto_depIdxs = nil +} diff --git a/proxy/vmess/account.proto b/proxy/vmess/account.proto new file mode 100644 index 00000000..1572d268 --- /dev/null +++ b/proxy/vmess/account.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package xray.proxy.vmess; +option csharp_namespace = "Xray.Proxy.Vmess"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vmess"; +option java_package = "com.xray.proxy.vmess"; +option java_multiple_files = true; + +import "common/protocol/headers.proto"; + +message Account { + // ID of the account, in the form of a UUID, e.g., + // "66ad4540-b58c-4ad2-9926-ea63445a9b57". + string id = 1; + // Number of alternative IDs. Client and server must share the same number. + uint32 alter_id = 2; + // Security settings. Only applies to client side. + xray.common.protocol.SecurityConfig security_settings = 3; + // Define tests enabled for this account + string tests_enabled = 4; +} diff --git a/proxy/vmess/aead/authid.go b/proxy/vmess/aead/authid.go new file mode 100644 index 00000000..c744a8ce --- /dev/null +++ b/proxy/vmess/aead/authid.go @@ -0,0 +1,119 @@ +package aead + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + rand3 "crypto/rand" + "encoding/binary" + "errors" + "hash/crc32" + "io" + "math" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/antireplay" +) + +var ( + ErrNotFound = errors.New("user do not exist") + ErrReplay = errors.New("replayed request") +) + +func CreateAuthID(cmdKey []byte, time int64) [16]byte { + buf := bytes.NewBuffer(nil) + common.Must(binary.Write(buf, binary.BigEndian, time)) + var zero uint32 + common.Must2(io.CopyN(buf, rand3.Reader, 4)) + zero = crc32.ChecksumIEEE(buf.Bytes()) + common.Must(binary.Write(buf, binary.BigEndian, zero)) + aesBlock := NewCipherFromKey(cmdKey) + if buf.Len() != 16 { + panic("Size unexpected") + } + var result [16]byte + aesBlock.Encrypt(result[:], buf.Bytes()) + return result +} + +func NewCipherFromKey(cmdKey []byte) cipher.Block { + aesBlock, err := aes.NewCipher(KDF16(cmdKey, KDFSaltConstAuthIDEncryptionKey)) + if err != nil { + panic(err) + } + return aesBlock +} + +type AuthIDDecoder struct { + s cipher.Block +} + +func NewAuthIDDecoder(cmdKey []byte) *AuthIDDecoder { + return &AuthIDDecoder{NewCipherFromKey(cmdKey)} +} + +func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) { + aidd.s.Decrypt(data[:], data[:]) + var t int64 + var zero uint32 + var rand int32 + reader := bytes.NewReader(data[:]) + common.Must(binary.Read(reader, binary.BigEndian, &t)) + common.Must(binary.Read(reader, binary.BigEndian, &rand)) + common.Must(binary.Read(reader, binary.BigEndian, &zero)) + return t, zero, rand, data[:] +} + +func NewAuthIDDecoderHolder() *AuthIDDecoderHolder { + return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewReplayFilter(120)} +} + +type AuthIDDecoderHolder struct { + decoders map[string]*AuthIDDecoderItem + filter *antireplay.ReplayFilter +} + +type AuthIDDecoderItem struct { + dec *AuthIDDecoder + ticket interface{} +} + +func NewAuthIDDecoderItem(key [16]byte, ticket interface{}) *AuthIDDecoderItem { + return &AuthIDDecoderItem{ + dec: NewAuthIDDecoder(key[:]), + ticket: ticket, + } +} + +func (a *AuthIDDecoderHolder) AddUser(key [16]byte, ticket interface{}) { + a.decoders[string(key[:])] = NewAuthIDDecoderItem(key, ticket) +} + +func (a *AuthIDDecoderHolder) RemoveUser(key [16]byte) { + delete(a.decoders, string(key[:])) +} + +func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) { + for _, v := range a.decoders { + t, z, _, d := v.dec.Decode(authID) + if z != crc32.ChecksumIEEE(d[:12]) { + continue + } + + if t < 0 { + continue + } + + if math.Abs(math.Abs(float64(t))-float64(time.Now().Unix())) > 120 { + continue + } + + if !a.filter.Check(authID[:]) { + return nil, ErrReplay + } + + return v.ticket, nil + } + return nil, ErrNotFound +} diff --git a/proxy/vmess/aead/authid_test.go b/proxy/vmess/aead/authid_test.go new file mode 100644 index 00000000..837d3372 --- /dev/null +++ b/proxy/vmess/aead/authid_test.go @@ -0,0 +1,127 @@ +package aead + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCreateAuthID(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) +} + +func TestCreateAuthIDAndDecode(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) +} + +func TestCreateAuthIDAndDecode2(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test") + authid2 := CreateAuthID(key2, time.Now().Unix()) + + res2, err2 := AuthDecoder.Match(authid2) + assert.EqualError(t, err2, "user do not exist") + assert.Nil(t, res2) +} + +func TestCreateAuthIDAndDecodeMassive(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + for i := 0; i <= 10000; i++ { + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i)) + var keyw2 [16]byte + copy(keyw2[:], key2) + AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i)) + } + + authid3 := CreateAuthID(key, time.Now().Unix()) + + res2, err2 := AuthDecoder.Match(authid3) + assert.Equal(t, "Demo User", res2) + assert.Nil(t, err2) +} + +func TestCreateAuthIDAndDecodeSuperMassive(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + for i := 0; i <= 1000000; i++ { + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i)) + var keyw2 [16]byte + copy(keyw2[:], key2) + AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i)) + } + + authid3 := CreateAuthID(key, time.Now().Unix()) + + before := time.Now() + res2, err2 := AuthDecoder.Match(authid3) + after := time.Now() + assert.Equal(t, "Demo User", res2) + assert.Nil(t, err2) + + fmt.Println(after.Sub(before).Seconds()) +} diff --git a/proxy/vmess/aead/consts.go b/proxy/vmess/aead/consts.go new file mode 100644 index 00000000..ef13977d --- /dev/null +++ b/proxy/vmess/aead/consts.go @@ -0,0 +1,14 @@ +package aead + +const ( + KDFSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption" + KDFSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key" + KDFSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV" + KDFSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key" + KDFSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV" + KDFSaltConstVMessAEADKDF = "VMess AEAD KDF" + KDFSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key" + KDFSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce" + KDFSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length" + KDFSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length" +) diff --git a/proxy/vmess/aead/encrypt.go b/proxy/vmess/aead/encrypt.go new file mode 100644 index 00000000..bcdf922d --- /dev/null +++ b/proxy/vmess/aead/encrypt.go @@ -0,0 +1,172 @@ +package aead + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "io" + "time" + + "github.com/xtls/xray-core/v1/common" +) + +func SealVMessAEADHeader(key [16]byte, data []byte) []byte { + generatedAuthID := CreateAuthID(key[:], time.Now().Unix()) + + connectionNonce := make([]byte, 8) + if _, err := io.ReadFull(rand.Reader, connectionNonce); err != nil { + panic(err.Error()) + } + + aeadPayloadLengthSerializeBuffer := bytes.NewBuffer(nil) + + headerPayloadDataLen := uint16(len(data)) + + common.Must(binary.Write(aeadPayloadLengthSerializeBuffer, binary.BigEndian, headerPayloadDataLen)) + + aeadPayloadLengthSerializedByte := aeadPayloadLengthSerializeBuffer.Bytes() + var payloadHeaderLengthAEADEncrypted []byte + + { + payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce)) + + payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + + payloadHeaderLengthAEADAESBlock, err := aes.NewCipher(payloadHeaderLengthAEADKey) + if err != nil { + panic(err.Error()) + } + + payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderLengthAEADAESBlock) + + if err != nil { + panic(err.Error()) + } + + payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:]) + } + + var payloadHeaderAEADEncrypted []byte + + { + payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce)) + + payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + + payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderAEADKey) + if err != nil { + panic(err.Error()) + } + + payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock) + + if err != nil { + panic(err.Error()) + } + + payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:]) + } + + var outputBuffer = bytes.NewBuffer(nil) + + common.Must2(outputBuffer.Write(generatedAuthID[:])) // 16 + common.Must2(outputBuffer.Write(payloadHeaderLengthAEADEncrypted)) // 2+16 + common.Must2(outputBuffer.Write(connectionNonce)) // 8 + common.Must2(outputBuffer.Write(payloadHeaderAEADEncrypted)) + + return outputBuffer.Bytes() +} + +func OpenVMessAEADHeader(key [16]byte, authid [16]byte, data io.Reader) ([]byte, bool, int, error) { + var payloadHeaderLengthAEADEncrypted [18]byte + var nonce [8]byte + + var bytesRead int + + authidCheckValueReadBytesCounts, err := io.ReadFull(data, payloadHeaderLengthAEADEncrypted[:]) + bytesRead += authidCheckValueReadBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + nonceReadBytesCounts, err := io.ReadFull(data, nonce[:]) + bytesRead += nonceReadBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + // Decrypt Length + + var decryptedAEADHeaderLengthPayloadResult []byte + + { + payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(authid[:]), string(nonce[:])) + + payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(authid[:]), string(nonce[:]))[:12] + + payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderLengthAEADKey) + if err != nil { + panic(err.Error()) + } + + payloadHeaderLengthAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock) + + if err != nil { + panic(err.Error()) + } + + decryptedAEADHeaderLengthPayload, erropenAEAD := payloadHeaderLengthAEAD.Open(nil, payloadHeaderLengthAEADNonce, payloadHeaderLengthAEADEncrypted[:], authid[:]) + + if erropenAEAD != nil { + return nil, true, bytesRead, erropenAEAD + } + + decryptedAEADHeaderLengthPayloadResult = decryptedAEADHeaderLengthPayload + } + + var length uint16 + + common.Must(binary.Read(bytes.NewReader(decryptedAEADHeaderLengthPayloadResult), binary.BigEndian, &length)) + + var decryptedAEADHeaderPayloadR []byte + + var payloadHeaderAEADEncryptedReadedBytesCounts int + + { + payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(authid[:]), string(nonce[:])) + + payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(authid[:]), string(nonce[:]))[:12] + + // 16 == AEAD Tag size + payloadHeaderAEADEncrypted := make([]byte, length+16) + + payloadHeaderAEADEncryptedReadedBytesCounts, err = io.ReadFull(data, payloadHeaderAEADEncrypted) + bytesRead += payloadHeaderAEADEncryptedReadedBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + payloadHeaderAEADAESBlock, err := aes.NewCipher(payloadHeaderAEADKey) + if err != nil { + panic(err.Error()) + } + + payloadHeaderAEAD, err := cipher.NewGCM(payloadHeaderAEADAESBlock) + + if err != nil { + panic(err.Error()) + } + + decryptedAEADHeaderPayload, erropenAEAD := payloadHeaderAEAD.Open(nil, payloadHeaderAEADNonce, payloadHeaderAEADEncrypted, authid[:]) + + if erropenAEAD != nil { + return nil, true, bytesRead, erropenAEAD + } + + decryptedAEADHeaderPayloadR = decryptedAEADHeaderPayload + } + + return decryptedAEADHeaderPayloadR, false, bytesRead, nil +} diff --git a/proxy/vmess/aead/encrypt_test.go b/proxy/vmess/aead/encrypt_test.go new file mode 100644 index 00000000..b18cfcce --- /dev/null +++ b/proxy/vmess/aead/encrypt_test.go @@ -0,0 +1,104 @@ +package aead + +import ( + "bytes" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpenVMessAEADHeader(t *testing.T) { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + + var AEADR = bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, _, _, err := OpenVMessAEADHeader(keyw, authid, AEADR) + + fmt.Println(string(out)) + fmt.Println(err) +} + +func TestOpenVMessAEADHeader2(t *testing.T) { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + + var AEADR = bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, _, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, string(TestHeader), string(out)) + assert.Nil(t, err) +} + +func TestOpenVMessAEADHeader4(t *testing.T) { + for i := 0; i <= 60; i++ { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + var sealedm [16]byte + copy(sealedm[:], sealed) + sealed[i] ^= 0xff + var AEADR = bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, true, drain) + assert.NotNil(t, err) + if err == nil { + fmt.Println(">") + } + assert.Nil(t, out) + } +} + +func TestOpenVMessAEADHeader4Massive(t *testing.T) { + for j := 0; j < 1000; j++ { + for i := 0; i <= 60; i++ { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + var sealedm [16]byte + copy(sealedm[:], sealed) + sealed[i] ^= 0xff + var AEADR = bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, true, drain) + assert.NotNil(t, err) + if err == nil { + fmt.Println(">") + } + assert.Nil(t, out) + } + } +} diff --git a/proxy/vmess/aead/kdf.go b/proxy/vmess/aead/kdf.go new file mode 100644 index 00000000..ebcea0a3 --- /dev/null +++ b/proxy/vmess/aead/kdf.go @@ -0,0 +1,24 @@ +package aead + +import ( + "crypto/hmac" + "crypto/sha256" + "hash" +) + +func KDF(key []byte, path ...string) []byte { + hmacf := hmac.New(sha256.New, []byte(KDFSaltConstVMessAEADKDF)) + + for _, v := range path { + hmacf = hmac.New(func() hash.Hash { + return hmacf + }, []byte(v)) + } + hmacf.Write(key) + return hmacf.Sum(nil) +} + +func KDF16(key []byte, path ...string) []byte { + r := KDF(key, path...) + return r[:16] +} diff --git a/proxy/vmess/encoding/auth.go b/proxy/vmess/encoding/auth.go new file mode 100644 index 00000000..a6e3dc2f --- /dev/null +++ b/proxy/vmess/encoding/auth.go @@ -0,0 +1,119 @@ +package encoding + +import ( + "crypto/md5" + "encoding/binary" + "hash/fnv" + + "github.com/xtls/xray-core/v1/common" + + "golang.org/x/crypto/sha3" +) + +// Authenticate authenticates a byte array using Fnv hash. +func Authenticate(b []byte) uint32 { + fnv1hash := fnv.New32a() + common.Must2(fnv1hash.Write(b)) + return fnv1hash.Sum32() +} + +type NoOpAuthenticator struct{} + +func (NoOpAuthenticator) NonceSize() int { + return 0 +} + +func (NoOpAuthenticator) Overhead() int { + return 0 +} + +// Seal implements AEAD.Seal(). +func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + return append(dst[:0], plaintext...) +} + +// Open implements AEAD.Open(). +func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + return append(dst[:0], ciphertext...), nil +} + +// FnvAuthenticator is an AEAD based on Fnv hash. +type FnvAuthenticator struct { +} + +// NonceSize implements AEAD.NonceSize(). +func (*FnvAuthenticator) NonceSize() int { + return 0 +} + +// Overhead impelements AEAD.Overhead(). +func (*FnvAuthenticator) Overhead() int { + return 4 +} + +// Seal implements AEAD.Seal(). +func (*FnvAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + dst = append(dst, 0, 0, 0, 0) + binary.BigEndian.PutUint32(dst, Authenticate(plaintext)) + return append(dst, plaintext...) +} + +// Open implements AEAD.Open(). +func (*FnvAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + if binary.BigEndian.Uint32(ciphertext[:4]) != Authenticate(ciphertext[4:]) { + return dst, newError("invalid authentication") + } + return append(dst, ciphertext[4:]...), nil +} + +// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array. +func GenerateChacha20Poly1305Key(b []byte) []byte { + key := make([]byte, 32) + t := md5.Sum(b) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + return key +} + +type ShakeSizeParser struct { + shake sha3.ShakeHash + buffer [2]byte +} + +func NewShakeSizeParser(nonce []byte) *ShakeSizeParser { + shake := sha3.NewShake128() + common.Must2(shake.Write(nonce)) + return &ShakeSizeParser{ + shake: shake, + } +} + +func (*ShakeSizeParser) SizeBytes() int32 { + return 2 +} + +func (s *ShakeSizeParser) next() uint16 { + common.Must2(s.shake.Read(s.buffer[:])) + return binary.BigEndian.Uint16(s.buffer[:]) +} + +func (s *ShakeSizeParser) Decode(b []byte) (uint16, error) { + mask := s.next() + size := binary.BigEndian.Uint16(b) + return mask ^ size, nil +} + +func (s *ShakeSizeParser) Encode(size uint16, b []byte) []byte { + mask := s.next() + binary.BigEndian.PutUint16(b, mask^size) + return b[:2] +} + +func (s *ShakeSizeParser) NextPaddingLen() uint16 { + return s.next() % 64 +} + +func (s *ShakeSizeParser) MaxPaddingLen() uint16 { + return 64 +} diff --git a/proxy/vmess/encoding/auth_test.go b/proxy/vmess/encoding/auth_test.go new file mode 100644 index 00000000..b841901c --- /dev/null +++ b/proxy/vmess/encoding/auth_test.go @@ -0,0 +1,27 @@ +package encoding_test + +import ( + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/proxy/vmess/encoding" +) + +func TestFnvAuth(t *testing.T) { + fnvAuth := new(FnvAuthenticator) + + expectedText := make([]byte, 256) + _, err := rand.Read(expectedText) + common.Must(err) + + buffer := make([]byte, 512) + b := fnvAuth.Seal(buffer[:0], nil, expectedText, nil) + b, err = fnvAuth.Open(buffer[:0], nil, b, nil) + common.Must(err) + if r := cmp.Diff(b, expectedText); r != "" { + t.Error(r) + } +} diff --git a/proxy/vmess/encoding/client.go b/proxy/vmess/encoding/client.go new file mode 100644 index 00000000..b9161c78 --- /dev/null +++ b/proxy/vmess/encoding/client.go @@ -0,0 +1,338 @@ +package encoding + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "hash" + "hash/fnv" + "io" + + "golang.org/x/crypto/chacha20poly1305" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/bitmask" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/proxy/vmess" + vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead" +) + +func hashTimestamp(h hash.Hash, t protocol.Timestamp) []byte { + common.Must2(serial.WriteUint64(h, uint64(t))) + common.Must2(serial.WriteUint64(h, uint64(t))) + common.Must2(serial.WriteUint64(h, uint64(t))) + common.Must2(serial.WriteUint64(h, uint64(t))) + return h.Sum(nil) +} + +// ClientSession stores connection session info for VMess client. +type ClientSession struct { + isAEAD bool + idHash protocol.IDHash + requestBodyKey [16]byte + requestBodyIV [16]byte + responseBodyKey [16]byte + responseBodyIV [16]byte + responseReader io.Reader + responseHeader byte +} + +// NewClientSession creates a new ClientSession. +func NewClientSession(ctx context.Context, isAEAD bool, idHash protocol.IDHash) *ClientSession { + session := &ClientSession{ + isAEAD: isAEAD, + idHash: idHash, + } + + randomBytes := make([]byte, 33) // 16 + 16 + 1 + common.Must2(rand.Read(randomBytes)) + copy(session.requestBodyKey[:], randomBytes[:16]) + copy(session.requestBodyIV[:], randomBytes[16:32]) + session.responseHeader = randomBytes[32] + + if !session.isAEAD { + session.responseBodyKey = md5.Sum(session.requestBodyKey[:]) + session.responseBodyIV = md5.Sum(session.requestBodyIV[:]) + } else { + BodyKey := sha256.Sum256(session.requestBodyKey[:]) + copy(session.responseBodyKey[:], BodyKey[:16]) + BodyIV := sha256.Sum256(session.requestBodyIV[:]) + copy(session.responseBodyIV[:], BodyIV[:16]) + } + + return session +} + +func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error { + timestamp := protocol.NewTimestampGenerator(protocol.NowTime(), 30)() + account := header.User.Account.(*vmess.MemoryAccount) + if !c.isAEAD { + idHash := c.idHash(account.AnyValidID().Bytes()) + common.Must2(serial.WriteUint64(idHash, uint64(timestamp))) + common.Must2(writer.Write(idHash.Sum(nil))) + } + + buffer := buf.New() + defer buffer.Release() + + common.Must(buffer.WriteByte(Version)) + common.Must2(buffer.Write(c.requestBodyIV[:])) + common.Must2(buffer.Write(c.requestBodyKey[:])) + common.Must(buffer.WriteByte(c.responseHeader)) + common.Must(buffer.WriteByte(byte(header.Option))) + + paddingLen := dice.Roll(16) + security := byte(paddingLen<<4) | byte(header.Security) + common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)})) + + if header.Command != protocol.RequestCommandMux { + if err := addrParser.WriteAddressPort(buffer, header.Address, header.Port); err != nil { + return newError("failed to writer address and port").Base(err) + } + } + + if paddingLen > 0 { + common.Must2(buffer.ReadFullFrom(rand.Reader, int32(paddingLen))) + } + + { + fnv1a := fnv.New32a() + common.Must2(fnv1a.Write(buffer.Bytes())) + hashBytes := buffer.Extend(int32(fnv1a.Size())) + fnv1a.Sum(hashBytes[:0]) + } + + if !c.isAEAD { + iv := hashTimestamp(md5.New(), timestamp) + aesStream := crypto.NewAesEncryptionStream(account.ID.CmdKey(), iv) + aesStream.XORKeyStream(buffer.Bytes(), buffer.Bytes()) + common.Must2(writer.Write(buffer.Bytes())) + } else { + var fixedLengthCmdKey [16]byte + copy(fixedLengthCmdKey[:], account.ID.CmdKey()) + vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes()) + common.Must2(io.Copy(writer, bytes.NewReader(vmessout))) + } + + return nil +} + +func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer { + var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(c.requestBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + padding = sizeParser.(crypto.PaddingLengthGenerator) + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamWriter(sizeParser, writer) + } + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding) + } + + return buf.NewWriter(writer) + case protocol.SecurityType_LEGACY: + aesStream := crypto.NewAesEncryptionStream(c.requestBodyKey[:], c.requestBodyIV[:]) + cryptionWriter := crypto.NewCryptionWriter(aesStream, writer) + if request.Option.Has(protocol.RequestOptionChunkStream) { + auth := &crypto.AEADAuthenticator{ + AEAD: new(FnvAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, cryptionWriter, request.Command.TransferType(), padding) + } + + return &buf.SequentialWriter{Writer: cryptionWriter} + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(c.requestBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding) + case protocol.SecurityType_CHACHA20_POLY1305: + aead, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:])) + common.Must(err) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding) + default: + panic("Unknown security type.") + } +} + +func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) { + if !c.isAEAD { + aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:]) + c.responseReader = crypto.NewCryptionReader(aesStream, reader) + } else { + aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey) + aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block) + aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD) + + var aeadEncryptedResponseHeaderLength [18]byte + var decryptedResponseHeaderLength int + var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16 + + if _, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil { + return nil, newError("Unable to Read Header Len").Base(err) + } + if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil { + return nil, newError("Failed To Decrypt Length").Base(err) + } else { + common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer)) + decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer) + } + + aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey) + aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12] + + aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block) + aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD) + + encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16) + + if _, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil { + return nil, newError("Unable to Read Header Data").Base(err) + } + + if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil { + return nil, newError("Failed To Decrypt Payload").Base(err) + } else { + c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer) + } + } + + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(c.responseReader, 4); err != nil { + return nil, newError("failed to read response header").Base(err).AtWarning() + } + + if buffer.Byte(0) != c.responseHeader { + return nil, newError("unexpected response header. Expecting ", int(c.responseHeader), " but actually ", int(buffer.Byte(0))) + } + + header := &protocol.ResponseHeader{ + Option: bitmask.Byte(buffer.Byte(1)), + } + + if buffer.Byte(2) != 0 { + cmdID := buffer.Byte(2) + dataLen := int32(buffer.Byte(3)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(c.responseReader, dataLen); err != nil { + return nil, newError("failed to read response command").Base(err) + } + command, err := UnmarshalCommand(cmdID, buffer.Bytes()) + if err == nil { + header.Command = command + } + } + if c.isAEAD { + aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:]) + c.responseReader = crypto.NewCryptionReader(aesStream, reader) + } + return header, nil +} + +func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader { + var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(c.responseBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + padding = sizeParser.(crypto.PaddingLengthGenerator) + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamReader(sizeParser, reader) + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + + return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding) + } + + return buf.NewReader(reader) + case protocol.SecurityType_LEGACY: + if request.Option.Has(protocol.RequestOptionChunkStream) { + auth := &crypto.AEADAuthenticator{ + AEAD: new(FnvAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, c.responseReader, request.Command.TransferType(), padding) + } + + return buf.NewReader(c.responseReader) + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(c.responseBodyKey[:]) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding) + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.responseBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding) + default: + panic("Unknown security type.") + } +} + +func GenerateChunkNonce(nonce []byte, size uint32) crypto.BytesGenerator { + c := append([]byte(nil), nonce...) + count := uint16(0) + return func() []byte { + binary.BigEndian.PutUint16(c, count) + count++ + return c[:size] + } +} diff --git a/proxy/vmess/encoding/commands.go b/proxy/vmess/encoding/commands.go new file mode 100644 index 00000000..305b7fb2 --- /dev/null +++ b/proxy/vmess/encoding/commands.go @@ -0,0 +1,148 @@ +package encoding + +import ( + "encoding/binary" + "io" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/uuid" +) + +var ( + ErrCommandTypeMismatch = newError("Command type mismatch.") + ErrUnknownCommand = newError("Unknown command.") + ErrCommandTooLarge = newError("Command too large.") +) + +func MarshalCommand(command interface{}, writer io.Writer) error { + if command == nil { + return ErrUnknownCommand + } + + var cmdID byte + var factory CommandFactory + switch command.(type) { + case *protocol.CommandSwitchAccount: + factory = new(CommandSwitchAccountFactory) + cmdID = 1 + default: + return ErrUnknownCommand + } + + buffer := buf.New() + defer buffer.Release() + + err := factory.Marshal(command, buffer) + if err != nil { + return err + } + + auth := Authenticate(buffer.Bytes()) + length := buffer.Len() + 4 + if length > 255 { + return ErrCommandTooLarge + } + + common.Must2(writer.Write([]byte{cmdID, byte(length), byte(auth >> 24), byte(auth >> 16), byte(auth >> 8), byte(auth)})) + common.Must2(writer.Write(buffer.Bytes())) + return nil +} + +func UnmarshalCommand(cmdID byte, data []byte) (protocol.ResponseCommand, error) { + if len(data) <= 4 { + return nil, newError("insufficient length") + } + expectedAuth := Authenticate(data[4:]) + actualAuth := binary.BigEndian.Uint32(data[:4]) + if expectedAuth != actualAuth { + return nil, newError("invalid auth") + } + + var factory CommandFactory + switch cmdID { + case 1: + factory = new(CommandSwitchAccountFactory) + default: + return nil, ErrUnknownCommand + } + return factory.Unmarshal(data[4:]) +} + +type CommandFactory interface { + Marshal(command interface{}, writer io.Writer) error + Unmarshal(data []byte) (interface{}, error) +} + +type CommandSwitchAccountFactory struct { +} + +func (f *CommandSwitchAccountFactory) Marshal(command interface{}, writer io.Writer) error { + cmd, ok := command.(*protocol.CommandSwitchAccount) + if !ok { + return ErrCommandTypeMismatch + } + + hostStr := "" + if cmd.Host != nil { + hostStr = cmd.Host.String() + } + common.Must2(writer.Write([]byte{byte(len(hostStr))})) + + if len(hostStr) > 0 { + common.Must2(writer.Write([]byte(hostStr))) + } + + common.Must2(serial.WriteUint16(writer, cmd.Port.Value())) + + idBytes := cmd.ID.Bytes() + common.Must2(writer.Write(idBytes)) + common.Must2(serial.WriteUint16(writer, cmd.AlterIds)) + common.Must2(writer.Write([]byte{byte(cmd.Level)})) + + common.Must2(writer.Write([]byte{cmd.ValidMin})) + return nil +} + +func (f *CommandSwitchAccountFactory) Unmarshal(data []byte) (interface{}, error) { + cmd := new(protocol.CommandSwitchAccount) + if len(data) == 0 { + return nil, newError("insufficient length.") + } + lenHost := int(data[0]) + if len(data) < lenHost+1 { + return nil, newError("insufficient length.") + } + if lenHost > 0 { + cmd.Host = net.ParseAddress(string(data[1 : 1+lenHost])) + } + portStart := 1 + lenHost + if len(data) < portStart+2 { + return nil, newError("insufficient length.") + } + cmd.Port = net.PortFromBytes(data[portStart : portStart+2]) + idStart := portStart + 2 + if len(data) < idStart+16 { + return nil, newError("insufficient length.") + } + cmd.ID, _ = uuid.ParseBytes(data[idStart : idStart+16]) + alterIDStart := idStart + 16 + if len(data) < alterIDStart+2 { + return nil, newError("insufficient length.") + } + cmd.AlterIds = binary.BigEndian.Uint16(data[alterIDStart : alterIDStart+2]) + levelStart := alterIDStart + 2 + if len(data) < levelStart+1 { + return nil, newError("insufficient length.") + } + cmd.Level = uint32(data[levelStart]) + timeStart := levelStart + 1 + if len(data) < timeStart { + return nil, newError("insufficient length.") + } + cmd.ValidMin = data[timeStart] + return cmd, nil +} diff --git a/proxy/vmess/encoding/commands_test.go b/proxy/vmess/encoding/commands_test.go new file mode 100644 index 00000000..b29de3c0 --- /dev/null +++ b/proxy/vmess/encoding/commands_test.go @@ -0,0 +1,37 @@ +package encoding_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/uuid" + . "github.com/xtls/xray-core/v1/proxy/vmess/encoding" +) + +func TestSwitchAccount(t *testing.T) { + sa := &protocol.CommandSwitchAccount{ + Port: 1234, + ID: uuid.New(), + AlterIds: 1024, + Level: 128, + ValidMin: 16, + } + + buffer := buf.New() + common.Must(MarshalCommand(sa, buffer)) + + cmd, err := UnmarshalCommand(1, buffer.BytesFrom(2)) + common.Must(err) + + sa2, ok := cmd.(*protocol.CommandSwitchAccount) + if !ok { + t.Fatal("failed to convert command to CommandSwitchAccount") + } + if r := cmp.Diff(sa2, sa); r != "" { + t.Error(r) + } +} diff --git a/proxy/vmess/encoding/encoding.go b/proxy/vmess/encoding/encoding.go new file mode 100644 index 00000000..f69ec74f --- /dev/null +++ b/proxy/vmess/encoding/encoding.go @@ -0,0 +1,19 @@ +package encoding + +import ( + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +const ( + Version = byte(1) +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) diff --git a/proxy/vmess/encoding/encoding_test.go b/proxy/vmess/encoding/encoding_test.go new file mode 100644 index 00000000..5dd9ed5d --- /dev/null +++ b/proxy/vmess/encoding/encoding_test.go @@ -0,0 +1,157 @@ +package encoding_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/proxy/vmess" + . "github.com/xtls/xray-core/v1/proxy/vmess/encoding" +) + +func toAccount(a *vmess.Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestRequestSerialization(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + AlterId: 0, + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + Security: protocol.SecurityType_AES128_GCM, + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash) + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + actualRequest, err := server.DecodeRequestHeader(buffer) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + + _, err = server.DecodeRequestHeader(buffer2) + // anti replay attack + if err == nil { + t.Error("nil error") + } +} + +func TestInvalidRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + AlterId: 0, + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommand(100), + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + Security: protocol.SecurityType_AES128_GCM, + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash) + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + _, err := server.DecodeRequestHeader(buffer) + if err == nil { + t.Error("nil error") + } +} + +func TestMuxRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + AlterId: 0, + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommandMux, + Security: protocol.SecurityType_AES128_GCM, + Address: net.DomainAddress("v1.mux.cool"), + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash) + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + actualRequest, err := server.DecodeRequestHeader(buffer) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } +} diff --git a/proxy/vmess/encoding/errors.generated.go b/proxy/vmess/encoding/errors.generated.go new file mode 100644 index 00000000..b12e7ece --- /dev/null +++ b/proxy/vmess/encoding/errors.generated.go @@ -0,0 +1,9 @@ +package encoding + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/vmess/encoding/server.go b/proxy/vmess/encoding/server.go new file mode 100644 index 00000000..b4995908 --- /dev/null +++ b/proxy/vmess/encoding/server.go @@ -0,0 +1,492 @@ +package encoding + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/sha256" + "encoding/binary" + "hash/fnv" + "io" + "io/ioutil" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/bitmask" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/crypto" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/proxy/vmess" + vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead" + "golang.org/x/crypto/chacha20poly1305" +) + +type sessionID struct { + user [16]byte + key [16]byte + nonce [16]byte +} + +// SessionHistory keeps track of historical session ids, to prevent replay attacks. +type SessionHistory struct { + sync.RWMutex + cache map[sessionID]time.Time + task *task.Periodic +} + +// NewSessionHistory creates a new SessionHistory object. +func NewSessionHistory() *SessionHistory { + h := &SessionHistory{ + cache: make(map[sessionID]time.Time, 128), + } + h.task = &task.Periodic{ + Interval: time.Second * 30, + Execute: h.removeExpiredEntries, + } + return h +} + +// Close implements common.Closable. +func (h *SessionHistory) Close() error { + return h.task.Close() +} + +func (h *SessionHistory) addIfNotExits(session sessionID) bool { + h.Lock() + + if expire, found := h.cache[session]; found && expire.After(time.Now()) { + h.Unlock() + return false + } + + h.cache[session] = time.Now().Add(time.Minute * 3) + h.Unlock() + common.Must(h.task.Start()) + return true +} + +func (h *SessionHistory) removeExpiredEntries() error { + now := time.Now() + + h.Lock() + defer h.Unlock() + + if len(h.cache) == 0 { + return newError("nothing to do") + } + + for session, expire := range h.cache { + if expire.Before(now) { + delete(h.cache, session) + } + } + + if len(h.cache) == 0 { + h.cache = make(map[sessionID]time.Time, 128) + } + + return nil +} + +// ServerSession keeps information for a session in VMess server. +type ServerSession struct { + userValidator *vmess.TimedUserValidator + sessionHistory *SessionHistory + requestBodyKey [16]byte + requestBodyIV [16]byte + responseBodyKey [16]byte + responseBodyIV [16]byte + responseWriter io.Writer + responseHeader byte + + isAEADRequest bool + + isAEADForced bool +} + +// NewServerSession creates a new ServerSession, using the given UserValidator. +// The ServerSession instance doesn't take ownership of the validator. +func NewServerSession(validator *vmess.TimedUserValidator, sessionHistory *SessionHistory) *ServerSession { + return &ServerSession{ + userValidator: validator, + sessionHistory: sessionHistory, + } +} + +func parseSecurityType(b byte) protocol.SecurityType { + if _, f := protocol.SecurityType_name[int32(b)]; f { + st := protocol.SecurityType(b) + // For backward compatibility. + if st == protocol.SecurityType_UNKNOWN { + st = protocol.SecurityType_LEGACY + } + return st + } + return protocol.SecurityType_UNKNOWN +} + +// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream. +func (s *ServerSession) DecodeRequestHeader(reader io.Reader) (*protocol.RequestHeader, error) { + buffer := buf.New() + behaviorRand := dice.NewDeterministicDice(int64(s.userValidator.GetBehaviorSeed())) + BaseDrainSize := behaviorRand.Roll(3266) + RandDrainMax := behaviorRand.Roll(64) + 1 + RandDrainRolled := dice.Roll(RandDrainMax) + DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled + readSizeRemain := DrainSize + + drainConnection := func(e error) error { + // We read a deterministic generated length of data before closing the connection to offset padding read pattern + readSizeRemain -= int(buffer.Len()) + if readSizeRemain > 0 { + err := s.DrainConnN(reader, readSizeRemain) + if err != nil { + return newError("failed to drain connection DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(err).Base(e) + } + return newError("connection drained DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(e) + } + return e + } + + defer func() { + buffer.Release() + }() + + if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil { + return nil, newError("failed to read request header").Base(err) + } + + var decryptor io.Reader + var vmessAccount *vmess.MemoryAccount + + user, foundAEAD, errorAEAD := s.userValidator.GetAEAD(buffer.Bytes()) + + var fixedSizeAuthID [16]byte + copy(fixedSizeAuthID[:], buffer.Bytes()) + + switch { + case foundAEAD: + vmessAccount = user.Account.(*vmess.MemoryAccount) + var fixedSizeCmdKey [16]byte + copy(fixedSizeCmdKey[:], vmessAccount.ID.CmdKey()) + aeadData, shouldDrain, bytesRead, errorReason := vmessaead.OpenVMessAEADHeader(fixedSizeCmdKey, fixedSizeAuthID, reader) + if errorReason != nil { + if shouldDrain { + readSizeRemain -= bytesRead + return nil, drainConnection(newError("AEAD read failed").Base(errorReason)) + } else { + return nil, drainConnection(newError("AEAD read failed, drain skipped").Base(errorReason)) + } + } + decryptor = bytes.NewReader(aeadData) + s.isAEADRequest = true + + case !s.isAEADForced && errorAEAD == vmessaead.ErrNotFound: + userLegacy, timestamp, valid, userValidationError := s.userValidator.Get(buffer.Bytes()) + if !valid || userValidationError != nil { + return nil, drainConnection(newError("invalid user").Base(userValidationError)) + } + user = userLegacy + iv := hashTimestamp(md5.New(), timestamp) + vmessAccount = userLegacy.Account.(*vmess.MemoryAccount) + + aesStream := crypto.NewAesDecryptionStream(vmessAccount.ID.CmdKey(), iv) + decryptor = crypto.NewCryptionReader(aesStream, reader) + + default: + return nil, drainConnection(newError("invalid user").Base(errorAEAD)) + } + + readSizeRemain -= int(buffer.Len()) + buffer.Clear() + if _, err := buffer.ReadFullFrom(decryptor, 38); err != nil { + return nil, newError("failed to read request header").Base(err) + } + + request := &protocol.RequestHeader{ + User: user, + Version: buffer.Byte(0), + } + + copy(s.requestBodyIV[:], buffer.BytesRange(1, 17)) // 16 bytes + copy(s.requestBodyKey[:], buffer.BytesRange(17, 33)) // 16 bytes + var sid sessionID + copy(sid.user[:], vmessAccount.ID.Bytes()) + sid.key = s.requestBodyKey + sid.nonce = s.requestBodyIV + if !s.sessionHistory.addIfNotExits(sid) { + if !s.isAEADRequest { + drainErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:]) + if drainErr != nil { + return nil, drainConnection(newError("duplicated session id, possibly under replay attack, and failed to taint userHash").Base(drainErr)) + } + return nil, drainConnection(newError("duplicated session id, possibly under replay attack, userHash tainted")) + } else { + return nil, newError("duplicated session id, possibly under replay attack, but this is a AEAD request") + } + } + + s.responseHeader = buffer.Byte(33) // 1 byte + request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte + paddingLen := int(buffer.Byte(35) >> 4) + request.Security = parseSecurityType(buffer.Byte(35) & 0x0F) + // 1 bytes reserved + request.Command = protocol.RequestCommand(buffer.Byte(37)) + + switch request.Command { + case protocol.RequestCommandMux: + request.Address = net.DomainAddress("v1.mux.cool") + request.Port = 0 + + case protocol.RequestCommandTCP, protocol.RequestCommandUDP: + if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil { + request.Address = addr + request.Port = port + } + } + + if paddingLen > 0 { + if _, err := buffer.ReadFullFrom(decryptor, int32(paddingLen)); err != nil { + if !s.isAEADRequest { + burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:]) + if burnErr != nil { + return nil, newError("failed to read padding, failed to taint userHash").Base(burnErr).Base(err) + } + return nil, newError("failed to read padding, userHash tainted").Base(err) + } + return nil, newError("failed to read padding").Base(err) + } + } + + if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil { + if !s.isAEADRequest { + burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:]) + if burnErr != nil { + return nil, newError("failed to read checksum, failed to taint userHash").Base(burnErr).Base(err) + } + return nil, newError("failed to read checksum, userHash tainted").Base(err) + } + return nil, newError("failed to read checksum").Base(err) + } + + fnv1a := fnv.New32a() + common.Must2(fnv1a.Write(buffer.BytesTo(-4))) + actualHash := fnv1a.Sum32() + expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4)) + + if actualHash != expectedHash { + if !s.isAEADRequest { + Autherr := newError("invalid auth, legacy userHash tainted") + burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:]) + if burnErr != nil { + Autherr = newError("invalid auth, can't taint legacy userHash").Base(burnErr) + } + // It is possible that we are under attack described in https://github.com/xray/xray-core/issues/2523 + return nil, drainConnection(Autherr) + } else { + return nil, newError("invalid auth, but this is a AEAD request") + } + } + + if request.Address == nil { + return nil, newError("invalid remote address") + } + + if request.Security == protocol.SecurityType_UNKNOWN || request.Security == protocol.SecurityType_AUTO { + return nil, newError("unknown security type: ", request.Security) + } + + return request, nil +} + +// DecodeRequestBody returns Reader from which caller can fetch decrypted body. +func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader { + var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(s.requestBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + padding = sizeParser.(crypto.PaddingLengthGenerator) + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamReader(sizeParser, reader) + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding) + } + return buf.NewReader(reader) + + case protocol.SecurityType_LEGACY: + aesStream := crypto.NewAesDecryptionStream(s.requestBodyKey[:], s.requestBodyIV[:]) + cryptionReader := crypto.NewCryptionReader(aesStream, reader) + if request.Option.Has(protocol.RequestOptionChunkStream) { + auth := &crypto.AEADAuthenticator{ + AEAD: new(FnvAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, cryptionReader, request.Command.TransferType(), padding) + } + return buf.NewReader(cryptionReader) + + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(s.requestBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding) + + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.requestBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding) + + default: + panic("Unknown security type.") + } +} + +// EncodeResponseHeader writes encoded response header into the given writer. +func (s *ServerSession) EncodeResponseHeader(header *protocol.ResponseHeader, writer io.Writer) { + var encryptionWriter io.Writer + if !s.isAEADRequest { + s.responseBodyKey = md5.Sum(s.requestBodyKey[:]) + s.responseBodyIV = md5.Sum(s.requestBodyIV[:]) + } else { + BodyKey := sha256.Sum256(s.requestBodyKey[:]) + copy(s.responseBodyKey[:], BodyKey[:16]) + BodyIV := sha256.Sum256(s.requestBodyIV[:]) + copy(s.responseBodyIV[:], BodyIV[:16]) + } + + aesStream := crypto.NewAesEncryptionStream(s.responseBodyKey[:], s.responseBodyIV[:]) + encryptionWriter = crypto.NewCryptionWriter(aesStream, writer) + s.responseWriter = encryptionWriter + + aeadEncryptedHeaderBuffer := bytes.NewBuffer(nil) + + if s.isAEADRequest { + encryptionWriter = aeadEncryptedHeaderBuffer + } + + common.Must2(encryptionWriter.Write([]byte{s.responseHeader, byte(header.Option)})) + err := MarshalCommand(header.Command, encryptionWriter) + if err != nil { + common.Must2(encryptionWriter.Write([]byte{0x00, 0x00})) + } + + if s.isAEADRequest { + aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey) + aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block) + aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD) + + aeadResponseHeaderLengthEncryptionBuffer := bytes.NewBuffer(nil) + + decryptedResponseHeaderLengthBinaryDeserializeBuffer := uint16(aeadEncryptedHeaderBuffer.Len()) + + common.Must(binary.Write(aeadResponseHeaderLengthEncryptionBuffer, binary.BigEndian, decryptedResponseHeaderLengthBinaryDeserializeBuffer)) + + AEADEncryptedLength := aeadResponseHeaderLengthEncryptionAEAD.Seal(nil, aeadResponseHeaderLengthEncryptionIV, aeadResponseHeaderLengthEncryptionBuffer.Bytes(), nil) + common.Must2(io.Copy(writer, bytes.NewReader(AEADEncryptedLength))) + + aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey) + aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12] + + aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block) + aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD) + + aeadEncryptedHeaderPayload := aeadResponseHeaderPayloadEncryptionAEAD.Seal(nil, aeadResponseHeaderPayloadEncryptionIV, aeadEncryptedHeaderBuffer.Bytes(), nil) + common.Must2(io.Copy(writer, bytes.NewReader(aeadEncryptedHeaderPayload))) + } +} + +// EncodeResponseBody returns a Writer that auto-encrypt content written by caller. +func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer { + var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(s.responseBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + padding = sizeParser.(crypto.PaddingLengthGenerator) + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamWriter(sizeParser, writer) + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding) + } + return buf.NewWriter(writer) + + case protocol.SecurityType_LEGACY: + if request.Option.Has(protocol.RequestOptionChunkStream) { + auth := &crypto.AEADAuthenticator{ + AEAD: new(FnvAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, s.responseWriter, request.Command.TransferType(), padding) + } + return &buf.SequentialWriter{Writer: s.responseWriter} + + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(s.responseBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding) + + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.responseBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding) + + default: + panic("Unknown security type.") + } +} + +func (s *ServerSession) DrainConnN(reader io.Reader, n int) error { + _, err := io.CopyN(ioutil.Discard, reader, int64(n)) + return err +} diff --git a/proxy/vmess/errors.generated.go b/proxy/vmess/errors.generated.go new file mode 100644 index 00000000..bf5f70da --- /dev/null +++ b/proxy/vmess/errors.generated.go @@ -0,0 +1,9 @@ +package vmess + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/vmess/inbound/config.go b/proxy/vmess/inbound/config.go new file mode 100644 index 00000000..25badc45 --- /dev/null +++ b/proxy/vmess/inbound/config.go @@ -0,0 +1,11 @@ +// +build !confonly + +package inbound + +// GetDefaultValue returns default settings of DefaultConfig. +func (c *Config) GetDefaultValue() *DefaultConfig { + if c.GetDefault() == nil { + return &DefaultConfig{} + } + return c.Default +} diff --git a/proxy/vmess/inbound/config.pb.go b/proxy/vmess/inbound/config.pb.go new file mode 100644 index 00000000..36fa1660 --- /dev/null +++ b/proxy/vmess/inbound/config.pb.go @@ -0,0 +1,333 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vmess/inbound/config.proto + +package inbound + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 DetourConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` +} + +func (x *DetourConfig) Reset() { + *x = DetourConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DetourConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetourConfig) ProtoMessage() {} + +func (x *DetourConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_inbound_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 DetourConfig.ProtoReflect.Descriptor instead. +func (*DetourConfig) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *DetourConfig) GetTo() string { + if x != nil { + return x.To + } + return "" +} + +type DefaultConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AlterId uint32 `protobuf:"varint,1,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"` + Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"` +} + +func (x *DefaultConfig) Reset() { + *x = DefaultConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DefaultConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DefaultConfig) ProtoMessage() {} + +func (x *DefaultConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_inbound_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 DefaultConfig.ProtoReflect.Descriptor instead. +func (*DefaultConfig) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{1} +} + +func (x *DefaultConfig) GetAlterId() uint32 { + if x != nil { + return x.AlterId + } + return 0 +} + +func (x *DefaultConfig) GetLevel() uint32 { + if x != nil { + return x.Level + } + return 0 +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User []*protocol.User `protobuf:"bytes,1,rep,name=user,proto3" json:"user,omitempty"` + Default *DefaultConfig `protobuf:"bytes,2,opt,name=default,proto3" json:"default,omitempty"` + Detour *DetourConfig `protobuf:"bytes,3,opt,name=detour,proto3" json:"detour,omitempty"` + SecureEncryptionOnly bool `protobuf:"varint,4,opt,name=secure_encryption_only,json=secureEncryptionOnly,proto3" json:"secure_encryption_only,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[2] + 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_proxy_vmess_inbound_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetUser() []*protocol.User { + if x != nil { + return x.User + } + return nil +} + +func (x *Config) GetDefault() *DefaultConfig { + if x != nil { + return x.Default + } + return nil +} + +func (x *Config) GetDetour() *DetourConfig { + if x != nil { + return x.Detour + } + return nil +} + +func (x *Config) GetSecureEncryptionOnly() bool { + if x != nil { + return x.SecureEncryptionOnly + } + return false +} + +var File_proxy_vmess_inbound_config_proto protoreflect.FileDescriptor + +var file_proxy_vmess_inbound_config_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x18, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x1a, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1e, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x6f, + 0x75, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x6c, 0x74, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x6c, 0x74, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0xf1, 0x01, 0x0a, 0x06, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x3e, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x6f, + 0x75, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x2e, 0x44, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x06, 0x64, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x65, 0x63, 0x75, + 0x72, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x6e, + 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x6e, 0x6c, 0x79, 0x42, 0x6d, + 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, + 0x5a, 0x30, 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, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0xaa, 0x02, 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x56, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_vmess_inbound_config_proto_rawDescOnce sync.Once + file_proxy_vmess_inbound_config_proto_rawDescData = file_proxy_vmess_inbound_config_proto_rawDesc +) + +func file_proxy_vmess_inbound_config_proto_rawDescGZIP() []byte { + file_proxy_vmess_inbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vmess_inbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_inbound_config_proto_rawDescData) + }) + return file_proxy_vmess_inbound_config_proto_rawDescData +} + +var file_proxy_vmess_inbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_vmess_inbound_config_proto_goTypes = []interface{}{ + (*DetourConfig)(nil), // 0: xray.proxy.vmess.inbound.DetourConfig + (*DefaultConfig)(nil), // 1: xray.proxy.vmess.inbound.DefaultConfig + (*Config)(nil), // 2: xray.proxy.vmess.inbound.Config + (*protocol.User)(nil), // 3: xray.common.protocol.User +} +var file_proxy_vmess_inbound_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.vmess.inbound.Config.user:type_name -> xray.common.protocol.User + 1, // 1: xray.proxy.vmess.inbound.Config.default:type_name -> xray.proxy.vmess.inbound.DefaultConfig + 0, // 2: xray.proxy.vmess.inbound.Config.detour:type_name -> xray.proxy.vmess.inbound.DetourConfig + 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_proxy_vmess_inbound_config_proto_init() } +func file_proxy_vmess_inbound_config_proto_init() { + if File_proxy_vmess_inbound_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vmess_inbound_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DetourConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_vmess_inbound_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DefaultConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_vmess_inbound_config_proto_msgTypes[2].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_proxy_vmess_inbound_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_inbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vmess_inbound_config_proto_depIdxs, + MessageInfos: file_proxy_vmess_inbound_config_proto_msgTypes, + }.Build() + File_proxy_vmess_inbound_config_proto = out.File + file_proxy_vmess_inbound_config_proto_rawDesc = nil + file_proxy_vmess_inbound_config_proto_goTypes = nil + file_proxy_vmess_inbound_config_proto_depIdxs = nil +} diff --git a/proxy/vmess/inbound/config.proto b/proxy/vmess/inbound/config.proto new file mode 100644 index 00000000..0d901424 --- /dev/null +++ b/proxy/vmess/inbound/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xray.proxy.vmess.inbound; +option csharp_namespace = "Xray.Proxy.Vmess.Inbound"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vmess/inbound"; +option java_package = "com.xray.proxy.vmess.inbound"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; + +message DetourConfig { + string to = 1; +} + +message DefaultConfig { + uint32 alter_id = 1; + uint32 level = 2; +} + +message Config { + repeated xray.common.protocol.User user = 1; + DefaultConfig default = 2; + DetourConfig detour = 3; + bool secure_encryption_only = 4; +} diff --git a/proxy/vmess/inbound/errors.generated.go b/proxy/vmess/inbound/errors.generated.go new file mode 100644 index 00000000..f1f4116f --- /dev/null +++ b/proxy/vmess/inbound/errors.generated.go @@ -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{}) +} diff --git a/proxy/vmess/inbound/inbound.go b/proxy/vmess/inbound/inbound.go new file mode 100644 index 00000000..182def61 --- /dev/null +++ b/proxy/vmess/inbound/inbound.go @@ -0,0 +1,357 @@ +// +build !confonly + +package inbound + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "io" + "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/errors" + "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/common/signal" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/core" + feature_inbound "github.com/xtls/xray-core/v1/features/inbound" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/encoding" + "github.com/xtls/xray-core/v1/transport/internet" +) + +type userByEmail struct { + sync.Mutex + cache map[string]*protocol.MemoryUser + defaultLevel uint32 + defaultAlterIDs uint16 +} + +func newUserByEmail(config *DefaultConfig) *userByEmail { + return &userByEmail{ + cache: make(map[string]*protocol.MemoryUser), + defaultLevel: config.Level, + defaultAlterIDs: uint16(config.AlterId), + } +} + +func (v *userByEmail) addNoLock(u *protocol.MemoryUser) bool { + email := strings.ToLower(u.Email) + _, found := v.cache[email] + if found { + return false + } + v.cache[email] = u + return true +} + +func (v *userByEmail) Add(u *protocol.MemoryUser) bool { + v.Lock() + defer v.Unlock() + + return v.addNoLock(u) +} + +func (v *userByEmail) Get(email string) (*protocol.MemoryUser, bool) { + email = strings.ToLower(email) + + v.Lock() + defer v.Unlock() + + user, found := v.cache[email] + if !found { + id := uuid.New() + rawAccount := &vmess.Account{ + Id: id.String(), + AlterId: uint32(v.defaultAlterIDs), + } + account, err := rawAccount.AsAccount() + common.Must(err) + user = &protocol.MemoryUser{ + Level: v.defaultLevel, + Email: email, + Account: account, + } + v.cache[email] = user + } + return user, found +} + +func (v *userByEmail) Remove(email string) bool { + email = strings.ToLower(email) + + v.Lock() + defer v.Unlock() + + if _, found := v.cache[email]; !found { + return false + } + delete(v.cache, email) + return true +} + +// Handler is an inbound connection handler that handles messages in VMess protocol. +type Handler struct { + policyManager policy.Manager + inboundHandlerManager feature_inbound.Manager + clients *vmess.TimedUserValidator + usersByEmail *userByEmail + detours *DetourConfig + sessionHistory *encoding.SessionHistory + secure bool +} + +// New creates a new VMess inbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + v := core.MustFromContext(ctx) + handler := &Handler{ + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), + clients: vmess.NewTimedUserValidator(protocol.DefaultIDHash), + detours: config.Detour, + usersByEmail: newUserByEmail(config.GetDefaultValue()), + sessionHistory: encoding.NewSessionHistory(), + secure: config.SecureEncryptionOnly, + } + + for _, user := range config.User { + mUser, err := user.ToMemoryUser() + if err != nil { + return nil, newError("failed to get VMess user").Base(err) + } + + if err := handler.AddUser(ctx, mUser); err != nil { + return nil, newError("failed to initiate user").Base(err) + } + } + + return handler, nil +} + +// Close implements common.Closable. +func (h *Handler) Close() error { + return errors.Combine( + h.clients.Close(), + h.sessionHistory.Close(), + common.Close(h.usersByEmail)) +} + +// Network implements proxy.Inbound.Network(). +func (*Handler) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +func (h *Handler) GetUser(email string) *protocol.MemoryUser { + user, existing := h.usersByEmail.Get(email) + if !existing { + h.clients.Add(user) + } + return user +} + +func (h *Handler) AddUser(ctx context.Context, user *protocol.MemoryUser) error { + if len(user.Email) > 0 && !h.usersByEmail.Add(user) { + return newError("User ", user.Email, " already exists.") + } + return h.clients.Add(user) +} + +func (h *Handler) RemoveUser(ctx context.Context, email string) error { + if email == "" { + return newError("Email must not be empty.") + } + if !h.usersByEmail.Remove(email) { + return newError("User ", email, " not found.") + } + h.clients.Remove(email) + return nil +} + +func transferResponse(timer signal.ActivityUpdater, session *encoding.ServerSession, request *protocol.RequestHeader, response *protocol.ResponseHeader, input buf.Reader, output *buf.BufferedWriter) error { + session.EncodeResponseHeader(response, output) + + bodyWriter := session.EncodeResponseBody(request, output) + + { + // Optimize for small response packet + data, err := input.ReadMultiBuffer() + if err != nil { + return err + } + + if err := bodyWriter.WriteMultiBuffer(data); err != nil { + return err + } + } + + if err := output.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return err + } + + if request.Option.Has(protocol.RequestOptionChunkStream) { + if err := bodyWriter.WriteMultiBuffer(buf.MultiBuffer{}); err != nil { + return err + } + } + + return nil +} + +func isInsecureEncryption(s protocol.SecurityType) bool { + return s == protocol.SecurityType_NONE || s == protocol.SecurityType_LEGACY || s == protocol.SecurityType_UNKNOWN +} + +// Process implements proxy.Inbound.Process(). +func (h *Handler) Process(ctx context.Context, network net.Network, connection internet.Connection, dispatcher routing.Dispatcher) error { + sessionPolicy := h.policyManager.ForLevel(0) + if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + reader := &buf.BufferedReader{Reader: buf.NewReader(connection)} + svrSession := encoding.NewServerSession(h.clients, h.sessionHistory) + request, err := svrSession.DecodeRequestHeader(reader) + if err != nil { + if errors.Cause(err) != io.EOF { + log.Record(&log.AccessMessage{ + From: connection.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + err = newError("invalid request from ", connection.RemoteAddr()).Base(err).AtInfo() + } + return err + } + + if h.secure && isInsecureEncryption(request.Security) { + log.Record(&log.AccessMessage{ + From: connection.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: "Insecure encryption", + Email: request.User.Email, + }) + return newError("client is using insecure encryption: ", request.Security) + } + + if request.Command != protocol.RequestCommandMux { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: connection.RemoteAddr(), + To: request.Destination(), + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } + + newError("received request for ", request.Destination()).WriteToLog(session.ExportIDToError(ctx)) + + if err := connection.SetReadDeadline(time.Time{}); err != nil { + newError("unable to set back read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = request.User + + sessionPolicy = h.policyManager.ForLevel(request.User.Level) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + link, err := dispatcher.Dispatch(ctx, request.Destination()) + if err != nil { + return newError("failed to dispatch request to ", request.Destination()).Base(err) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + bodyReader := svrSession.DecodeRequestBody(request, reader) + if err := buf.Copy(bodyReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + writer := buf.NewBufferedWriter(buf.NewWriter(connection)) + defer writer.Flush() + + response := &protocol.ResponseHeader{ + Command: h.generateCommand(ctx, request), + } + return transferResponse(timer, svrSession, request, response, link.Reader, writer) + } + + var requestDonePost = task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return newError("connection ends").Base(err) + } + + return nil +} + +func (h *Handler) generateCommand(ctx context.Context, request *protocol.RequestHeader) protocol.ResponseCommand { + if h.detours != nil { + tag := h.detours.To + if h.inboundHandlerManager != nil { + handler, err := h.inboundHandlerManager.GetHandler(ctx, tag) + if err != nil { + newError("failed to get detour handler: ", tag).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + return nil + } + proxyHandler, port, availableMin := handler.GetRandomInboundProxy() + inboundHandler, ok := proxyHandler.(*Handler) + if ok && inboundHandler != nil { + if availableMin > 255 { + availableMin = 255 + } + + newError("pick detour handler for port ", port, " for ", availableMin, " minutes.").AtDebug().WriteToLog(session.ExportIDToError(ctx)) + user := inboundHandler.GetUser(request.User.Email) + if user == nil { + return nil + } + account := user.Account.(*vmess.MemoryAccount) + return &protocol.CommandSwitchAccount{ + Port: port, + ID: account.ID.UUID(), + AlterIds: uint16(len(account.AlterIDs)), + Level: user.Level, + ValidMin: byte(availableMin), + } + } + } + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/proxy/vmess/outbound/command.go b/proxy/vmess/outbound/command.go new file mode 100644 index 00000000..bd45cfcd --- /dev/null +++ b/proxy/vmess/outbound/command.go @@ -0,0 +1,44 @@ +// +build !confonly + +package outbound + +import ( + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/proxy/vmess" +) + +func (h *Handler) handleSwitchAccount(cmd *protocol.CommandSwitchAccount) { + rawAccount := &vmess.Account{ + Id: cmd.ID.String(), + AlterId: uint32(cmd.AlterIds), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_LEGACY, + }, + } + + account, err := rawAccount.AsAccount() + common.Must(err) + user := &protocol.MemoryUser{ + Email: "", + Level: cmd.Level, + Account: account, + } + dest := net.TCPDestination(cmd.Host, cmd.Port) + until := time.Now().Add(time.Duration(cmd.ValidMin) * time.Minute) + h.serverList.AddServer(protocol.NewServerSpec(dest, protocol.BeforeTime(until), user)) +} + +func (h *Handler) handleCommand(dest net.Destination, cmd protocol.ResponseCommand) { + switch typedCommand := cmd.(type) { + case *protocol.CommandSwitchAccount: + if typedCommand.Host == nil { + typedCommand.Host = dest.Address + } + h.handleSwitchAccount(typedCommand) + default: + } +} diff --git a/proxy/vmess/outbound/config.go b/proxy/vmess/outbound/config.go new file mode 100644 index 00000000..a1e73e06 --- /dev/null +++ b/proxy/vmess/outbound/config.go @@ -0,0 +1 @@ +package outbound diff --git a/proxy/vmess/outbound/config.pb.go b/proxy/vmess/outbound/config.pb.go new file mode 100644 index 00000000..1812818d --- /dev/null +++ b/proxy/vmess/outbound/config.pb.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: proxy/vmess/outbound/config.proto + +package outbound + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 + + Receiver []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=Receiver,proto3" json:"Receiver,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_vmess_outbound_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_proxy_vmess_outbound_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_proxy_vmess_outbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetReceiver() []*protocol.ServerEndpoint { + if x != nil { + return x.Receiver + } + return nil +} + +var File_proxy_vmess_outbound_config_proto protoreflect.FileDescriptor + +var file_proxy_vmess_outbound_config_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x21, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x4a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x08, 0x52, + 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x52, 0x08, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x70, 0x0a, + 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, + 0x5a, 0x31, 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, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x6f, 0x75, 0x74, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x19, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x56, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_vmess_outbound_config_proto_rawDescOnce sync.Once + file_proxy_vmess_outbound_config_proto_rawDescData = file_proxy_vmess_outbound_config_proto_rawDesc +) + +func file_proxy_vmess_outbound_config_proto_rawDescGZIP() []byte { + file_proxy_vmess_outbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vmess_outbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_vmess_outbound_config_proto_rawDescData) + }) + return file_proxy_vmess_outbound_config_proto_rawDescData +} + +var file_proxy_vmess_outbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vmess_outbound_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.proxy.vmess.outbound.Config + (*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint +} +var file_proxy_vmess_outbound_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vmess.outbound.Config.Receiver:type_name -> xray.common.protocol.ServerEndpoint + 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_proxy_vmess_outbound_config_proto_init() } +func file_proxy_vmess_outbound_config_proto_init() { + if File_proxy_vmess_outbound_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_vmess_outbound_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_proxy_vmess_outbound_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_outbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vmess_outbound_config_proto_depIdxs, + MessageInfos: file_proxy_vmess_outbound_config_proto_msgTypes, + }.Build() + File_proxy_vmess_outbound_config_proto = out.File + file_proxy_vmess_outbound_config_proto_rawDesc = nil + file_proxy_vmess_outbound_config_proto_goTypes = nil + file_proxy_vmess_outbound_config_proto_depIdxs = nil +} diff --git a/proxy/vmess/outbound/config.proto b/proxy/vmess/outbound/config.proto new file mode 100644 index 00000000..0ac0f410 --- /dev/null +++ b/proxy/vmess/outbound/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.vmess.outbound; +option csharp_namespace = "Xray.Proxy.Vmess.Outbound"; +option go_package = "github.com/xtls/xray-core/v1/proxy/vmess/outbound"; +option java_package = "com.xray.proxy.vmess.outbound"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Config { + repeated xray.common.protocol.ServerEndpoint Receiver = 1; +} diff --git a/proxy/vmess/outbound/errors.generated.go b/proxy/vmess/outbound/errors.generated.go new file mode 100644 index 00000000..b6bfdd86 --- /dev/null +++ b/proxy/vmess/outbound/errors.generated.go @@ -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{}) +} diff --git a/proxy/vmess/outbound/outbound.go b/proxy/vmess/outbound/outbound.go new file mode 100644 index 00000000..148f85ea --- /dev/null +++ b/proxy/vmess/outbound/outbound.go @@ -0,0 +1,205 @@ +// +build !confonly + +package outbound + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "context" + "time" + + "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/platform" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/task" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/features/policy" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/encoding" + "github.com/xtls/xray-core/v1/transport" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Handler is an outbound connection handler for VMess protocol. +type Handler struct { + serverList *protocol.ServerList + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// New creates a new VMess outbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Receiver { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to parse server spec").Base(err) + } + serverList.AddServer(s) + } + + v := core.MustFromContext(ctx) + handler := &Handler{ + serverList: serverList, + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return handler, nil +} + +// Process implements proxy.Outbound.Process(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + var rec *protocol.ServerSpec + var conn internet.Connection + + err := retry.ExponentialBackoff(5, 200).On(func() error { + rec = h.serverPicker.PickServer() + rawConn, err := dialer.Dial(ctx, rec.Destination()) + if err != nil { + return err + } + conn = rawConn + + return nil + }) + if err != nil { + return newError("failed to find an available destination").Base(err).AtWarning() + } + defer conn.Close() + + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified").AtError() + } + + target := outbound.Target + newError("tunneling request to ", target, " via ", rec.Destination()).WriteToLog(session.ExportIDToError(ctx)) + + command := protocol.RequestCommandTCP + if target.Network == net.Network_UDP { + command = protocol.RequestCommandUDP + } + if target.Address.Family().IsDomain() && target.Address.Domain() == "v1.mux.cool" { + command = protocol.RequestCommandMux + } + + user := rec.PickUser() + request := &protocol.RequestHeader{ + Version: encoding.Version, + User: user, + Command: command, + Address: target.Address, + Port: target.Port, + Option: protocol.RequestOptionChunkStream, + } + + account := request.User.Account.(*vmess.MemoryAccount) + request.Security = account.Security + + if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 { + request.Option.Set(protocol.RequestOptionChunkMasking) + } + + if shouldEnablePadding(request.Security) && request.Option.Has(protocol.RequestOptionChunkMasking) { + request.Option.Set(protocol.RequestOptionGlobalPadding) + } + + input := link.Reader + output := link.Writer + + isAEAD := false + if !aeadDisabled && len(account.AlterIDs) == 0 { + isAEAD = true + } + + session := encoding.NewClientSession(ctx, isAEAD, protocol.DefaultIDHash) + sessionPolicy := h.policyManager.ForLevel(request.User.Level) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + writer := buf.NewBufferedWriter(buf.NewWriter(conn)) + if err := session.EncodeRequestHeader(request, writer); err != nil { + return newError("failed to encode request").Base(err).AtWarning() + } + + bodyWriter := session.EncodeRequestBody(request, writer) + if err := buf.CopyOnceTimeout(input, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return newError("failed to write first payload").Base(err) + } + + if err := writer.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return err + } + + if request.Option.Has(protocol.RequestOptionChunkStream) { + if err := bodyWriter.WriteMultiBuffer(buf.MultiBuffer{}); err != nil { + return err + } + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &buf.BufferedReader{Reader: buf.NewReader(conn)} + header, err := session.DecodeResponseHeader(reader) + if err != nil { + return newError("failed to read header").Base(err) + } + h.handleCommand(rec.Destination(), header.Command) + + bodyReader := session.DecodeResponseBody(request, reader) + + return buf.Copy(bodyReader, output, buf.UpdateActivity(timer)) + } + + var responseDonePost = task.OnSuccess(responseDone, task.Close(output)) + if err := task.Run(ctx, requestDone, responseDonePost); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +var ( + enablePadding = false + aeadDisabled = false +) + +func shouldEnablePadding(s protocol.SecurityType) bool { + return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + paddingValue := platform.NewEnvFlag("xray.vmess.padding").GetValue(func() string { return defaultFlagValue }) + if paddingValue != defaultFlagValue { + enablePadding = true + } + + isAeadDisabled := platform.NewEnvFlag("xray.vmess.aead.disabled").GetValue(func() string { return defaultFlagValue }) + if isAeadDisabled == "true" { + aeadDisabled = true + } +} diff --git a/proxy/vmess/validator.go b/proxy/vmess/validator.go new file mode 100644 index 00000000..c1cc256f --- /dev/null +++ b/proxy/vmess/validator.go @@ -0,0 +1,252 @@ +// +build !confonly + +package vmess + +import ( + "crypto/hmac" + "crypto/sha256" + "hash/crc64" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/proxy/vmess/aead" +) + +const ( + updateInterval = 10 * time.Second + cacheDurationSec = 120 +) + +type user struct { + user protocol.MemoryUser + lastSec protocol.Timestamp +} + +// TimedUserValidator is a user Validator based on time. +type TimedUserValidator struct { + sync.RWMutex + users []*user + userHash map[[16]byte]indexTimePair + hasher protocol.IDHash + baseTime protocol.Timestamp + task *task.Periodic + + behaviorSeed uint64 + behaviorFused bool + + aeadDecoderHolder *aead.AuthIDDecoderHolder +} + +type indexTimePair struct { + user *user + timeInc uint32 + + taintedFuse *uint32 +} + +// NewTimedUserValidator creates a new TimedUserValidator. +func NewTimedUserValidator(hasher protocol.IDHash) *TimedUserValidator { + tuv := &TimedUserValidator{ + users: make([]*user, 0, 16), + userHash: make(map[[16]byte]indexTimePair, 1024), + hasher: hasher, + baseTime: protocol.Timestamp(time.Now().Unix() - cacheDurationSec*2), + aeadDecoderHolder: aead.NewAuthIDDecoderHolder(), + } + tuv.task = &task.Periodic{ + Interval: updateInterval, + Execute: func() error { + tuv.updateUserHash() + return nil + }, + } + common.Must(tuv.task.Start()) + return tuv +} + +func (v *TimedUserValidator) generateNewHashes(nowSec protocol.Timestamp, user *user) { + var hashValue [16]byte + genEndSec := nowSec + cacheDurationSec + genHashForID := func(id *protocol.ID) { + idHash := v.hasher(id.Bytes()) + genBeginSec := user.lastSec + if genBeginSec < nowSec-cacheDurationSec { + genBeginSec = nowSec - cacheDurationSec + } + for ts := genBeginSec; ts <= genEndSec; ts++ { + common.Must2(serial.WriteUint64(idHash, uint64(ts))) + idHash.Sum(hashValue[:0]) + idHash.Reset() + + v.userHash[hashValue] = indexTimePair{ + user: user, + timeInc: uint32(ts - v.baseTime), + taintedFuse: new(uint32), + } + } + } + + account := user.user.Account.(*MemoryAccount) + + genHashForID(account.ID) + for _, id := range account.AlterIDs { + genHashForID(id) + } + user.lastSec = genEndSec +} + +func (v *TimedUserValidator) removeExpiredHashes(expire uint32) { + for key, pair := range v.userHash { + if pair.timeInc < expire { + delete(v.userHash, key) + } + } +} + +func (v *TimedUserValidator) updateUserHash() { + now := time.Now() + nowSec := protocol.Timestamp(now.Unix()) + + v.Lock() + defer v.Unlock() + + for _, user := range v.users { + v.generateNewHashes(nowSec, user) + } + + expire := protocol.Timestamp(now.Unix() - cacheDurationSec) + if expire > v.baseTime { + v.removeExpiredHashes(uint32(expire - v.baseTime)) + } +} + +func (v *TimedUserValidator) Add(u *protocol.MemoryUser) error { + v.Lock() + defer v.Unlock() + + nowSec := time.Now().Unix() + + uu := &user{ + user: *u, + lastSec: protocol.Timestamp(nowSec - cacheDurationSec), + } + v.users = append(v.users, uu) + v.generateNewHashes(protocol.Timestamp(nowSec), uu) + + account := uu.user.Account.(*MemoryAccount) + if !v.behaviorFused { + hashkdf := hmac.New(sha256.New, []byte("VMESSBSKDF")) + hashkdf.Write(account.ID.Bytes()) + v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil)) + } + + var cmdkeyfl [16]byte + copy(cmdkeyfl[:], account.ID.CmdKey()) + v.aeadDecoderHolder.AddUser(cmdkeyfl, u) + + return nil +} + +func (v *TimedUserValidator) Get(userHash []byte) (*protocol.MemoryUser, protocol.Timestamp, bool, error) { + v.RLock() + defer v.RUnlock() + + v.behaviorFused = true + + var fixedSizeHash [16]byte + copy(fixedSizeHash[:], userHash) + pair, found := v.userHash[fixedSizeHash] + if found { + user := pair.user.user + if atomic.LoadUint32(pair.taintedFuse) == 0 { + return &user, protocol.Timestamp(pair.timeInc) + v.baseTime, true, nil + } + return nil, 0, false, ErrTainted + } + return nil, 0, false, ErrNotFound +} + +func (v *TimedUserValidator) GetAEAD(userHash []byte) (*protocol.MemoryUser, bool, error) { + v.RLock() + defer v.RUnlock() + + var userHashFL [16]byte + copy(userHashFL[:], userHash) + + userd, err := v.aeadDecoderHolder.Match(userHashFL) + if err != nil { + return nil, false, err + } + return userd.(*protocol.MemoryUser), true, err +} + +func (v *TimedUserValidator) Remove(email string) bool { + v.Lock() + defer v.Unlock() + + email = strings.ToLower(email) + idx := -1 + for i, u := range v.users { + if strings.EqualFold(u.user.Email, email) { + idx = i + var cmdkeyfl [16]byte + copy(cmdkeyfl[:], u.user.Account.(*MemoryAccount).ID.CmdKey()) + v.aeadDecoderHolder.RemoveUser(cmdkeyfl) + break + } + } + if idx == -1 { + return false + } + ulen := len(v.users) + + v.users[idx] = v.users[ulen-1] + v.users[ulen-1] = nil + v.users = v.users[:ulen-1] + + return true +} + +// Close implements common.Closable. +func (v *TimedUserValidator) Close() error { + return v.task.Close() +} + +func (v *TimedUserValidator) GetBehaviorSeed() uint64 { + v.Lock() + defer v.Unlock() + + v.behaviorFused = true + if v.behaviorSeed == 0 { + v.behaviorSeed = dice.RollUint64() + } + return v.behaviorSeed +} + +func (v *TimedUserValidator) BurnTaintFuse(userHash []byte) error { + v.RLock() + defer v.RUnlock() + + var userHashFL [16]byte + copy(userHashFL[:], userHash) + + pair, found := v.userHash[userHashFL] + if found { + if atomic.CompareAndSwapUint32(pair.taintedFuse, 0, 1) { + return nil + } + return ErrTainted + } + return ErrNotFound +} + +var ErrNotFound = newError("Not Found") + +var ErrTainted = newError("ErrTainted") diff --git a/proxy/vmess/validator_test.go b/proxy/vmess/validator_test.go new file mode 100644 index 00000000..0a22ea31 --- /dev/null +++ b/proxy/vmess/validator_test.go @@ -0,0 +1,110 @@ +package vmess_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/uuid" + . "github.com/xtls/xray-core/v1/proxy/vmess" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestUserValidator(t *testing.T) { + hasher := protocol.DefaultIDHash + v := NewTimedUserValidator(hasher) + defer common.Close(v) + + id := uuid.New() + user := &protocol.MemoryUser{ + Email: "test", + Account: toAccount(&Account{ + Id: id.String(), + AlterId: 8, + }), + } + common.Must(v.Add(user)) + + { + testSmallLag := func(lag time.Duration) { + ts := protocol.Timestamp(time.Now().Add(time.Second * lag).Unix()) + idHash := hasher(id.Bytes()) + common.Must2(serial.WriteUint64(idHash, uint64(ts))) + userHash := idHash.Sum(nil) + + euser, ets, found, _ := v.Get(userHash) + if !found { + t.Fatal("user not found") + } + if euser.Email != user.Email { + t.Error("unexpected user email: ", euser.Email, " want ", user.Email) + } + if ets != ts { + t.Error("unexpected timestamp: ", ets, " want ", ts) + } + } + + testSmallLag(0) + testSmallLag(40) + testSmallLag(-40) + testSmallLag(80) + testSmallLag(-80) + testSmallLag(120) + testSmallLag(-120) + } + + { + testBigLag := func(lag time.Duration) { + ts := protocol.Timestamp(time.Now().Add(time.Second * lag).Unix()) + idHash := hasher(id.Bytes()) + common.Must2(serial.WriteUint64(idHash, uint64(ts))) + userHash := idHash.Sum(nil) + + euser, _, found, _ := v.Get(userHash) + if found || euser != nil { + t.Error("unexpected user") + } + } + + testBigLag(121) + testBigLag(-121) + testBigLag(310) + testBigLag(-310) + testBigLag(500) + testBigLag(-500) + } + + if v := v.Remove(user.Email); !v { + t.Error("unable to remove user") + } + if v := v.Remove(user.Email); v { + t.Error("remove user twice") + } +} + +func BenchmarkUserValidator(b *testing.B) { + for i := 0; i < b.N; i++ { + hasher := protocol.DefaultIDHash + v := NewTimedUserValidator(hasher) + + for j := 0; j < 1500; j++ { + id := uuid.New() + v.Add(&protocol.MemoryUser{ + Email: "test", + Account: toAccount(&Account{ + Id: id.String(), + AlterId: 16, + }), + }) + } + + common.Close(v) + } +} diff --git a/proxy/vmess/vmess.go b/proxy/vmess/vmess.go new file mode 100644 index 00000000..d58abca7 --- /dev/null +++ b/proxy/vmess/vmess.go @@ -0,0 +1,8 @@ +// Package vmess contains the implementation of VMess protocol and transportation. +// +// VMess contains both inbound and outbound connections. VMess inbound is usually used on servers +// together with 'freedom' to talk to final destination, while VMess outbound is usually used on +// clients with 'socks' for proxying. +package vmess + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/proxy/vmess/vmessCtxInterface.go b/proxy/vmess/vmessCtxInterface.go new file mode 100644 index 00000000..5d26f9e5 --- /dev/null +++ b/proxy/vmess/vmessCtxInterface.go @@ -0,0 +1,4 @@ +package vmess + +// example +const AlterID = "VMessCtxInterface_AlterID" diff --git a/testing/coverage/coverall b/testing/coverage/coverall new file mode 100644 index 00000000..a8463437 --- /dev/null +++ b/testing/coverage/coverall @@ -0,0 +1,48 @@ +#!/bin/bash + +FAIL=0 + +XRAY_OUT=${PWD}/out/xray +export XRAY_COV=${XRAY_OUT}/cov +COVERAGE_FILE=${XRAY_COV}/coverage.txt + +function test_package { + DIR=".$1" + DEP=$(go list -f '{{ join .Deps "\n" }}' $DIR | grep xray | tr '\n' ',') + DEP=${DEP}$DIR + RND_NAME=$(openssl rand -hex 16) + COV_PROFILE=${XRAY_COV}/${RND_NAME}.out + go test -tags "json coverage" -coverprofile=${COV_PROFILE} -coverpkg=$DEP $DIR || FAIL=1 +} + +rm -rf ${XRAY_OUT} +mkdir -p ${XRAY_COV} +touch ${COVERAGE_FILE} + +TEST_FILES=(./*_test.go) +if [ -f ${TEST_FILES[0]} ]; then + test_package "" +fi + +for DIR in $(find * -type d ! -path "*.git*" ! -path "*vendor*" ! -path "*external*"); do + TEST_FILES=($DIR/*_test.go) + if [ -f ${TEST_FILES[0]} ]; then + test_package "/$DIR" + fi +done + +for OUT_FILE in $(find ${XRAY_COV} -name "*.out"); do + echo "Merging file ${OUT_FILE}" + cat ${OUT_FILE} | grep -v "mode: set" >> ${COVERAGE_FILE} +done + +COV_SORTED=${XRAY_COV}/coverallsorted.out +cat ${COVERAGE_FILE} | sort -t: -k1 | grep -vw "testing" | grep -v ".pb.go" | grep -vw "vendor" | grep -vw "external" > ${COV_SORTED} +echo "mode: set" | cat - ${COV_SORTED} > ${COVERAGE_FILE} + +if [ "$FAIL" -eq 0 ]; then + echo "Uploading coverage datea to codecov." + #bash <(curl -s https://codecov.io/bash) -f ${COVERAGE_FILE} -v || echo "Codecov did not collect coverage reports." +fi + +exit $FAIL diff --git a/testing/coverage/coverall2 b/testing/coverage/coverall2 new file mode 100644 index 00000000..ce6b0319 --- /dev/null +++ b/testing/coverage/coverall2 @@ -0,0 +1,40 @@ +#!/bin/bash + +COVERAGE_FILE=${PWD}/coverage.txt +COV_SORTED=${PWD}/coverallsorted.out + +touch "$COVERAGE_FILE" + +function test_package { + DIR=".$1" + DEP=$(go list -f '{{ join .Deps "\n" }}' "$DIR" | grep xray | tr '\n' ',') + DEP=${DEP}$DIR + RND_NAME=$(openssl rand -hex 16) + COV_PROFILE=${RND_NAME}.out + go test -coverprofile="$COV_PROFILE" -coverpkg="$DEP" "$DIR" || return +} + +TEST_FILES=(./*_test.go) +if [ -f "${TEST_FILES[0]}" ]; then + test_package "" +fi + +# shellcheck disable=SC2044 +for DIR in $(find ./* -type d ! -path "*.git*" ! -path "*vendor*" ! -path "*external*"); do + TEST_FILES=("$DIR"/*_test.go) + if [ -f "${TEST_FILES[0]}" ]; then + test_package "/$DIR" + fi +done + +# merge out +while IFS= read -r -d '' OUT_FILE +do + echo "Merging file ${OUT_FILE}" + < "${OUT_FILE}" grep -v "mode: set" >> "$COVERAGE_FILE" +done < <(find ./* -name "*.out" -print0) + +< "$COVERAGE_FILE" sort -t: -k1 | grep -vw "testing" | grep -v ".pb.go" | grep -vw "vendor" | grep -vw "external" > "$COV_SORTED" +echo "mode: set" | cat - "${COV_SORTED}" > "${COVERAGE_FILE}" + +bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' \ No newline at end of file diff --git a/testing/mocks/dns.go b/testing/mocks/dns.go new file mode 100644 index 00000000..8b1776df --- /dev/null +++ b/testing/mocks/dns.go @@ -0,0 +1,91 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/v1/features/dns (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + gomock "github.com/golang/mock/gomock" + net "net" + reflect "reflect" +) + +// DNSClient is a mock of Client interface +type DNSClient struct { + ctrl *gomock.Controller + recorder *DNSClientMockRecorder +} + +// DNSClientMockRecorder is the mock recorder for DNSClient +type DNSClientMockRecorder struct { + mock *DNSClient +} + +// NewDNSClient creates a new mock instance +func NewDNSClient(ctrl *gomock.Controller) *DNSClient { + mock := &DNSClient{ctrl: ctrl} + mock.recorder = &DNSClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *DNSClient) EXPECT() *DNSClientMockRecorder { + return m.recorder +} + +// Close mocks base method +func (m *DNSClient) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *DNSClientMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*DNSClient)(nil).Close)) +} + +// LookupIP mocks base method +func (m *DNSClient) LookupIP(arg0 string) ([]net.IP, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupIP", arg0) + ret0, _ := ret[0].([]net.IP) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LookupIP indicates an expected call of LookupIP +func (mr *DNSClientMockRecorder) LookupIP(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0) +} + +// Start mocks base method +func (m *DNSClient) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *DNSClientMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*DNSClient)(nil).Start)) +} + +// Type mocks base method +func (m *DNSClient) Type() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Type indicates an expected call of Type +func (mr *DNSClientMockRecorder) Type() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*DNSClient)(nil).Type)) +} diff --git a/testing/mocks/io.go b/testing/mocks/io.go new file mode 100644 index 00000000..d1f92cea --- /dev/null +++ b/testing/mocks/io.go @@ -0,0 +1,86 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: io (interfaces: Reader,Writer) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// Reader is a mock of Reader interface +type Reader struct { + ctrl *gomock.Controller + recorder *ReaderMockRecorder +} + +// ReaderMockRecorder is the mock recorder for Reader +type ReaderMockRecorder struct { + mock *Reader +} + +// NewReader creates a new mock instance +func NewReader(ctrl *gomock.Controller) *Reader { + mock := &Reader{ctrl: ctrl} + mock.recorder = &ReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Reader) EXPECT() *ReaderMockRecorder { + return m.recorder +} + +// Read mocks base method +func (m *Reader) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read +func (mr *ReaderMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Reader)(nil).Read), arg0) +} + +// Writer is a mock of Writer interface +type Writer struct { + ctrl *gomock.Controller + recorder *WriterMockRecorder +} + +// WriterMockRecorder is the mock recorder for Writer +type WriterMockRecorder struct { + mock *Writer +} + +// NewWriter creates a new mock instance +func NewWriter(ctrl *gomock.Controller) *Writer { + mock := &Writer{ctrl: ctrl} + mock.recorder = &WriterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Writer) EXPECT() *WriterMockRecorder { + return m.recorder +} + +// Write mocks base method +func (m *Writer) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write +func (mr *WriterMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Writer)(nil).Write), arg0) +} diff --git a/testing/mocks/log.go b/testing/mocks/log.go new file mode 100644 index 00000000..94bab43e --- /dev/null +++ b/testing/mocks/log.go @@ -0,0 +1,46 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/v1/common/log (interfaces: Handler) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + gomock "github.com/golang/mock/gomock" + log "github.com/xtls/xray-core/v1/common/log" + reflect "reflect" +) + +// LogHandler is a mock of Handler interface +type LogHandler struct { + ctrl *gomock.Controller + recorder *LogHandlerMockRecorder +} + +// LogHandlerMockRecorder is the mock recorder for LogHandler +type LogHandlerMockRecorder struct { + mock *LogHandler +} + +// NewLogHandler creates a new mock instance +func NewLogHandler(ctrl *gomock.Controller) *LogHandler { + mock := &LogHandler{ctrl: ctrl} + mock.recorder = &LogHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *LogHandler) EXPECT() *LogHandlerMockRecorder { + return m.recorder +} + +// Handle mocks base method +func (m *LogHandler) Handle(arg0 log.Message) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Handle", arg0) +} + +// Handle indicates an expected call of Handle +func (mr *LogHandlerMockRecorder) Handle(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*LogHandler)(nil).Handle), arg0) +} diff --git a/testing/mocks/mux.go b/testing/mocks/mux.go new file mode 100644 index 00000000..18464dba --- /dev/null +++ b/testing/mocks/mux.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/v1/common/mux (interfaces: ClientWorkerFactory) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + gomock "github.com/golang/mock/gomock" + mux "github.com/xtls/xray-core/v1/common/mux" + reflect "reflect" +) + +// MuxClientWorkerFactory is a mock of ClientWorkerFactory interface +type MuxClientWorkerFactory struct { + ctrl *gomock.Controller + recorder *MuxClientWorkerFactoryMockRecorder +} + +// MuxClientWorkerFactoryMockRecorder is the mock recorder for MuxClientWorkerFactory +type MuxClientWorkerFactoryMockRecorder struct { + mock *MuxClientWorkerFactory +} + +// NewMuxClientWorkerFactory creates a new mock instance +func NewMuxClientWorkerFactory(ctrl *gomock.Controller) *MuxClientWorkerFactory { + mock := &MuxClientWorkerFactory{ctrl: ctrl} + mock.recorder = &MuxClientWorkerFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MuxClientWorkerFactory) EXPECT() *MuxClientWorkerFactoryMockRecorder { + return m.recorder +} + +// Create mocks base method +func (m *MuxClientWorkerFactory) Create() (*mux.ClientWorker, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create") + ret0, _ := ret[0].(*mux.ClientWorker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create +func (mr *MuxClientWorkerFactoryMockRecorder) Create() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MuxClientWorkerFactory)(nil).Create)) +} diff --git a/testing/mocks/outbound.go b/testing/mocks/outbound.go new file mode 100644 index 00000000..fd370c29 --- /dev/null +++ b/testing/mocks/outbound.go @@ -0,0 +1,170 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/v1/features/outbound (interfaces: Manager,HandlerSelector) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + outbound "github.com/xtls/xray-core/v1/features/outbound" + reflect "reflect" +) + +// OutboundManager is a mock of Manager interface +type OutboundManager struct { + ctrl *gomock.Controller + recorder *OutboundManagerMockRecorder +} + +// OutboundManagerMockRecorder is the mock recorder for OutboundManager +type OutboundManagerMockRecorder struct { + mock *OutboundManager +} + +// NewOutboundManager creates a new mock instance +func NewOutboundManager(ctrl *gomock.Controller) *OutboundManager { + mock := &OutboundManager{ctrl: ctrl} + mock.recorder = &OutboundManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *OutboundManager) EXPECT() *OutboundManagerMockRecorder { + return m.recorder +} + +// AddHandler mocks base method +func (m *OutboundManager) AddHandler(arg0 context.Context, arg1 outbound.Handler) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddHandler", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddHandler indicates an expected call of AddHandler +func (mr *OutboundManagerMockRecorder) AddHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*OutboundManager)(nil).AddHandler), arg0, arg1) +} + +// Close mocks base method +func (m *OutboundManager) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *OutboundManagerMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*OutboundManager)(nil).Close)) +} + +// GetDefaultHandler mocks base method +func (m *OutboundManager) GetDefaultHandler() outbound.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDefaultHandler") + ret0, _ := ret[0].(outbound.Handler) + return ret0 +} + +// GetDefaultHandler indicates an expected call of GetDefaultHandler +func (mr *OutboundManagerMockRecorder) GetDefaultHandler() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultHandler", reflect.TypeOf((*OutboundManager)(nil).GetDefaultHandler)) +} + +// GetHandler mocks base method +func (m *OutboundManager) GetHandler(arg0 string) outbound.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHandler", arg0) + ret0, _ := ret[0].(outbound.Handler) + return ret0 +} + +// GetHandler indicates an expected call of GetHandler +func (mr *OutboundManagerMockRecorder) GetHandler(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHandler", reflect.TypeOf((*OutboundManager)(nil).GetHandler), arg0) +} + +// RemoveHandler mocks base method +func (m *OutboundManager) RemoveHandler(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveHandler", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveHandler indicates an expected call of RemoveHandler +func (mr *OutboundManagerMockRecorder) RemoveHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*OutboundManager)(nil).RemoveHandler), arg0, arg1) +} + +// Start mocks base method +func (m *OutboundManager) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *OutboundManagerMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*OutboundManager)(nil).Start)) +} + +// Type mocks base method +func (m *OutboundManager) Type() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Type indicates an expected call of Type +func (mr *OutboundManagerMockRecorder) Type() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*OutboundManager)(nil).Type)) +} + +// OutboundHandlerSelector is a mock of HandlerSelector interface +type OutboundHandlerSelector struct { + ctrl *gomock.Controller + recorder *OutboundHandlerSelectorMockRecorder +} + +// OutboundHandlerSelectorMockRecorder is the mock recorder for OutboundHandlerSelector +type OutboundHandlerSelectorMockRecorder struct { + mock *OutboundHandlerSelector +} + +// NewOutboundHandlerSelector creates a new mock instance +func NewOutboundHandlerSelector(ctrl *gomock.Controller) *OutboundHandlerSelector { + mock := &OutboundHandlerSelector{ctrl: ctrl} + mock.recorder = &OutboundHandlerSelectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *OutboundHandlerSelector) EXPECT() *OutboundHandlerSelectorMockRecorder { + return m.recorder +} + +// Select mocks base method +func (m *OutboundHandlerSelector) Select(arg0 []string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Select", arg0) + ret0, _ := ret[0].([]string) + return ret0 +} + +// Select indicates an expected call of Select +func (mr *OutboundHandlerSelectorMockRecorder) Select(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*OutboundHandlerSelector)(nil).Select), arg0) +} diff --git a/testing/mocks/proxy.go b/testing/mocks/proxy.go new file mode 100644 index 00000000..e8a1fe35 --- /dev/null +++ b/testing/mocks/proxy.go @@ -0,0 +1,103 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/v1/proxy (interfaces: Inbound,Outbound) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + net "github.com/xtls/xray-core/v1/common/net" + routing "github.com/xtls/xray-core/v1/features/routing" + transport "github.com/xtls/xray-core/v1/transport" + internet "github.com/xtls/xray-core/v1/transport/internet" + reflect "reflect" +) + +// ProxyInbound is a mock of Inbound interface +type ProxyInbound struct { + ctrl *gomock.Controller + recorder *ProxyInboundMockRecorder +} + +// ProxyInboundMockRecorder is the mock recorder for ProxyInbound +type ProxyInboundMockRecorder struct { + mock *ProxyInbound +} + +// NewProxyInbound creates a new mock instance +func NewProxyInbound(ctrl *gomock.Controller) *ProxyInbound { + mock := &ProxyInbound{ctrl: ctrl} + mock.recorder = &ProxyInboundMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *ProxyInbound) EXPECT() *ProxyInboundMockRecorder { + return m.recorder +} + +// Network mocks base method +func (m *ProxyInbound) Network() []net.Network { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Network") + ret0, _ := ret[0].([]net.Network) + return ret0 +} + +// Network indicates an expected call of Network +func (mr *ProxyInboundMockRecorder) Network() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*ProxyInbound)(nil).Network)) +} + +// Process mocks base method +func (m *ProxyInbound) Process(arg0 context.Context, arg1 net.Network, arg2 internet.Connection, arg3 routing.Dispatcher) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Process indicates an expected call of Process +func (mr *ProxyInboundMockRecorder) Process(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyInbound)(nil).Process), arg0, arg1, arg2, arg3) +} + +// ProxyOutbound is a mock of Outbound interface +type ProxyOutbound struct { + ctrl *gomock.Controller + recorder *ProxyOutboundMockRecorder +} + +// ProxyOutboundMockRecorder is the mock recorder for ProxyOutbound +type ProxyOutboundMockRecorder struct { + mock *ProxyOutbound +} + +// NewProxyOutbound creates a new mock instance +func NewProxyOutbound(ctrl *gomock.Controller) *ProxyOutbound { + mock := &ProxyOutbound{ctrl: ctrl} + mock.recorder = &ProxyOutboundMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *ProxyOutbound) EXPECT() *ProxyOutboundMockRecorder { + return m.recorder +} + +// Process mocks base method +func (m *ProxyOutbound) Process(arg0 context.Context, arg1 *transport.Link, arg2 internet.Dialer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Process indicates an expected call of Process +func (mr *ProxyOutboundMockRecorder) Process(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyOutbound)(nil).Process), arg0, arg1, arg2) +} diff --git a/testing/scenarios/command_test.go b/testing/scenarios/command_test.go new file mode 100644 index 00000000..cd7dc0c8 --- /dev/null +++ b/testing/scenarios/command_test.go @@ -0,0 +1,498 @@ +package scenarios + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/grpc" + + "github.com/xtls/xray-core/v1/app/commander" + "github.com/xtls/xray-core/v1/app/policy" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/app/proxyman/command" + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/app/stats" + statscmd "github.com/xtls/xray-core/v1/app/stats/command" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/uuid" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func TestCommanderRemoveHandler(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + clientPort := tcp.PickPort() + cmdPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(cmdPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "default-outbound", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithInsecure(), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + resp, err := hsClient.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ + Tag: "d", + }) + common.Must(err) + if resp == nil { + t.Error("unexpected nil response") + } + + { + _, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(clientPort), + }) + if err == nil { + t.Error("unexpected nil error") + } + } +} + +func TestCommanderAddRemoveUser(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + u1 := protocol.NewID(uuid.New()) + u2 := protocol.NewID(uuid.New()) + + cmdPort := tcp.PickPort() + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "v", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u1.String(), + AlterId: 64, + }), + }, + }, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(cmdPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u2.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != io.EOF && + /*We might wish to drain the connection*/ + (err != nil && !strings.HasSuffix(err.Error(), "i/o timeout")) { + t.Fatal("expected error: ", err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithInsecure(), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + resp, err := hsClient.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: "v", + Operation: serial.ToTypedMessage( + &command.AddUserOperation{ + User: &protocol.User{ + Email: "test@example.com", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u2.String(), + AlterId: 64, + }), + }, + }), + }) + common.Must(err) + if resp == nil { + t.Fatal("nil response") + } + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + resp, err = hsClient.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: "v", + Operation: serial.ToTypedMessage(&command.RemoveUserOperation{Email: "test@example.com"}), + }) + common.Must(err) + if resp == nil { + t.Fatal("nil response") + } +} + +func TestCommanderStats(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + cmdPort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&stats.Config{}), + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&statscmd.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + 1: { + Stats: &policy.Policy_Stats{ + UserUplink: true, + UserDownlink: true, + }, + }, + }, + System: &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + InboundUplink: true, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "vmess", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Level: 1, + Email: "test", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(cmdPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to create all servers", err) + } + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 10240*1024, time.Second*20)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithInsecure(), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + const name = "user>>>test>>>traffic>>>uplink" + sClient := statscmd.NewStatsServiceClient(cmdConn) + + sresp, err := sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: name, + Reset_: true, + }) + common.Must(err) + if r := cmp.Diff(sresp.Stat, &statscmd.Stat{ + Name: name, + Value: 10240 * 1024, + }, cmpopts.IgnoreUnexported(statscmd.Stat{})); r != "" { + t.Error(r) + } + + sresp, err = sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: name, + }) + common.Must(err) + if r := cmp.Diff(sresp.Stat, &statscmd.Stat{ + Name: name, + Value: 0, + }, cmpopts.IgnoreUnexported(statscmd.Stat{})); r != "" { + t.Error(r) + } + + sresp, err = sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: "inbound>>>vmess>>>traffic>>>uplink", + Reset_: true, + }) + common.Must(err) + if sresp.Stat.Value <= 10240*1024 { + t.Error("value < 10240*1024: ", sresp.Stat.Value) + } +} diff --git a/testing/scenarios/common.go b/testing/scenarios/common.go new file mode 100644 index 00000000..364e376b --- /dev/null +++ b/testing/scenarios/common.go @@ -0,0 +1,207 @@ +package scenarios + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "runtime" + "sync" + "syscall" + "time" + + "github.com/golang/protobuf/proto" + "github.com/xtls/xray-core/v1/app/dispatcher" + "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/common/log" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/retry" + "github.com/xtls/xray-core/v1/common/serial" + core "github.com/xtls/xray-core/v1/core" +) + +func xor(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'c' + } + return r +} + +func readFrom(conn net.Conn, timeout time.Duration, length int) []byte { + b := make([]byte, length) + deadline := time.Now().Add(timeout) + conn.SetReadDeadline(deadline) + n, err := io.ReadFull(conn, b[:length]) + if err != nil { + fmt.Println("Unexpected error from readFrom:", err) + } + return b[:n] +} + +func readFrom2(conn net.Conn, timeout time.Duration, length int) ([]byte, error) { + b := make([]byte, length) + deadline := time.Now().Add(timeout) + conn.SetReadDeadline(deadline) + n, err := io.ReadFull(conn, b[:length]) + if err != nil { + return nil, err + } + return b[:n], nil +} + +func InitializeServerConfigs(configs ...*core.Config) ([]*exec.Cmd, error) { + servers := make([]*exec.Cmd, 0, 10) + + for _, config := range configs { + server, err := InitializeServerConfig(config) + if err != nil { + CloseAllServers(servers) + return nil, err + } + servers = append(servers, server) + } + + time.Sleep(time.Second * 2) + + return servers, nil +} + +func InitializeServerConfig(config *core.Config) (*exec.Cmd, error) { + err := BuildXray() + if err != nil { + return nil, err + } + + config = withDefaultApps(config) + configBytes, err := proto.Marshal(config) + if err != nil { + return nil, err + } + proc := RunXrayProtobuf(configBytes) + + if err := proc.Start(); err != nil { + return nil, err + } + + return proc, nil +} + +var ( + testBinaryPath string + testBinaryPathGen sync.Once +) + +func genTestBinaryPath() { + testBinaryPathGen.Do(func() { + var tempDir string + common.Must(retry.Timed(5, 100).On(func() error { + dir, err := ioutil.TempDir("", "xray") + if err != nil { + return err + } + tempDir = dir + return nil + })) + file := filepath.Join(tempDir, "xray.test") + if runtime.GOOS == "windows" { + file += ".exe" + } + testBinaryPath = file + fmt.Printf("Generated binary path: %s\n", file) + }) +} + +func GetSourcePath() string { + return filepath.Join("example.com", "core", "main") +} + +func CloseAllServers(servers []*exec.Cmd) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "Closing all servers.", + }) + for _, server := range servers { + if runtime.GOOS == "windows" { + server.Process.Kill() + } else { + server.Process.Signal(syscall.SIGTERM) + } + } + for _, server := range servers { + server.Process.Wait() + } + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "All server closed.", + }) +} + +func withDefaultApps(config *core.Config) *core.Config { + config.App = append(config.App, serial.ToTypedMessage(&dispatcher.Config{})) + config.App = append(config.App, serial.ToTypedMessage(&proxyman.InboundConfig{})) + config.App = append(config.App, serial.ToTypedMessage(&proxyman.OutboundConfig{})) + return config +} + +func testTCPConn(port net.Port, payloadSize int, timeout time.Duration) func() error { + return func() error { + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(port), + }) + if err != nil { + return err + } + defer conn.Close() + + return testTCPConn2(conn, payloadSize, timeout)() + } +} + +func testUDPConn(port net.Port, payloadSize int, timeout time.Duration) func() error { + return func() error { + conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(port), + }) + if err != nil { + return err + } + defer conn.Close() + + return testTCPConn2(conn, payloadSize, timeout)() + } +} + +func testTCPConn2(conn net.Conn, payloadSize int, timeout time.Duration) func() error { + return func() error { + payload := make([]byte, payloadSize) + common.Must2(rand.Read(payload)) + + nBytes, err := conn.Write(payload) + if err != nil { + return err + } + if nBytes != len(payload) { + return errors.New("expect ", len(payload), " written, but actually ", nBytes) + } + + response, err := readFrom2(conn, timeout, payloadSize) + if err != nil { + return err + } + _ = response + + if r := bytes.Compare(response, xor(payload)); r != 0 { + return errors.New(r) + } + + return nil + } +} diff --git a/testing/scenarios/common_coverage.go b/testing/scenarios/common_coverage.go new file mode 100644 index 00000000..0082097d --- /dev/null +++ b/testing/scenarios/common_coverage.go @@ -0,0 +1,36 @@ +// +build coverage + +package scenarios + +import ( + "bytes" + "os" + "os/exec" + + "github.com/xtls/xray-core/v1/common/uuid" +) + +func BuildXray() error { + genTestBinaryPath() + if _, err := os.Stat(testBinaryPath); err == nil { + return nil + } + + cmd := exec.Command("go", "test", "-tags", "coverage coveragemain", "-coverpkg", "github.com/xtls/xray-core/v1/...", "-c", "-o", testBinaryPath, GetSourcePath()) + return cmd.Run() +} + +func RunXrayProtobuf(config []byte) *exec.Cmd { + genTestBinaryPath() + + covDir := os.Getenv("XRAY_COV") + os.MkdirAll(covDir, os.ModeDir) + randomID := uuid.New() + profile := randomID.String() + ".out" + proc := exec.Command(testBinaryPath, "-config=stdin:", "-format=pb", "-test.run", "TestRunMainForCoverage", "-test.coverprofile", profile, "-test.outputdir", covDir) + proc.Stdin = bytes.NewBuffer(config) + proc.Stderr = os.Stderr + proc.Stdout = os.Stdout + + return proc +} diff --git a/testing/scenarios/common_regular.go b/testing/scenarios/common_regular.go new file mode 100644 index 00000000..ac42bb06 --- /dev/null +++ b/testing/scenarios/common_regular.go @@ -0,0 +1,31 @@ +// +build !coverage + +package scenarios + +import ( + "bytes" + "fmt" + "os" + "os/exec" +) + +func BuildXray() error { + genTestBinaryPath() + if _, err := os.Stat(testBinaryPath); err == nil { + return nil + } + + fmt.Printf("Building Xray into path (%s)\n", testBinaryPath) + cmd := exec.Command("go", "build", "-o="+testBinaryPath, GetSourcePath()) + return cmd.Run() +} + +func RunXrayProtobuf(config []byte) *exec.Cmd { + genTestBinaryPath() + proc := exec.Command(testBinaryPath, "-config=stdin:", "-format=pb") + proc.Stdin = bytes.NewBuffer(config) + proc.Stderr = os.Stderr + proc.Stdout = os.Stdout + + return proc +} diff --git a/testing/scenarios/dns_test.go b/testing/scenarios/dns_test.go new file mode 100644 index 00000000..1b289bc9 --- /dev/null +++ b/testing/scenarios/dns_test.go @@ -0,0 +1,99 @@ +package scenarios + +import ( + "fmt" + "testing" + "time" + + "github.com/xtls/xray-core/v1/app/dns" + "github.com/xtls/xray-core/v1/app/proxyman" + "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" + "github.com/xtls/xray-core/v1/proxy/blackhole" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/socks" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + xproxy "golang.org/x/net/proxy" +) + +func TestResolveIP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dns.Config{ + Hosts: map[string]*net.IPOrDomain{ + "google.com": net.NewIPOrDomain(dest.Address), + }, + }), + serial.ToTypedMessage(&router.Config{ + DomainStrategy: router.Config_IpIfNonMatch, + Rule: []*router.RoutingRule{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{127, 0, 0, 0}, + Prefix: 8, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DomainStrategy: freedom.Config_USE_IP, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, serverPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", fmt.Sprintf("google.com:%d", dest.Port)) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} diff --git a/testing/scenarios/dokodemo_test.go b/testing/scenarios/dokodemo_test.go new file mode 100644 index 00000000..7a73ee5e --- /dev/null +++ b/testing/scenarios/dokodemo_test.go @@ -0,0 +1,209 @@ +package scenarios + +import ( + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" +) + +func TestDokodemoTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := uint32(tcp.PickPort()) + clientPortRange := uint32(5) + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: &net.PortRange{From: clientPort, To: clientPort + clientPortRange}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for port := clientPort; port <= clientPort+clientPortRange; port++ { + if err := testTCPConn(net.Port(port), 1024, time.Second*2)(); err != nil { + t.Error(err) + } + } +} + +func TestDokodemoUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := uint32(tcp.PickPort()) + clientPortRange := uint32(5) + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: &net.PortRange{From: clientPort, To: clientPort + clientPortRange}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for port := clientPort; port <= clientPort+clientPortRange; port++ { + errg.Go(testUDPConn(net.Port(port), 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/testing/scenarios/feature_test.go b/testing/scenarios/feature_test.go new file mode 100644 index 00000000..d49bb689 --- /dev/null +++ b/testing/scenarios/feature_test.go @@ -0,0 +1,736 @@ +package scenarios + +import ( + "context" + "io/ioutil" + "net/http" + "net/url" + "testing" + "time" + + "github.com/xtls/xray-core/v1/app/dispatcher" + "github.com/xtls/xray-core/v1/app/log" + "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/app/router" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/blackhole" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + v2http "github.com/xtls/xray-core/v1/proxy/http" + "github.com/xtls/xray-core/v1/proxy/socks" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" + "github.com/xtls/xray-core/v1/transport/internet" + xproxy "golang.org/x/net/proxy" +) + +func TestPassiveConnection(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + SendFirst: []byte("send first"), + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(serverPort), + }) + common.Must(err) + + { + response := make([]byte, 1024) + nBytes, err := conn.Read(response) + common.Must(err) + if string(response[:nBytes]) != "send first" { + t.Error("unexpected first response: ", string(response[:nBytes])) + } + } + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestProxy(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverUserID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + proxyUserID := protocol.NewID(uuid.New()) + proxyPort := tcp.PickPort() + proxyConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(proxyPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + ProxySettings: &internet.ProxyConfig{ + Tag: "proxy", + }, + }), + }, + { + Tag: "proxy", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, proxyConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestProxyOverKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverUserID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + proxyUserID := protocol.NewID(uuid.New()) + proxyPort := tcp.PickPort() + proxyConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(proxyPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + }, + }), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + ProxySettings: &internet.ProxyConfig{ + Tag: "proxy", + }, + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + }, + }), + }, + { + Tag: "proxy", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, proxyConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestBlackhole(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + tcpServer2 := tcp.Server{ + MsgProcessor: xor, + } + dest2, err := tcpServer2.Start() + common.Must(err) + defer tcpServer2.Close() + + serverPort := tcp.PickPort() + serverPort2 := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort2), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest2.Address), + Port: uint32(dest2.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "blocked", + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "blocked", + }, + PortRange: net.SinglePortRange(dest2.Port), + }, + }, + }), + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(serverPort2, 1024, time.Second*5)(); err == nil { + t.Error("nil error") + } +} + +func TestForward(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(dest.Port), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, serverPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", "google.com:80") + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} + +func TestUDPConnection(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + + time.Sleep(20 * time.Second) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestDomainSniffing(t *testing.T) { + sniffingPort := tcp.PickPort() + httpPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "snif", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(sniffingPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + DomainOverride: []proxyman.KnownProtocols{ + proxyman.KnownProtocols_TLS, + }, + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: 443, + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + { + Tag: "http", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(httpPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "redir", + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(sniffingPort), + }, + }, + }), + }, + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + InboundTag: []string{"snif"}, + }, { + TargetTag: &router.RoutingRule_Tag{ + Tag: "redir", + }, + InboundTag: []string{"http"}, + }, + }, + }), + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + httpPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("https://www.github.com/") + common.Must(err) + if resp.StatusCode != 200 { + t.Error("unexpected status code: ", resp.StatusCode) + } + common.Must(resp.Write(ioutil.Discard)) + } +} + +func TestDialXray(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Inbound: []*core.InboundHandlerConfig{}, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + client, err := core.New(clientConfig) + common.Must(err) + + conn, err := core.Dial(context.Background(), client, dest) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} diff --git a/testing/scenarios/http_test.go b/testing/scenarios/http_test.go new file mode 100644 index 00000000..d89b31b8 --- /dev/null +++ b/testing/scenarios/http_test.go @@ -0,0 +1,371 @@ +package scenarios + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "io/ioutil" + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "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/core" + "github.com/xtls/xray-core/v1/proxy/freedom" + v2http "github.com/xtls/xray-core/v1/proxy/http" + v2httptest "github.com/xtls/xray-core/v1/testing/servers/http" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func TestHttpConformance(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: make(map[string]http.HandlerFunc), + } + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("http://127.0.0.1:" + httpServerPort.String()) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := ioutil.ReadAll(resp.Body) + common.Must(err) + if string(content) != "Home" { + t.Fatal("body: ", string(content)) + } + } +} + +func TestHttpError(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: func(msg []byte) []byte { + return []byte{} + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + time.AfterFunc(time.Second*2, func() { + tcpServer.ShouldClose = true + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("http://127.0.0.1:" + dest.Port.String()) + common.Must(err) + if resp.StatusCode != 503 { + t.Error("status: ", resp.StatusCode) + } + } +} + +func TestHTTPConnectMethod(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + payload := make([]byte, 1024*64) + common.Must2(rand.Read(payload)) + + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "Connect", "http://"+dest.NetAddr()+"/", bytes.NewReader(payload)) + req.Header.Set("X-a", "b") + req.Header.Set("X-b", "d") + common.Must(err) + + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content := make([]byte, len(payload)) + common.Must2(io.ReadFull(resp.Body, content)) + if r := cmp.Diff(content, xor(payload)); r != "" { + t.Fatal(r) + } + } +} + +func TestHttpPost(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: map[string]http.HandlerFunc{ + "/testpost": func(w http.ResponseWriter, r *http.Request) { + payload, err := buf.ReadAllToBytes(r.Body) + r.Body.Close() + + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Unable to read all payload")) + return + } + payload = xor(payload) + w.Write(payload) + }, + }, + } + + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + payload := make([]byte, 1024*64) + common.Must2(rand.Read(payload)) + + resp, err := client.Post("http://127.0.0.1:"+httpServerPort.String()+"/testpost", "application/x-www-form-urlencoded", bytes.NewReader(payload)) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := ioutil.ReadAll(resp.Body) + common.Must(err) + if r := cmp.Diff(content, xor(payload)); r != "" { + t.Fatal(r) + } + } +} + +func setProxyBasicAuth(req *http.Request, user, pass string) { + req.SetBasicAuth(user, pass) + req.Header.Set("Proxy-Authorization", req.Header.Get("Authorization")) + req.Header.Del("Authorization") +} + +func TestHttpBasicAuth(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: make(map[string]http.HandlerFunc), + } + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{ + Accounts: map[string]string{ + "a": "b", + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + { + resp, err := client.Get("http://127.0.0.1:" + httpServerPort.String()) + common.Must(err) + if resp.StatusCode != 407 { + t.Fatal("status: ", resp.StatusCode) + } + } + + { + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:"+httpServerPort.String(), nil) + common.Must(err) + + setProxyBasicAuth(req, "a", "c") + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 407 { + t.Fatal("status: ", resp.StatusCode) + } + } + + { + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:"+httpServerPort.String(), nil) + common.Must(err) + + setProxyBasicAuth(req, "a", "b") + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := ioutil.ReadAll(resp.Body) + common.Must(err) + if string(content) != "Home" { + t.Fatal("body: ", string(content)) + } + } + } +} diff --git a/testing/scenarios/policy_test.go b/testing/scenarios/policy_test.go new file mode 100644 index 00000000..dcf807d8 --- /dev/null +++ b/testing/scenarios/policy_test.go @@ -0,0 +1,267 @@ +package scenarios + +import ( + "io" + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/policy" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func startQuickClosingTCPServer() (net.Listener, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + go func() { + for { + conn, err := listener.Accept() + if err != nil { + break + } + b := make([]byte, 1024) + conn.Read(b) + conn.Close() + } + }() + return listener, nil +} + +func TestVMessClosing(t *testing.T) { + tcpServer, err := startQuickClosingTCPServer() + common.Must(err) + defer tcpServer.Close() + + dest := net.DestinationFromAddr(tcpServer.Addr()) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != io.EOF { + t.Error(err) + } +} + +func TestZeroBuffer(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + Buffer: &policy.Policy_Buffer{ + Connection: 0, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/testing/scenarios/reverse_test.go b/testing/scenarios/reverse_test.go new file mode 100644 index 00000000..50f22733 --- /dev/null +++ b/testing/scenarios/reverse_test.go @@ -0,0 +1,395 @@ +package scenarios + +import ( + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/policy" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/app/reverse" + "github.com/xtls/xray-core/v1/app/router" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/blackhole" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" +) + +func TestReverseProxy(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + externalPort := tcp.PickPort() + reversePort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + { + Tag: "portal", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + { + InboundTag: []string{"external"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "external", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(externalPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(reversePort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + { + Tag: "bridge", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "reverse", + }, + }, + { + InboundTag: []string{"bridge"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "freedom", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "freedom", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "reverse", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 32; i++ { + errg.Go(testTCPConn(externalPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestReverseProxyLongRunning(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + externalPort := tcp.PickPort() + reversePort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Warning, + ErrorLogType: log.LogType_Console, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + serial.ToTypedMessage(&reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + { + Tag: "portal", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + { + InboundTag: []string{"external"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "external", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(externalPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(reversePort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Warning, + ErrorLogType: log.LogType_Console, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + serial.ToTypedMessage(&reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + { + Tag: "bridge", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "reverse", + }, + }, + { + InboundTag: []string{"bridge"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "freedom", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "freedom", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "reverse", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + for i := 0; i < 4096; i++ { + if err := testTCPConn(externalPort, 1024, time.Second*20)(); err != nil { + t.Error(err) + } + } +} diff --git a/testing/scenarios/shadowsocks_test.go b/testing/scenarios/shadowsocks_test.go new file mode 100644 index 00000000..db2f0b23 --- /dev/null +++ b/testing/scenarios/shadowsocks_test.go @@ -0,0 +1,824 @@ +package scenarios + +import ( + "crypto/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/errors" + clog "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/serial" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/shadowsocks" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" +) + +func TestShadowsocksAES256TCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_256_CFB, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestShadowsocksAES128UDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_128_CFB, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(func() error { + conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(clientPort), + }) + if err != nil { + return err + } + defer conn.Close() + + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + nBytes, err := conn.Write(payload) + if err != nil { + return err + } + if nBytes != len(payload) { + return errors.New("expect ", len(payload), " written, but actually ", nBytes) + } + + response := readFrom(conn, time.Second*5, 1024) + if r := cmp.Diff(response, xor(payload)); r != "" { + return errors.New(r) + } + return nil + }) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestShadowsocksChacha20TCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_CHACHA20_IETF, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksChacha20Poly1305TCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_CHACHA20_POLY1305, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES256GCMTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_256_GCM, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES128GCMUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_128_GCM, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES128GCMUDPMux(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_128_GCM, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 8, + }, + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksNone(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_NONE, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + User: &protocol.User{ + Account: account, + Level: 1, + }, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: account, + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } +} diff --git a/testing/scenarios/socks_test.go b/testing/scenarios/socks_test.go new file mode 100644 index 00000000..913291d2 --- /dev/null +++ b/testing/scenarios/socks_test.go @@ -0,0 +1,371 @@ +package scenarios + +import ( + "testing" + "time" + + xproxy "golang.org/x/net/proxy" + socks4 "h12.io/socks" + + "github.com/xtls/xray-core/v1/app/proxyman" + "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/protocol" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/blackhole" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/socks" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" +) + +func TestSocksBridgeTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestSocksBridageUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: true, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP, net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestSocksBridageUDPWithRouting(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "out", + }, + InboundTag: []string{"socks"}, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "socks", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: true, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + { + Tag: "out", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP, net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestSocksConformanceMod(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + authPort := tcp.PickPort() + noAuthPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(authPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(noAuthPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + authDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, authPort).NetAddr(), &xproxy.Auth{User: "Test Account", Password: "Test Password"}, xproxy.Direct) + common.Must(err) + conn, err := authDialer.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + dialer := socks4.Dial("socks4://" + net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr()) + conn, err := dialer("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + dialer := socks4.Dial("socks4://" + net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr()) + conn, err := dialer("tcp", net.TCPDestination(net.LocalHostIP, tcpServer.Port).NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go new file mode 100644 index 00000000..d2531c1f --- /dev/null +++ b/testing/scenarios/tls_test.go @@ -0,0 +1,591 @@ +package scenarios + +import ( + "crypto/x509" + "runtime" + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/common/uuid" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/http" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/websocket" +) + +func TestSimpleTLSConnection(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + AllowInsecure: true, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Fatal(err) + } +} + +func TestAutoIssuingCertificate(t *testing.T) { + if runtime.GOOS == "windows" { + // Not supported on Windows yet. + return + } + + if runtime.GOARCH == "arm64" { + return + } + + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + caCert, err := cert.Generate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment|x509.KeyUsageCertSign)) + common.Must(err) + certPEM, keyPEM := caCert.ToPEM() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{{ + Certificate: certPEM, + Key: keyPEM, + Usage: tls.Certificate_AUTHORITY_ISSUE, + }}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + ServerName: "example.com", + Certificate: []*tls.Certificate{{ + Certificate: certPEM, + Usage: tls.Certificate_AUTHORITY_VERIFY, + }}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for i := 0; i < 10; i++ { + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } + } +} + +func TestTLSOverKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + AllowInsecure: true, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestTLSOverWebSocket(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_WebSocket, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_WebSocket, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_WebSocket, + Settings: serial.ToTypedMessage(&websocket.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + AllowInsecure: true, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestHTTP2(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_HTTP, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_HTTP, + Settings: serial.ToTypedMessage(&http.Config{ + Host: []string{"example.com"}, + Path: "/testpath", + }), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_HTTP, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_HTTP, + Settings: serial.ToTypedMessage(&http.Config{ + Host: []string{"example.com"}, + Path: "/testpath", + }), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + AllowInsecure: true, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/testing/scenarios/transport_test.go b/testing/scenarios/transport_test.go new file mode 100644 index 00000000..b46a4f65 --- /dev/null +++ b/testing/scenarios/transport_test.go @@ -0,0 +1,385 @@ +package scenarios + +import ( + "os" + "runtime" + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/domainsocket" + "github.com/xtls/xray-core/v1/transport/internet/headers/http" + "github.com/xtls/xray-core/v1/transport/internet/headers/wechat" + "github.com/xtls/xray-core/v1/transport/internet/quic" + tcptransport "github.com/xtls/xray-core/v1/transport/internet/tcp" +) + +func TestHTTPConnectionHeader(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_TCP, + Settings: serial.ToTypedMessage(&tcptransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_TCP, + Settings: serial.ToTypedMessage(&tcptransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestDomainSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Not supported on windows") + return + } + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + const dsPath = "/tmp/ds_scenario" + os.Remove(dsPath) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_DomainSocket, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_DomainSocket, + Settings: serial.ToTypedMessage(&domainsocket.Config{ + Path: dsPath, + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_DomainSocket, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_DomainSocket, + Settings: serial.ToTypedMessage(&domainsocket.Config{ + Path: dsPath, + }), + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestVMessQuic(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "quic", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "quic", + Settings: serial.ToTypedMessage(&quic.Config{ + Header: serial.ToTypedMessage(&wechat.VideoConfig{}), + Security: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "quic", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "quic", + Settings: serial.ToTypedMessage(&quic.Config{ + Header: serial.ToTypedMessage(&wechat.VideoConfig{}), + Security: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/testing/scenarios/vmess_test.go b/testing/scenarios/vmess_test.go new file mode 100644 index 00000000..4c1772dc --- /dev/null +++ b/testing/scenarios/vmess_test.go @@ -0,0 +1,1185 @@ +package scenarios + +import ( + "os" + "testing" + "time" + + "github.com/xtls/xray-core/v1/app/log" + "github.com/xtls/xray-core/v1/app/proxyman" + "github.com/xtls/xray-core/v1/common" + clog "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/serial" + "github.com/xtls/xray-core/v1/common/uuid" + core "github.com/xtls/xray-core/v1/core" + "github.com/xtls/xray-core/v1/proxy/dokodemo" + "github.com/xtls/xray-core/v1/proxy/freedom" + "github.com/xtls/xray-core/v1/proxy/vmess" + "github.com/xtls/xray-core/v1/proxy/vmess/inbound" + "github.com/xtls/xray-core/v1/proxy/vmess/outbound" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/testing/servers/udp" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/kcp" + "golang.org/x/sync/errgroup" +) + +func TestVMessDynamicPort(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + Detour: &inbound.DetourConfig{ + To: "detour", + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: &net.PortRange{ + From: uint32(serverPort + 1), + To: uint32(serverPort + 100), + }, + Listen: net.NewIPOrDomain(net.LocalHostIP), + AllocationStrategy: &proxyman.AllocationStrategy{ + Type: proxyman.AllocationStrategy_Random, + Concurrency: &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ + Value: 2, + }, + Refresh: &proxyman.AllocationStrategy_AllocationStrategyRefresh{ + Value: 5, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{}), + Tag: "detour", + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for i := 0; i < 10; i++ { + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } + } +} + +func TestVMessGCM(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMReadv(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + const envName = "XRAY_BUF_READV" + common.Must(os.Setenv(envName, "enable")) + defer os.Unsetenv(envName) + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessChacha20(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_CHACHA20_POLY1305, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessNone(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Minute*2)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessKCPLarge(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_MKCP, + Settings: serial.ToTypedMessage(&kcp.Config{ + ReadBuffer: &kcp.ReadBuffer{ + Size: 512 * 1024, + }, + WriteBuffer: &kcp.WriteBuffer{ + Size: 512 * 1024, + }, + UplinkCapacity: &kcp.UplinkCapacity{ + Value: 20, + }, + DownlinkCapacity: &kcp.DownlinkCapacity{ + Value: 20, + }, + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + Protocol: internet.TransportProtocol_MKCP, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_MKCP, + Settings: serial.ToTypedMessage(&kcp.Config{ + ReadBuffer: &kcp.ReadBuffer{ + Size: 512 * 1024, + }, + WriteBuffer: &kcp.WriteBuffer{ + Size: 512 * 1024, + }, + UplinkCapacity: &kcp.UplinkCapacity{ + Value: 20, + }, + DownlinkCapacity: &kcp.DownlinkCapacity{ + Value: 20, + }, + }), + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + var errg errgroup.Group + for i := 0; i < 2; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Minute*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } + + defer func() { + <-time.After(5 * time.Second) + CloseAllServers(servers) + }() +} + +func TestVMessGCMMux(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 4, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for range "abcd" { + var errg errgroup.Group + for i := 0; i < 16; i++ { + errg.Go(testTCPConn(clientPort, 10240, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + } +} + +func TestVMessGCMMuxUDP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + udpServer := udp.Server{ + MsgProcessor: xor, + } + udpDest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientUDPPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientUDPPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(udpDest.Address), + Port: uint32(udpDest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 4, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 64, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + for range "abcd" { + var errg errgroup.Group + for i := 0; i < 16; i++ { + errg.Go(testTCPConn(clientPort, 10240, time.Second*20)) + errg.Go(testUDPConn(clientUDPPort, 1024, time.Second*10)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } + time.Sleep(time.Second) + } + + defer func() { + <-time.After(5 * time.Second) + CloseAllServers(servers) + }() +} diff --git a/testing/servers/http/http.go b/testing/servers/http/http.go new file mode 100644 index 00000000..8d76809e --- /dev/null +++ b/testing/servers/http/http.go @@ -0,0 +1,40 @@ +package tcp + +import ( + "net/http" + + "github.com/xtls/xray-core/v1/common/net" +) + +type Server struct { + Port net.Port + PathHandler map[string]http.HandlerFunc + server *http.Server +} + +func (s *Server) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/" { + resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + resp.WriteHeader(http.StatusOK) + resp.Write([]byte("Home")) + return + } + + handler, found := s.PathHandler[req.URL.Path] + if found { + handler(resp, req) + } +} + +func (s *Server) Start() (net.Destination, error) { + s.server = &http.Server{ + Addr: "127.0.0.1:" + s.Port.String(), + Handler: s, + } + go s.server.ListenAndServe() + return net.TCPDestination(net.LocalHostIP, s.Port), nil +} + +func (s *Server) Close() error { + return s.server.Close() +} diff --git a/testing/servers/tcp/port.go b/testing/servers/tcp/port.go new file mode 100644 index 00000000..fe99ad24 --- /dev/null +++ b/testing/servers/tcp/port.go @@ -0,0 +1,16 @@ +package tcp + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" +) + +// PickPort returns an unused TCP port in the system. The port returned is highly likely to be unused, but not guaranteed. +func PickPort() net.Port { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + common.Must(err) + defer listener.Close() + + addr := listener.Addr().(*net.TCPAddr) + return net.Port(addr.Port) +} diff --git a/testing/servers/tcp/tcp.go b/testing/servers/tcp/tcp.go new file mode 100644 index 00000000..04443c58 --- /dev/null +++ b/testing/servers/tcp/tcp.go @@ -0,0 +1,110 @@ +package tcp + +import ( + "context" + "fmt" + "io" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +type Server struct { + Port net.Port + MsgProcessor func(msg []byte) []byte + ShouldClose bool + SendFirst []byte + Listen net.Address + listener net.Listener +} + +func (server *Server) Start() (net.Destination, error) { + return server.StartContext(context.Background(), nil) +} + +func (server *Server) StartContext(ctx context.Context, sockopt *internet.SocketConfig) (net.Destination, error) { + listenerAddr := server.Listen + if listenerAddr == nil { + listenerAddr = net.LocalHostIP + } + listener, err := internet.ListenSystem(ctx, &net.TCPAddr{ + IP: listenerAddr.IP(), + Port: int(server.Port), + }, sockopt) + if err != nil { + return net.Destination{}, err + } + + localAddr := listener.Addr().(*net.TCPAddr) + server.Port = net.Port(localAddr.Port) + server.listener = listener + go server.acceptConnections(listener.(*net.TCPListener)) + + return net.TCPDestination(net.IPAddress(localAddr.IP), net.Port(localAddr.Port)), nil +} + +func (server *Server) acceptConnections(listener *net.TCPListener) { + for { + conn, err := listener.Accept() + if err != nil { + fmt.Printf("Failed accept TCP connection: %v\n", err) + return + } + + go server.handleConnection(conn) + } +} + +func (server *Server) handleConnection(conn net.Conn) { + if len(server.SendFirst) > 0 { + conn.Write(server.SendFirst) + } + + pReader, pWriter := pipe.New(pipe.WithoutSizeLimit()) + err := task.Run(context.Background(), func() error { + defer pWriter.Close() + + for { + b := buf.New() + if _, err := b.ReadFrom(conn); err != nil { + if err == io.EOF { + return nil + } + return err + } + copy(b.Bytes(), server.MsgProcessor(b.Bytes())) + if err := pWriter.WriteMultiBuffer(buf.MultiBuffer{b}); err != nil { + return err + } + } + }, func() error { + defer pReader.Interrupt() + + w := buf.NewWriter(conn) + for { + mb, err := pReader.ReadMultiBuffer() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if err := w.WriteMultiBuffer(mb); err != nil { + return err + } + } + }) + + if err != nil { + fmt.Println("failed to transfer data: ", err.Error()) + } + + conn.Close() +} + +func (server *Server) Close() error { + return server.listener.Close() +} diff --git a/testing/servers/udp/port.go b/testing/servers/udp/port.go new file mode 100644 index 00000000..51e3559e --- /dev/null +++ b/testing/servers/udp/port.go @@ -0,0 +1,19 @@ +package udp + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" +) + +// PickPort returns an unused UDP port in the system. The port returned is highly likely to be unused, but not guaranteed. +func PickPort() net.Port { + conn, err := net.ListenUDP("udp4", &net.UDPAddr{ + IP: net.LocalHostIP.IP(), + Port: 0, + }) + common.Must(err) + defer conn.Close() + + addr := conn.LocalAddr().(*net.UDPAddr) + return net.Port(addr.Port) +} diff --git a/testing/servers/udp/udp.go b/testing/servers/udp/udp.go new file mode 100644 index 00000000..9e9f0671 --- /dev/null +++ b/testing/servers/udp/udp.go @@ -0,0 +1,54 @@ +package udp + +import ( + "fmt" + + "github.com/xtls/xray-core/v1/common/net" +) + +type Server struct { + Port net.Port + MsgProcessor func(msg []byte) []byte + accepting bool + conn *net.UDPConn +} + +func (server *Server) Start() (net.Destination, error) { + conn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(server.Port), + Zone: "", + }) + if err != nil { + return net.Destination{}, err + } + server.Port = net.Port(conn.LocalAddr().(*net.UDPAddr).Port) + fmt.Println("UDP server started on port ", server.Port) + + server.conn = conn + go server.handleConnection(conn) + localAddr := conn.LocalAddr().(*net.UDPAddr) + return net.UDPDestination(net.IPAddress(localAddr.IP), net.Port(localAddr.Port)), nil +} + +func (server *Server) handleConnection(conn *net.UDPConn) { + server.accepting = true + for server.accepting { + buffer := make([]byte, 2*1024) + nBytes, addr, err := conn.ReadFromUDP(buffer) + if err != nil { + fmt.Printf("Failed to read from UDP: %v\n", err) + continue + } + + response := server.MsgProcessor(buffer[:nBytes]) + if _, err := conn.WriteToUDP(response, addr); err != nil { + fmt.Println("Failed to write to UDP: ", err.Error()) + } + } +} + +func (server *Server) Close() error { + server.accepting = false + return server.conn.Close() +} diff --git a/transport/config.go b/transport/config.go new file mode 100644 index 00000000..b25bb537 --- /dev/null +++ b/transport/config.go @@ -0,0 +1,13 @@ +package transport + +import ( + "github.com/xtls/xray-core/v1/transport/internet" +) + +// Apply applies this Config. +func (c *Config) Apply() error { + if c == nil { + return nil + } + return internet.ApplyGlobalTransportSettings(c.TransportSettings) +} diff --git a/transport/config.pb.go b/transport/config.pb.go new file mode 100644 index 00000000..2918d120 --- /dev/null +++ b/transport/config.pb.go @@ -0,0 +1,165 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/config.proto + +package transport + +import ( + proto "github.com/golang/protobuf/proto" + internet "github.com/xtls/xray-core/v1/transport/internet" + 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 + +// Global transport settings. This affects all type of connections that go +// through Xray. Deprecated. Use each settings in StreamConfig. +// +// Deprecated: Do not use. +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TransportSettings []*internet.TransportConfig `protobuf:"bytes,1,rep,name=transport_settings,json=transportSettings,proto3" json:"transport_settings,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_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_transport_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_transport_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetTransportSettings() []*internet.TransportConfig { + if x != nil { + return x.TransportSettings + } + return nil +} + +var File_transport_config_proto protoreflect.FileDescriptor + +var file_transport_config_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x1f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x65, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x57, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x3a, 0x02, 0x18, 0x01, + 0x42, 0x4f, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x01, 0x5a, 0x26, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0xaa, 0x02, 0x0e, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_config_proto_rawDescOnce sync.Once + file_transport_config_proto_rawDescData = file_transport_config_proto_rawDesc +) + +func file_transport_config_proto_rawDescGZIP() []byte { + file_transport_config_proto_rawDescOnce.Do(func() { + file_transport_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_config_proto_rawDescData) + }) + return file_transport_config_proto_rawDescData +} + +var file_transport_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.Config + (*internet.TransportConfig)(nil), // 1: xray.transport.internet.TransportConfig +} +var file_transport_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.Config.transport_settings:type_name -> xray.transport.internet.TransportConfig + 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_transport_config_proto_init() } +func file_transport_config_proto_init() { + if File_transport_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_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_transport_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_config_proto_goTypes, + DependencyIndexes: file_transport_config_proto_depIdxs, + MessageInfos: file_transport_config_proto_msgTypes, + }.Build() + File_transport_config_proto = out.File + file_transport_config_proto_rawDesc = nil + file_transport_config_proto_goTypes = nil + file_transport_config_proto_depIdxs = nil +} diff --git a/transport/config.proto b/transport/config.proto new file mode 100644 index 00000000..b71a9914 --- /dev/null +++ b/transport/config.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.transport; +option csharp_namespace = "Xray.Transport"; +option go_package = "github.com/xtls/xray-core/v1/transport"; +option java_package = "com.xray.transport"; +option java_multiple_files = true; + +import "transport/internet/config.proto"; + +// Global transport settings. This affects all type of connections that go +// through Xray. Deprecated. Use each settings in StreamConfig. +message Config { + option deprecated = true; + repeated xray.transport.internet.TransportConfig transport_settings = 1; +} diff --git a/transport/internet/config.go b/transport/internet/config.go new file mode 100644 index 00000000..74db07c7 --- /dev/null +++ b/transport/internet/config.go @@ -0,0 +1,124 @@ +package internet + +import ( + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/features" +) + +type ConfigCreator func() interface{} + +var ( + globalTransportConfigCreatorCache = make(map[string]ConfigCreator) + globalTransportSettings []*TransportConfig +) + +const unknownProtocol = "unknown" + +func transportProtocolToString(protocol TransportProtocol) string { + switch protocol { + case TransportProtocol_TCP: + return "tcp" + case TransportProtocol_UDP: + return "udp" + case TransportProtocol_HTTP: + return "http" + case TransportProtocol_MKCP: + return "mkcp" + case TransportProtocol_WebSocket: + return "websocket" + case TransportProtocol_DomainSocket: + return "domainsocket" + default: + return unknownProtocol + } +} + +func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error { + if _, found := globalTransportConfigCreatorCache[name]; found { + return newError("protocol ", name, " is already registered").AtError() + } + globalTransportConfigCreatorCache[name] = creator + return nil +} + +func CreateTransportConfig(name string) (interface{}, error) { + creator, ok := globalTransportConfigCreatorCache[name] + if !ok { + return nil, newError("unknown transport protocol: ", name) + } + return creator(), nil +} + +func (c *TransportConfig) GetTypedSettings() (interface{}, error) { + return c.Settings.GetInstance() +} + +func (c *TransportConfig) GetUnifiedProtocolName() string { + if len(c.ProtocolName) > 0 { + return c.ProtocolName + } + + return transportProtocolToString(c.Protocol) +} + +func (c *StreamConfig) GetEffectiveProtocol() string { + if c == nil { + return "tcp" + } + + if len(c.ProtocolName) > 0 { + return c.ProtocolName + } + + return transportProtocolToString(c.Protocol) +} + +func (c *StreamConfig) GetEffectiveTransportSettings() (interface{}, error) { + protocol := c.GetEffectiveProtocol() + return c.GetTransportSettingsFor(protocol) +} + +func (c *StreamConfig) GetTransportSettingsFor(protocol string) (interface{}, error) { + if c != nil { + for _, settings := range c.TransportSettings { + if settings.GetUnifiedProtocolName() == protocol { + return settings.GetTypedSettings() + } + } + } + + for _, settings := range globalTransportSettings { + if settings.GetUnifiedProtocolName() == protocol { + return settings.GetTypedSettings() + } + } + + return CreateTransportConfig(protocol) +} + +func (c *StreamConfig) GetEffectiveSecuritySettings() (interface{}, error) { + for _, settings := range c.SecuritySettings { + if settings.Type == c.SecurityType { + return settings.GetInstance() + } + } + return serial.GetInstance(c.SecurityType) +} + +func (c *StreamConfig) HasSecuritySettings() bool { + return len(c.SecurityType) > 0 +} + +func ApplyGlobalTransportSettings(settings []*TransportConfig) error { + features.PrintDeprecatedFeatureWarning("global transport settings") + globalTransportSettings = settings + return nil +} + +func (c *ProxyConfig) HasTag() bool { + return c != nil && len(c.Tag) > 0 +} + +func (m SocketConfig_TProxyMode) IsEnabled() bool { + return m != SocketConfig_Off +} diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go new file mode 100644 index 00000000..e80031fc --- /dev/null +++ b/transport/internet/config.pb.go @@ -0,0 +1,711 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/config.proto + +package internet + +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 + +type TransportProtocol int32 + +const ( + TransportProtocol_TCP TransportProtocol = 0 + TransportProtocol_UDP TransportProtocol = 1 + TransportProtocol_MKCP TransportProtocol = 2 + TransportProtocol_WebSocket TransportProtocol = 3 + TransportProtocol_HTTP TransportProtocol = 4 + TransportProtocol_DomainSocket TransportProtocol = 5 +) + +// Enum value maps for TransportProtocol. +var ( + TransportProtocol_name = map[int32]string{ + 0: "TCP", + 1: "UDP", + 2: "MKCP", + 3: "WebSocket", + 4: "HTTP", + 5: "DomainSocket", + } + TransportProtocol_value = map[string]int32{ + "TCP": 0, + "UDP": 1, + "MKCP": 2, + "WebSocket": 3, + "HTTP": 4, + "DomainSocket": 5, + } +) + +func (x TransportProtocol) Enum() *TransportProtocol { + p := new(TransportProtocol) + *p = x + return p +} + +func (x TransportProtocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransportProtocol) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[0].Descriptor() +} + +func (TransportProtocol) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[0] +} + +func (x TransportProtocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransportProtocol.Descriptor instead. +func (TransportProtocol) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{0} +} + +type SocketConfig_TCPFastOpenState int32 + +const ( + // AsIs is to leave the current TFO state as is, unmodified. + SocketConfig_AsIs SocketConfig_TCPFastOpenState = 0 + // Enable is for enabling TFO explictly. + SocketConfig_Enable SocketConfig_TCPFastOpenState = 1 + // Disable is for disabling TFO explictly. + SocketConfig_Disable SocketConfig_TCPFastOpenState = 2 +) + +// Enum value maps for SocketConfig_TCPFastOpenState. +var ( + SocketConfig_TCPFastOpenState_name = map[int32]string{ + 0: "AsIs", + 1: "Enable", + 2: "Disable", + } + SocketConfig_TCPFastOpenState_value = map[string]int32{ + "AsIs": 0, + "Enable": 1, + "Disable": 2, + } +) + +func (x SocketConfig_TCPFastOpenState) Enum() *SocketConfig_TCPFastOpenState { + p := new(SocketConfig_TCPFastOpenState) + *p = x + return p +} + +func (x SocketConfig_TCPFastOpenState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SocketConfig_TCPFastOpenState) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[1].Descriptor() +} + +func (SocketConfig_TCPFastOpenState) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[1] +} + +func (x SocketConfig_TCPFastOpenState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SocketConfig_TCPFastOpenState.Descriptor instead. +func (SocketConfig_TCPFastOpenState) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{3, 0} +} + +type SocketConfig_TProxyMode int32 + +const ( + // TProxy is off. + SocketConfig_Off SocketConfig_TProxyMode = 0 + // TProxy mode. + SocketConfig_TProxy SocketConfig_TProxyMode = 1 + // Redirect mode. + SocketConfig_Redirect SocketConfig_TProxyMode = 2 +) + +// Enum value maps for SocketConfig_TProxyMode. +var ( + SocketConfig_TProxyMode_name = map[int32]string{ + 0: "Off", + 1: "TProxy", + 2: "Redirect", + } + SocketConfig_TProxyMode_value = map[string]int32{ + "Off": 0, + "TProxy": 1, + "Redirect": 2, + } +) + +func (x SocketConfig_TProxyMode) Enum() *SocketConfig_TProxyMode { + p := new(SocketConfig_TProxyMode) + *p = x + return p +} + +func (x SocketConfig_TProxyMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SocketConfig_TProxyMode) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[2].Descriptor() +} + +func (SocketConfig_TProxyMode) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[2] +} + +func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead. +func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{3, 1} +} + +type TransportConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Type of network that this settings supports. + // Deprecated. Use the string form below. + // + // Deprecated: Do not use. + Protocol TransportProtocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=xray.transport.internet.TransportProtocol" json:"protocol,omitempty"` + // Type of network that this settings supports. + ProtocolName string `protobuf:"bytes,3,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` + // Specific settings. Must be of the transports. + Settings *serial.TypedMessage `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` +} + +func (x *TransportConfig) Reset() { + *x = TransportConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransportConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransportConfig) ProtoMessage() {} + +func (x *TransportConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_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 TransportConfig.ProtoReflect.Descriptor instead. +func (*TransportConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Do not use. +func (x *TransportConfig) GetProtocol() TransportProtocol { + if x != nil { + return x.Protocol + } + return TransportProtocol_TCP +} + +func (x *TransportConfig) GetProtocolName() string { + if x != nil { + return x.ProtocolName + } + return "" +} + +func (x *TransportConfig) GetSettings() *serial.TypedMessage { + if x != nil { + return x.Settings + } + return nil +} + +type StreamConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Effective network. Deprecated. Use the string form below. + // + // Deprecated: Do not use. + Protocol TransportProtocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=xray.transport.internet.TransportProtocol" json:"protocol,omitempty"` + // Effective network. + ProtocolName string `protobuf:"bytes,5,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` + TransportSettings []*TransportConfig `protobuf:"bytes,2,rep,name=transport_settings,json=transportSettings,proto3" json:"transport_settings,omitempty"` + // Type of security. Must be a message name of the settings proto. + SecurityType string `protobuf:"bytes,3,opt,name=security_type,json=securityType,proto3" json:"security_type,omitempty"` + // Settings for transport security. For now the only choice is TLS. + SecuritySettings []*serial.TypedMessage `protobuf:"bytes,4,rep,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + SocketSettings *SocketConfig `protobuf:"bytes,6,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"` +} + +func (x *StreamConfig) Reset() { + *x = StreamConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StreamConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamConfig) ProtoMessage() {} + +func (x *StreamConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_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 StreamConfig.ProtoReflect.Descriptor instead. +func (*StreamConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{1} +} + +// Deprecated: Do not use. +func (x *StreamConfig) GetProtocol() TransportProtocol { + if x != nil { + return x.Protocol + } + return TransportProtocol_TCP +} + +func (x *StreamConfig) GetProtocolName() string { + if x != nil { + return x.ProtocolName + } + return "" +} + +func (x *StreamConfig) GetTransportSettings() []*TransportConfig { + if x != nil { + return x.TransportSettings + } + return nil +} + +func (x *StreamConfig) GetSecurityType() string { + if x != nil { + return x.SecurityType + } + return "" +} + +func (x *StreamConfig) GetSecuritySettings() []*serial.TypedMessage { + if x != nil { + return x.SecuritySettings + } + return nil +} + +func (x *StreamConfig) GetSocketSettings() *SocketConfig { + if x != nil { + return x.SocketSettings + } + return nil +} + +type ProxyConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` +} + +func (x *ProxyConfig) Reset() { + *x = ProxyConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyConfig) ProtoMessage() {} + +func (x *ProxyConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_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 ProxyConfig.ProtoReflect.Descriptor instead. +func (*ProxyConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ProxyConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +// SocketConfig is options to be applied on network sockets. +type SocketConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Mark of the connection. If non-zero, the value will be set to SO_MARK. + Mark int32 `protobuf:"varint,1,opt,name=mark,proto3" json:"mark,omitempty"` + // TFO is the state of TFO settings. + Tfo SocketConfig_TCPFastOpenState `protobuf:"varint,2,opt,name=tfo,proto3,enum=xray.transport.internet.SocketConfig_TCPFastOpenState" json:"tfo,omitempty"` + // TProxy is for enabling TProxy socket option. + Tproxy SocketConfig_TProxyMode `protobuf:"varint,3,opt,name=tproxy,proto3,enum=xray.transport.internet.SocketConfig_TProxyMode" json:"tproxy,omitempty"` + // ReceiveOriginalDestAddress is for enabling IP_RECVORIGDSTADDR socket + // option. This option is for UDP only. + ReceiveOriginalDestAddress bool `protobuf:"varint,4,opt,name=receive_original_dest_address,json=receiveOriginalDestAddress,proto3" json:"receive_original_dest_address,omitempty"` + BindAddress []byte `protobuf:"bytes,5,opt,name=bind_address,json=bindAddress,proto3" json:"bind_address,omitempty"` + BindPort uint32 `protobuf:"varint,6,opt,name=bind_port,json=bindPort,proto3" json:"bind_port,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,7,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` +} + +func (x *SocketConfig) Reset() { + *x = SocketConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SocketConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SocketConfig) ProtoMessage() {} + +func (x *SocketConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_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 SocketConfig.ProtoReflect.Descriptor instead. +func (*SocketConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{3} +} + +func (x *SocketConfig) GetMark() int32 { + if x != nil { + return x.Mark + } + return 0 +} + +func (x *SocketConfig) GetTfo() SocketConfig_TCPFastOpenState { + if x != nil { + return x.Tfo + } + return SocketConfig_AsIs +} + +func (x *SocketConfig) GetTproxy() SocketConfig_TProxyMode { + if x != nil { + return x.Tproxy + } + return SocketConfig_Off +} + +func (x *SocketConfig) GetReceiveOriginalDestAddress() bool { + if x != nil { + return x.ReceiveOriginalDestAddress + } + return false +} + +func (x *SocketConfig) GetBindAddress() []byte { + if x != nil { + return x.BindAddress + } + return nil +} + +func (x *SocketConfig) GetBindPort() uint32 { + if x != nil { + return x.BindPort + } + return 0 +} + +func (x *SocketConfig) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +var File_transport_internet_config_proto protoreflect.FileDescriptor + +var file_transport_internet_config_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x17, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 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, 0xc0, 0x01, + 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x4a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x42, + 0x02, 0x18, 0x01, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x23, 0x0a, + 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, + 0x20, 0x01, 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, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x22, 0x9c, 0x03, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x4a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x42, + 0x02, 0x18, 0x01, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x23, 0x0a, + 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x57, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, + 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, + 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x4d, 0x0a, 0x11, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 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, 0x10, 0x73, + 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, + 0x4e, 0x0a, 0x0f, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0e, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, + 0x1f, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, + 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, + 0x22, 0xd5, 0x03, 0x0a, 0x0c, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x61, 0x72, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x04, 0x6d, 0x61, 0x72, 0x6b, 0x12, 0x48, 0x0a, 0x03, 0x74, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53, 0x6f, 0x63, + 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x61, 0x73, + 0x74, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x03, 0x74, 0x66, 0x6f, 0x12, + 0x48, 0x0a, 0x06, 0x74, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x30, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, + 0x65, 0x52, 0x06, 0x74, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x65, 0x63, + 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, + 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, + 0x6c, 0x44, 0x65, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0b, 0x62, 0x69, 0x6e, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x08, 0x62, 0x69, 0x6e, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x15, + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x63, 0x63, + 0x65, 0x70, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x22, 0x35, 0x0a, 0x10, 0x54, 0x43, 0x50, 0x46, 0x61, 0x73, 0x74, 0x4f, 0x70, 0x65, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x0a, + 0x0a, 0x06, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x10, 0x02, 0x22, 0x2f, 0x0a, 0x0a, 0x54, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x66, 0x66, 0x10, 0x00, 0x12, 0x0a, + 0x0a, 0x06, 0x54, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x65, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0x02, 0x2a, 0x5a, 0x0a, 0x11, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, + 0x03, 0x54, 0x43, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x4d, 0x4b, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x57, 0x65, 0x62, + 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, + 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x6f, 0x63, 0x6b, + 0x65, 0x74, 0x10, 0x05, 0x42, 0x6a, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x65, 0x74, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0xaa, 0x02, 0x17, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_config_proto_rawDescOnce sync.Once + file_transport_internet_config_proto_rawDescData = file_transport_internet_config_proto_rawDesc +) + +func file_transport_internet_config_proto_rawDescGZIP() []byte { + file_transport_internet_config_proto_rawDescOnce.Do(func() { + file_transport_internet_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_config_proto_rawDescData) + }) + return file_transport_internet_config_proto_rawDescData +} + +var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_transport_internet_config_proto_goTypes = []interface{}{ + (TransportProtocol)(0), // 0: xray.transport.internet.TransportProtocol + (SocketConfig_TCPFastOpenState)(0), // 1: xray.transport.internet.SocketConfig.TCPFastOpenState + (SocketConfig_TProxyMode)(0), // 2: xray.transport.internet.SocketConfig.TProxyMode + (*TransportConfig)(nil), // 3: xray.transport.internet.TransportConfig + (*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig + (*ProxyConfig)(nil), // 5: xray.transport.internet.ProxyConfig + (*SocketConfig)(nil), // 6: xray.transport.internet.SocketConfig + (*serial.TypedMessage)(nil), // 7: xray.common.serial.TypedMessage +} +var file_transport_internet_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.TransportConfig.protocol:type_name -> xray.transport.internet.TransportProtocol + 7, // 1: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage + 0, // 2: xray.transport.internet.StreamConfig.protocol:type_name -> xray.transport.internet.TransportProtocol + 3, // 3: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig + 7, // 4: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage + 6, // 5: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig + 1, // 6: xray.transport.internet.SocketConfig.tfo:type_name -> xray.transport.internet.SocketConfig.TCPFastOpenState + 2, // 7: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_transport_internet_config_proto_init() } +func file_transport_internet_config_proto_init() { + if File_transport_internet_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TransportConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StreamConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SocketConfig); 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_transport_internet_config_proto_rawDesc, + NumEnums: 3, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_config_proto_goTypes, + DependencyIndexes: file_transport_internet_config_proto_depIdxs, + EnumInfos: file_transport_internet_config_proto_enumTypes, + MessageInfos: file_transport_internet_config_proto_msgTypes, + }.Build() + File_transport_internet_config_proto = out.File + file_transport_internet_config_proto_rawDesc = nil + file_transport_internet_config_proto_goTypes = nil + file_transport_internet_config_proto_depIdxs = nil +} diff --git a/transport/internet/config.proto b/transport/internet/config.proto new file mode 100644 index 00000000..d6dc354e --- /dev/null +++ b/transport/internet/config.proto @@ -0,0 +1,90 @@ +syntax = "proto3"; + +package xray.transport.internet; +option csharp_namespace = "Xray.Transport.Internet"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet"; +option java_package = "com.xray.transport.internet"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +enum TransportProtocol { + TCP = 0; + UDP = 1; + MKCP = 2; + WebSocket = 3; + HTTP = 4; + DomainSocket = 5; +} + +message TransportConfig { + // Type of network that this settings supports. + // Deprecated. Use the string form below. + TransportProtocol protocol = 1 [ deprecated = true ]; + + // Type of network that this settings supports. + string protocol_name = 3; + + // Specific settings. Must be of the transports. + xray.common.serial.TypedMessage settings = 2; +} + +message StreamConfig { + // Effective network. Deprecated. Use the string form below. + TransportProtocol protocol = 1 [ deprecated = true ]; + + // Effective network. + string protocol_name = 5; + + repeated TransportConfig transport_settings = 2; + + // Type of security. Must be a message name of the settings proto. + string security_type = 3; + + // Settings for transport security. For now the only choice is TLS. + repeated xray.common.serial.TypedMessage security_settings = 4; + + SocketConfig socket_settings = 6; +} + +message ProxyConfig { string tag = 1; } + +// SocketConfig is options to be applied on network sockets. +message SocketConfig { + // Mark of the connection. If non-zero, the value will be set to SO_MARK. + int32 mark = 1; + + enum TCPFastOpenState { + // AsIs is to leave the current TFO state as is, unmodified. + AsIs = 0; + // Enable is for enabling TFO explictly. + Enable = 1; + // Disable is for disabling TFO explictly. + Disable = 2; + } + + // TFO is the state of TFO settings. + TCPFastOpenState tfo = 2; + + enum TProxyMode { + // TProxy is off. + Off = 0; + // TProxy mode. + TProxy = 1; + // Redirect mode. + Redirect = 2; + } + + // TProxy is for enabling TProxy socket option. + TProxyMode tproxy = 3; + + // ReceiveOriginalDestAddress is for enabling IP_RECVORIGDSTADDR socket + // option. This option is for UDP only. + bool receive_original_dest_address = 4; + + bytes bind_address = 5; + + uint32 bind_port = 6; + + bool accept_proxy_protocol = 7; +} diff --git a/transport/internet/connection.go b/transport/internet/connection.go new file mode 100644 index 00000000..1f083142 --- /dev/null +++ b/transport/internet/connection.go @@ -0,0 +1,34 @@ +package internet + +import ( + "net" + + "github.com/xtls/xray-core/v1/features/stats" +) + +type Connection interface { + net.Conn +} + +type StatCouterConnection struct { + Connection + ReadCounter stats.Counter + WriteCounter stats.Counter +} + +func (c *StatCouterConnection) Read(b []byte) (int, error) { + nBytes, err := c.Connection.Read(b) + if c.ReadCounter != nil { + c.ReadCounter.Add(int64(nBytes)) + } + + return nBytes, err +} + +func (c *StatCouterConnection) Write(b []byte) (int, error) { + nBytes, err := c.Connection.Write(b) + if c.WriteCounter != nil { + c.WriteCounter.Add(int64(nBytes)) + } + return nBytes, err +} diff --git a/transport/internet/dialer.go b/transport/internet/dialer.go new file mode 100644 index 00000000..cfd40c93 --- /dev/null +++ b/transport/internet/dialer.go @@ -0,0 +1,72 @@ +package internet + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/session" +) + +// Dialer is the interface for dialing outbound connections. +type Dialer interface { + // Dial dials a system connection to the given destination. + Dial(ctx context.Context, destination net.Destination) (Connection, error) + + // Address returns the address used by this Dialer. Maybe nil if not known. + Address() net.Address +} + +// dialFunc is an interface to dial network connection to a specific destination. +type dialFunc func(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (Connection, error) + +var ( + transportDialerCache = make(map[string]dialFunc) +) + +// RegisterTransportDialer registers a Dialer with given name. +func RegisterTransportDialer(protocol string, dialer dialFunc) error { + if _, found := transportDialerCache[protocol]; found { + return newError(protocol, " dialer already registered").AtError() + } + transportDialerCache[protocol] = dialer + return nil +} + +// Dial dials a internet connection towards the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (Connection, error) { + if dest.Network == net.Network_TCP { + if streamSettings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, newError("failed to create default stream settings").Base(err) + } + streamSettings = s + } + + protocol := streamSettings.ProtocolName + dialer := transportDialerCache[protocol] + if dialer == nil { + return nil, newError(protocol, " dialer not registered").AtError() + } + return dialer(ctx, dest, streamSettings) + } + + if dest.Network == net.Network_UDP { + udpDialer := transportDialerCache["udp"] + if udpDialer == nil { + return nil, newError("UDP dialer not registered").AtError() + } + return udpDialer(ctx, dest, streamSettings) + } + + return nil, newError("unknown network ", dest.Network) +} + +// DialSystem calls system dialer to create a network connection. +func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + var src net.Address + if outbound := session.OutboundFromContext(ctx); outbound != nil { + src = outbound.Gateway + } + return effectiveSystemDialer.Dial(ctx, src, dest, sockopt) +} diff --git a/transport/internet/dialer_test.go b/transport/internet/dialer_test.go new file mode 100644 index 00000000..793c60b6 --- /dev/null +++ b/transport/internet/dialer_test.go @@ -0,0 +1,27 @@ +package internet_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + . "github.com/xtls/xray-core/v1/transport/internet" +) + +func TestDialWithLocalAddr(t *testing.T) { + server := &tcp.Server{} + dest, err := server.Start() + common.Must(err) + defer server.Close() + + conn, err := DialSystem(context.Background(), net.TCPDestination(net.LocalHostIP, dest.Port), nil) + common.Must(err) + if r := cmp.Diff(conn.RemoteAddr().String(), "127.0.0.1:"+dest.Port.String()); r != "" { + t.Error(r) + } + conn.Close() +} diff --git a/transport/internet/domainsocket/config.go b/transport/internet/domainsocket/config.go new file mode 100644 index 00000000..68dc60a9 --- /dev/null +++ b/transport/internet/domainsocket/config.go @@ -0,0 +1,38 @@ +// +build !confonly + +package domainsocket + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const protocolName = "domainsocket" +const sizeofSunPath = 108 + +func (c *Config) GetUnixAddr() (*net.UnixAddr, error) { + path := c.Path + if path == "" { + return nil, newError("empty domain socket path") + } + if c.Abstract && path[0] != '@' { + path = "@" + path + } + if c.Abstract && c.Padding { + raw := []byte(path) + addr := make([]byte, sizeofSunPath) + copy(addr, raw) + path = string(addr) + } + return &net.UnixAddr{ + Name: path, + Net: "unix", + }, nil +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/domainsocket/config.pb.go b/transport/internet/domainsocket/config.pb.go new file mode 100644 index 00000000..3aff9d64 --- /dev/null +++ b/transport/internet/domainsocket/config.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/domainsocket/config.proto + +package domainsocket + +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 + + // Path of the domain socket. This overrides the IP/Port parameter from + // upstream caller. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Abstract speicifies whether to use abstract namespace or not. + // Traditionally Unix domain socket is file system based. Abstract domain + // socket can be used without acquiring file lock. + Abstract bool `protobuf:"varint,2,opt,name=abstract,proto3" json:"abstract,omitempty"` + // Some apps, eg. haproxy, use the full length of sockaddr_un.sun_path to + // connect(2) or bind(2) when using abstract UDS. + Padding bool `protobuf:"varint,3,opt,name=padding,proto3" json:"padding,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_domainsocket_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_transport_internet_domainsocket_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_transport_internet_domainsocket_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Config) GetAbstract() bool { + if x != nil { + return x.Abstract + } + return false +} + +func (x *Config) GetPadding() bool { + if x != nil { + return x.Padding + } + return false +} + +var File_transport_internet_domainsocket_config_proto protoreflect.FileDescriptor + +var file_transport_internet_domainsocket_config_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x6f, 0x63, 0x6b, 0x65, + 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x24, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x6f, + 0x63, 0x6b, 0x65, 0x74, 0x22, 0x52, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x62, 0x73, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x62, 0x73, 0x74, 0x72, 0x61, 0x63, 0x74, 0x12, 0x18, + 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x91, 0x01, 0x0a, 0x28, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x3c, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x6f, 0x63, 0x6b, 0x65, 0x74, 0xaa, 0x02, 0x24, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_domainsocket_config_proto_rawDescOnce sync.Once + file_transport_internet_domainsocket_config_proto_rawDescData = file_transport_internet_domainsocket_config_proto_rawDesc +) + +func file_transport_internet_domainsocket_config_proto_rawDescGZIP() []byte { + file_transport_internet_domainsocket_config_proto_rawDescOnce.Do(func() { + file_transport_internet_domainsocket_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_domainsocket_config_proto_rawDescData) + }) + return file_transport_internet_domainsocket_config_proto_rawDescData +} + +var file_transport_internet_domainsocket_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_domainsocket_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.domainsocket.Config +} +var file_transport_internet_domainsocket_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_domainsocket_config_proto_init() } +func file_transport_internet_domainsocket_config_proto_init() { + if File_transport_internet_domainsocket_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_domainsocket_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_transport_internet_domainsocket_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_domainsocket_config_proto_goTypes, + DependencyIndexes: file_transport_internet_domainsocket_config_proto_depIdxs, + MessageInfos: file_transport_internet_domainsocket_config_proto_msgTypes, + }.Build() + File_transport_internet_domainsocket_config_proto = out.File + file_transport_internet_domainsocket_config_proto_rawDesc = nil + file_transport_internet_domainsocket_config_proto_goTypes = nil + file_transport_internet_domainsocket_config_proto_depIdxs = nil +} diff --git a/transport/internet/domainsocket/config.proto b/transport/internet/domainsocket/config.proto new file mode 100644 index 00000000..dcefa75d --- /dev/null +++ b/transport/internet/domainsocket/config.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xray.transport.internet.domainsocket; +option csharp_namespace = "Xray.Transport.Internet.DomainSocket"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/domainsocket"; +option java_package = "com.xray.transport.internet.domainsocket"; +option java_multiple_files = true; + +message Config { + // Path of the domain socket. This overrides the IP/Port parameter from + // upstream caller. + string path = 1; + // Abstract speicifies whether to use abstract namespace or not. + // Traditionally Unix domain socket is file system based. Abstract domain + // socket can be used without acquiring file lock. + bool abstract = 2; + // Some apps, eg. haproxy, use the full length of sockaddr_un.sun_path to + // connect(2) or bind(2) when using abstract UDS. + bool padding = 3; +} diff --git a/transport/internet/domainsocket/dial.go b/transport/internet/domainsocket/dial.go new file mode 100644 index 00000000..86e144ff --- /dev/null +++ b/transport/internet/domainsocket/dial.go @@ -0,0 +1,40 @@ +// +build !windows +// +build !wasm +// +build !confonly + +package domainsocket + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + settings := streamSettings.ProtocolSettings.(*Config) + addr, err := settings.GetUnixAddr() + if err != nil { + return nil, err + } + + conn, err := net.DialUnix("unix", nil, addr) + if err != nil { + return nil, newError("failed to dial unix: ", settings.Path).Base(err).AtWarning() + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + return tls.Client(conn, config.GetTLSConfig(tls.WithDestination(dest))), nil + } else if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + return xtls.Client(conn, config.GetXTLSConfig(xtls.WithDestination(dest))), nil + } + + return conn, nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/domainsocket/errgen.go b/transport/internet/domainsocket/errgen.go new file mode 100644 index 00000000..3df24e14 --- /dev/null +++ b/transport/internet/domainsocket/errgen.go @@ -0,0 +1,3 @@ +package domainsocket + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/domainsocket/errors.generated.go b/transport/internet/domainsocket/errors.generated.go new file mode 100644 index 00000000..ff14bd14 --- /dev/null +++ b/transport/internet/domainsocket/errors.generated.go @@ -0,0 +1,9 @@ +package domainsocket + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/domainsocket/listener.go b/transport/internet/domainsocket/listener.go new file mode 100644 index 00000000..60827076 --- /dev/null +++ b/transport/internet/domainsocket/listener.go @@ -0,0 +1,138 @@ +// +build !windows +// +build !wasm +// +build !confonly + +package domainsocket + +import ( + "context" + gotls "crypto/tls" + "os" + "strings" + + goxtls "github.com/xtls/go" + "golang.org/x/sys/unix" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +type Listener struct { + addr *net.UnixAddr + ln net.Listener + tlsConfig *gotls.Config + xtlsConfig *goxtls.Config + config *Config + addConn internet.ConnHandler + locker *fileLocker +} + +func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + settings := streamSettings.ProtocolSettings.(*Config) + addr, err := settings.GetUnixAddr() + if err != nil { + return nil, err + } + + unixListener, err := net.ListenUnix("unix", addr) + if err != nil { + return nil, newError("failed to listen domain socket").Base(err).AtWarning() + } + + ln := &Listener{ + addr: addr, + ln: unixListener, + config: settings, + addConn: handler, + } + + if !settings.Abstract { + ln.locker = &fileLocker{ + path: settings.Path + ".lock", + } + if err := ln.locker.Acquire(); err != nil { + unixListener.Close() + return nil, err + } + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + ln.tlsConfig = config.GetTLSConfig() + } + if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + ln.xtlsConfig = config.GetXTLSConfig() + } + + go ln.run() + + return ln, nil +} + +func (ln *Listener) Addr() net.Addr { + return ln.addr +} + +func (ln *Listener) Close() error { + if ln.locker != nil { + ln.locker.Release() + } + return ln.ln.Close() +} + +func (ln *Listener) run() { + for { + conn, err := ln.ln.Accept() + if err != nil { + if strings.Contains(err.Error(), "closed") { + break + } + newError("failed to accepted raw connections").Base(err).AtWarning().WriteToLog() + continue + } + + if ln.tlsConfig != nil { + conn = tls.Server(conn, ln.tlsConfig) + } else if ln.xtlsConfig != nil { + conn = xtls.Server(conn, ln.xtlsConfig) + } + + ln.addConn(internet.Connection(conn)) + } +} + +type fileLocker struct { + path string + file *os.File +} + +func (fl *fileLocker) Acquire() error { + f, err := os.Create(fl.path) + if err != nil { + return err + } + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil { + f.Close() + return newError("failed to lock file: ", fl.path).Base(err) + } + fl.file = f + return nil +} + +func (fl *fileLocker) Release() { + if err := unix.Flock(int(fl.file.Fd()), unix.LOCK_UN); err != nil { + newError("failed to unlock file: ", fl.path).Base(err).WriteToLog() + } + if err := fl.file.Close(); err != nil { + newError("failed to close file: ", fl.path).Base(err).WriteToLog() + } + if err := os.Remove(fl.path); err != nil { + newError("failed to remove file: ", fl.path).Base(err).WriteToLog() + } +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/transport/internet/domainsocket/listener_test.go b/transport/internet/domainsocket/listener_test.go new file mode 100644 index 00000000..662d45d1 --- /dev/null +++ b/transport/internet/domainsocket/listener_test.go @@ -0,0 +1,92 @@ +// +build !windows + +package domainsocket_test + +import ( + "context" + "runtime" + "testing" + + "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/transport/internet" + . "github.com/xtls/xray-core/v1/transport/internet/domainsocket" +) + +func TestListen(t *testing.T) { + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "domainsocket", + ProtocolSettings: &Config{ + Path: "/tmp/ts3", + }, + } + listener, err := Listen(ctx, nil, net.Port(0), streamSettings, func(conn internet.Connection) { + defer conn.Close() + + b := buf.New() + defer b.Release() + common.Must2(b.ReadFrom(conn)) + b.WriteString("Response") + + common.Must2(conn.Write(b.Bytes())) + }) + common.Must(err) + defer listener.Close() + + conn, err := Dial(ctx, net.Destination{}, streamSettings) + common.Must(err) + defer conn.Close() + + common.Must2(conn.Write([]byte("Request"))) + + b := buf.New() + defer b.Release() + common.Must2(b.ReadFrom(conn)) + + if b.String() != "RequestResponse" { + t.Error("expected response as 'RequestResponse' but got ", b.String()) + } +} + +func TestListenAbstract(t *testing.T) { + if runtime.GOOS != "linux" { + return + } + + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "domainsocket", + ProtocolSettings: &Config{ + Path: "/tmp/ts3", + Abstract: true, + }, + } + listener, err := Listen(ctx, nil, net.Port(0), streamSettings, func(conn internet.Connection) { + defer conn.Close() + + b := buf.New() + defer b.Release() + common.Must2(b.ReadFrom(conn)) + b.WriteString("Response") + + common.Must2(conn.Write(b.Bytes())) + }) + common.Must(err) + defer listener.Close() + + conn, err := Dial(ctx, net.Destination{}, streamSettings) + common.Must(err) + defer conn.Close() + + common.Must2(conn.Write([]byte("Request"))) + + b := buf.New() + defer b.Release() + common.Must2(b.ReadFrom(conn)) + + if b.String() != "RequestResponse" { + t.Error("expected response as 'RequestResponse' but got ", b.String()) + } +} diff --git a/transport/internet/errors.generated.go b/transport/internet/errors.generated.go new file mode 100644 index 00000000..b1390705 --- /dev/null +++ b/transport/internet/errors.generated.go @@ -0,0 +1,9 @@ +package internet + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/filelocker.go b/transport/internet/filelocker.go new file mode 100644 index 00000000..33dec736 --- /dev/null +++ b/transport/internet/filelocker.go @@ -0,0 +1,11 @@ +package internet + +import ( + "os" +) + +// FileLocker is UDS access lock +type FileLocker struct { + path string + file *os.File +} diff --git a/transport/internet/filelocker_other.go b/transport/internet/filelocker_other.go new file mode 100644 index 00000000..347cd66c --- /dev/null +++ b/transport/internet/filelocker_other.go @@ -0,0 +1,36 @@ +// +build !windows + +package internet + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// Acquire lock +func (fl *FileLocker) Acquire() error { + f, err := os.Create(fl.path) + if err != nil { + return err + } + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil { + f.Close() + return newError("failed to lock file: ", fl.path).Base(err) + } + fl.file = f + return nil +} + +// Release lock +func (fl *FileLocker) Release() { + if err := unix.Flock(int(fl.file.Fd()), unix.LOCK_UN); err != nil { + newError("failed to unlock file: ", fl.path).Base(err).WriteToLog() + } + if err := fl.file.Close(); err != nil { + newError("failed to close file: ", fl.path).Base(err).WriteToLog() + } + if err := os.Remove(fl.path); err != nil { + newError("failed to remove file: ", fl.path).Base(err).WriteToLog() + } +} diff --git a/transport/internet/filelocker_windows.go b/transport/internet/filelocker_windows.go new file mode 100644 index 00000000..adbe2e8e --- /dev/null +++ b/transport/internet/filelocker_windows.go @@ -0,0 +1,11 @@ +package internet + +// Acquire lock +func (fl *FileLocker) Acquire() error { + return nil +} + +// Release lock +func (fl *FileLocker) Release() { + return +} diff --git a/transport/internet/header.go b/transport/internet/header.go new file mode 100644 index 00000000..6e7ed4e0 --- /dev/null +++ b/transport/internet/header.go @@ -0,0 +1,40 @@ +package internet + +import ( + "context" + "net" + + "github.com/xtls/xray-core/v1/common" +) + +type PacketHeader interface { + Size() int32 + Serialize([]byte) +} + +func CreatePacketHeader(config interface{}) (PacketHeader, error) { + header, err := common.CreateObject(context.Background(), config) + if err != nil { + return nil, err + } + if h, ok := header.(PacketHeader); ok { + return h, nil + } + return nil, newError("not a packet header") +} + +type ConnectionAuthenticator interface { + Client(net.Conn) net.Conn + Server(net.Conn) net.Conn +} + +func CreateConnectionAuthenticator(config interface{}) (ConnectionAuthenticator, error) { + auth, err := common.CreateObject(context.Background(), config) + if err != nil { + return nil, err + } + if a, ok := auth.(ConnectionAuthenticator); ok { + return a, nil + } + return nil, newError("not a ConnectionAuthenticator") +} diff --git a/transport/internet/header_test.go b/transport/internet/header_test.go new file mode 100644 index 00000000..587339eb --- /dev/null +++ b/transport/internet/header_test.go @@ -0,0 +1,49 @@ +package internet_test + +import ( + "testing" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/headers/noop" + "github.com/xtls/xray-core/v1/transport/internet/headers/srtp" + "github.com/xtls/xray-core/v1/transport/internet/headers/utp" + "github.com/xtls/xray-core/v1/transport/internet/headers/wechat" + "github.com/xtls/xray-core/v1/transport/internet/headers/wireguard" +) + +func TestAllHeadersLoadable(t *testing.T) { + testCases := []struct { + Input interface{} + Size int32 + }{ + { + Input: new(noop.Config), + Size: 0, + }, + { + Input: new(srtp.Config), + Size: 4, + }, + { + Input: new(utp.Config), + Size: 4, + }, + { + Input: new(wechat.VideoConfig), + Size: 13, + }, + { + Input: new(wireguard.WireguardConfig), + Size: 4, + }, + } + + for _, testCase := range testCases { + header, err := CreatePacketHeader(testCase.Input) + common.Must(err) + if header.Size() != testCase.Size { + t.Error("expected size ", testCase.Size, " but got ", header.Size()) + } + } +} diff --git a/transport/internet/headers/http/config.go b/transport/internet/headers/http/config.go new file mode 100644 index 00000000..2778e364 --- /dev/null +++ b/transport/internet/headers/http/config.go @@ -0,0 +1,100 @@ +package http + +import ( + "strings" + + "github.com/xtls/xray-core/v1/common/dice" +) + +func pickString(arr []string) string { + n := len(arr) + switch n { + case 0: + return "" + case 1: + return arr[0] + default: + return arr[dice.Roll(n)] + } +} + +func (v *RequestConfig) PickURI() string { + return pickString(v.Uri) +} + +func (v *RequestConfig) PickHeaders() []string { + n := len(v.Header) + if n == 0 { + return nil + } + headers := make([]string, n) + for idx, headerConfig := range v.Header { + headerName := headerConfig.Name + headerValue := pickString(headerConfig.Value) + headers[idx] = headerName + ": " + headerValue + } + return headers +} + +func (v *RequestConfig) GetVersionValue() string { + if v == nil || v.Version == nil { + return "1.1" + } + return v.Version.Value +} + +func (v *RequestConfig) GetMethodValue() string { + if v == nil || v.Method == nil { + return "GET" + } + return v.Method.Value +} + +func (v *RequestConfig) GetFullVersion() string { + return "HTTP/" + v.GetVersionValue() +} + +func (v *ResponseConfig) HasHeader(header string) bool { + cHeader := strings.ToLower(header) + for _, tHeader := range v.Header { + if strings.EqualFold(tHeader.Name, cHeader) { + return true + } + } + return false +} + +func (v *ResponseConfig) PickHeaders() []string { + n := len(v.Header) + if n == 0 { + return nil + } + headers := make([]string, n) + for idx, headerConfig := range v.Header { + headerName := headerConfig.Name + headerValue := pickString(headerConfig.Value) + headers[idx] = headerName + ": " + headerValue + } + return headers +} + +func (v *ResponseConfig) GetVersionValue() string { + if v == nil || v.Version == nil { + return "1.1" + } + return v.Version.Value +} + +func (v *ResponseConfig) GetFullVersion() string { + return "HTTP/" + v.GetVersionValue() +} + +func (v *ResponseConfig) GetStatusValue() *Status { + if v == nil || v.Status == nil { + return &Status{ + Code: "200", + Reason: "OK", + } + } + return v.Status +} diff --git a/transport/internet/headers/http/config.pb.go b/transport/internet/headers/http/config.pb.go new file mode 100644 index 00000000..6bfdb07d --- /dev/null +++ b/transport/internet/headers/http/config.pb.go @@ -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: transport/internet/headers/http/config.proto + +package http + +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 Header struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // "Accept", "Cookie", etc + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Each entry must be valid in one piece. Random entry will be chosen if + // multiple entries present. + Value []string `protobuf:"bytes,2,rep,name=value,proto3" json:"value,omitempty"` +} + +func (x *Header) Reset() { + *x = Header{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Header) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Header) GetValue() []string { + if x != nil { + return x.Value + } + return nil +} + +// HTTP version. Default value "1.1". +type Version struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Version) Reset() { + *x = Version{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Version) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Version) ProtoMessage() {} + +func (x *Version) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 Version.ProtoReflect.Descriptor instead. +func (*Version) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Version) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +// HTTP method. Default value "GET". +type Method struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Method) Reset() { + *x = Method{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Method) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Method) ProtoMessage() {} + +func (x *Method) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 Method.ProtoReflect.Descriptor instead. +func (*Method) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Method) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type RequestConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Full HTTP version like "1.1". + Version *Version `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + // GET, POST, CONNECT etc + Method *Method `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + // URI like "/login.php" + Uri []string `protobuf:"bytes,3,rep,name=uri,proto3" json:"uri,omitempty"` + Header []*Header `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty"` +} + +func (x *RequestConfig) Reset() { + *x = RequestConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RequestConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestConfig) ProtoMessage() {} + +func (x *RequestConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 RequestConfig.ProtoReflect.Descriptor instead. +func (*RequestConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{3} +} + +func (x *RequestConfig) GetVersion() *Version { + if x != nil { + return x.Version + } + return nil +} + +func (x *RequestConfig) GetMethod() *Method { + if x != nil { + return x.Method + } + return nil +} + +func (x *RequestConfig) GetUri() []string { + if x != nil { + return x.Uri + } + return nil +} + +func (x *RequestConfig) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +type Status struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Status code. Default "200". + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + // Statue reason. Default "OK". + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` +} + +func (x *Status) Reset() { + *x = Status{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Status) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *Status) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type ResponseConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version *Version `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Status *Status `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Header []*Header `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty"` +} + +func (x *ResponseConfig) Reset() { + *x = ResponseConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResponseConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResponseConfig) ProtoMessage() {} + +func (x *ResponseConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_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 ResponseConfig.ProtoReflect.Descriptor instead. +func (*ResponseConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ResponseConfig) GetVersion() *Version { + if x != nil { + return x.Version + } + return nil +} + +func (x *ResponseConfig) GetStatus() *Status { + if x != nil { + return x.Status + } + return nil +} + +func (x *ResponseConfig) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Settings for authenticating requests. If not set, client side will not send + // authenication header, and server side will bypass authentication. + Request *RequestConfig `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + // Settings for authenticating responses. If not set, client side will bypass + // authentication, and server side will not send authentication header. + Response *ResponseConfig `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[6] + 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_transport_internet_headers_http_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{6} +} + +func (x *Config) GetRequest() *RequestConfig { + if x != nil { + return x.Request + } + return nil +} + +func (x *Config) GetResponse() *ResponseConfig { + if x != nil { + return x.Response + } + return nil +} + +var File_transport_internet_headers_http_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_http_config_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x68, 0x74, 0x74, + 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x24, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, + 0x68, 0x74, 0x74, 0x70, 0x22, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 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, 0x03, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1f, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1e, 0x0a, 0x06, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xf6, 0x01, 0x0a, 0x0d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x47, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x69, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x44, 0x0a, 0x06, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x22, 0x34, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x47, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x44, 0x0a, 0x06, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x74, + 0x70, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x22, 0xa9, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4d, 0x0a, 0x07, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, + 0x74, 0x74, 0x70, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x91, 0x01, 0x0a, + 0x28, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x50, 0x01, 0x5a, 0x3c, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x2f, 0x68, 0x74, 0x74, 0x70, 0xaa, 0x02, 0x24, 0x58, 0x72, 0x61, 0x79, + 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x48, 0x74, 0x74, 0x70, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_http_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_http_config_proto_rawDescData = file_transport_internet_headers_http_config_proto_rawDesc +) + +func file_transport_internet_headers_http_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_http_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_http_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_http_config_proto_rawDescData) + }) + return file_transport_internet_headers_http_config_proto_rawDescData +} + +var file_transport_internet_headers_http_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_transport_internet_headers_http_config_proto_goTypes = []interface{}{ + (*Header)(nil), // 0: xray.transport.internet.headers.http.Header + (*Version)(nil), // 1: xray.transport.internet.headers.http.Version + (*Method)(nil), // 2: xray.transport.internet.headers.http.Method + (*RequestConfig)(nil), // 3: xray.transport.internet.headers.http.RequestConfig + (*Status)(nil), // 4: xray.transport.internet.headers.http.Status + (*ResponseConfig)(nil), // 5: xray.transport.internet.headers.http.ResponseConfig + (*Config)(nil), // 6: xray.transport.internet.headers.http.Config +} +var file_transport_internet_headers_http_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.headers.http.RequestConfig.version:type_name -> xray.transport.internet.headers.http.Version + 2, // 1: xray.transport.internet.headers.http.RequestConfig.method:type_name -> xray.transport.internet.headers.http.Method + 0, // 2: xray.transport.internet.headers.http.RequestConfig.header:type_name -> xray.transport.internet.headers.http.Header + 1, // 3: xray.transport.internet.headers.http.ResponseConfig.version:type_name -> xray.transport.internet.headers.http.Version + 4, // 4: xray.transport.internet.headers.http.ResponseConfig.status:type_name -> xray.transport.internet.headers.http.Status + 0, // 5: xray.transport.internet.headers.http.ResponseConfig.header:type_name -> xray.transport.internet.headers.http.Header + 3, // 6: xray.transport.internet.headers.http.Config.request:type_name -> xray.transport.internet.headers.http.RequestConfig + 5, // 7: xray.transport.internet.headers.http.Config.response:type_name -> xray.transport.internet.headers.http.ResponseConfig + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_transport_internet_headers_http_config_proto_init() } +func file_transport_internet_headers_http_config_proto_init() { + if File_transport_internet_headers_http_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_http_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Header); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Version); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Method); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RequestConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Status); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ResponseConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_headers_http_config_proto_msgTypes[6].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_transport_internet_headers_http_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_http_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_http_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_http_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_http_config_proto = out.File + file_transport_internet_headers_http_config_proto_rawDesc = nil + file_transport_internet_headers_http_config_proto_goTypes = nil + file_transport_internet_headers_http_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/http/config.proto b/transport/internet/headers/http/config.proto new file mode 100644 index 00000000..9e09871e --- /dev/null +++ b/transport/internet/headers/http/config.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.http; +option csharp_namespace = "Xray.Transport.Internet.Headers.Http"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/http"; +option java_package = "com.xray.transport.internet.headers.http"; +option java_multiple_files = true; + +message Header { + // "Accept", "Cookie", etc + string name = 1; + + // Each entry must be valid in one piece. Random entry will be chosen if + // multiple entries present. + repeated string value = 2; +} + +// HTTP version. Default value "1.1". +message Version { + string value = 1; +} + +// HTTP method. Default value "GET". +message Method { + string value = 1; +} + +message RequestConfig { + // Full HTTP version like "1.1". + Version version = 1; + + // GET, POST, CONNECT etc + Method method = 2; + + // URI like "/login.php" + repeated string uri = 3; + + repeated Header header = 4; +} + +message Status { + // Status code. Default "200". + string code = 1; + + // Statue reason. Default "OK". + string reason = 2; +} + +message ResponseConfig { + Version version = 1; + + Status status = 2; + + repeated Header header = 3; +} + +message Config { + // Settings for authenticating requests. If not set, client side will not send + // authenication header, and server side will bypass authentication. + RequestConfig request = 1; + + // Settings for authenticating responses. If not set, client side will bypass + // authentication, and server side will not send authentication header. + ResponseConfig response = 2; +} diff --git a/transport/internet/headers/http/errors.generated.go b/transport/internet/headers/http/errors.generated.go new file mode 100644 index 00000000..b07b3fec --- /dev/null +++ b/transport/internet/headers/http/errors.generated.go @@ -0,0 +1,9 @@ +package http + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/headers/http/http.go b/transport/internet/headers/http/http.go new file mode 100644 index 00000000..c6be0487 --- /dev/null +++ b/transport/internet/headers/http/http.go @@ -0,0 +1,321 @@ +package http + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +import ( + "bufio" + "bytes" + "context" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" +) + +const ( + // CRLF is the line ending in HTTP header + CRLF = "\r\n" + + // ENDING is the double line ending between HTTP header and body. + ENDING = CRLF + CRLF + + // max length of HTTP header. Safety precaution for DDoS attack. + maxHeaderLength = 8192 +) + +var ( + ErrHeaderToLong = newError("Header too long.") + + ErrHeaderMisMatch = newError("Header Mismatch.") +) + +type Reader interface { + Read(io.Reader) (*buf.Buffer, error) +} + +type Writer interface { + Write(io.Writer) error +} + +type NoOpReader struct{} + +func (NoOpReader) Read(io.Reader) (*buf.Buffer, error) { + return nil, nil +} + +type NoOpWriter struct{} + +func (NoOpWriter) Write(io.Writer) error { + return nil +} + +type HeaderReader struct { + req *http.Request + expectedHeader *RequestConfig +} + +func (h *HeaderReader) ExpectThisRequest(expectedHeader *RequestConfig) *HeaderReader { + h.expectedHeader = expectedHeader + return h +} + +func (h *HeaderReader) Read(reader io.Reader) (*buf.Buffer, error) { + buffer := buf.New() + totalBytes := int32(0) + endingDetected := false + + var headerBuf bytes.Buffer + + for totalBytes < maxHeaderLength { + _, err := buffer.ReadFrom(reader) + if err != nil { + buffer.Release() + return nil, err + } + if n := bytes.Index(buffer.Bytes(), []byte(ENDING)); n != -1 { + headerBuf.Write(buffer.BytesRange(0, int32(n+len(ENDING)))) + buffer.Advance(int32(n + len(ENDING))) + endingDetected = true + break + } + lenEnding := int32(len(ENDING)) + if buffer.Len() >= lenEnding { + totalBytes += buffer.Len() - lenEnding + headerBuf.Write(buffer.BytesRange(0, buffer.Len()-lenEnding)) + leftover := buffer.BytesFrom(-lenEnding) + buffer.Clear() + copy(buffer.Extend(lenEnding), leftover) + + if _, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes())), false); err != io.ErrUnexpectedEOF { + return nil, err + } + } + } + + if !endingDetected { + buffer.Release() + return nil, ErrHeaderToLong + } + + if h.expectedHeader == nil { + if buffer.IsEmpty() { + buffer.Release() + return nil, nil + } + return buffer, nil + } + + // Parse the request + if req, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes())), false); err != nil { + return nil, err + } else { + h.req = req + } + + // Check req + path := h.req.URL.Path + hasThisURI := false + for _, u := range h.expectedHeader.Uri { + if u == path { + hasThisURI = true + } + } + + if !hasThisURI { + return nil, ErrHeaderMisMatch + } + + if buffer.IsEmpty() { + buffer.Release() + return nil, nil + } + + return buffer, nil +} + +type HeaderWriter struct { + header *buf.Buffer +} + +func NewHeaderWriter(header *buf.Buffer) *HeaderWriter { + return &HeaderWriter{ + header: header, + } +} + +func (w *HeaderWriter) Write(writer io.Writer) error { + if w.header == nil { + return nil + } + err := buf.WriteAllBytes(writer, w.header.Bytes()) + w.header.Release() + w.header = nil + return err +} + +type Conn struct { + net.Conn + + readBuffer *buf.Buffer + oneTimeReader Reader + oneTimeWriter Writer + errorWriter Writer + errorMismatchWriter Writer + errorTooLongWriter Writer + errReason error +} + +func NewConn(conn net.Conn, reader Reader, writer Writer, errorWriter Writer, errorMismatchWriter Writer, errorTooLongWriter Writer) *Conn { + return &Conn{ + Conn: conn, + oneTimeReader: reader, + oneTimeWriter: writer, + errorWriter: errorWriter, + errorMismatchWriter: errorMismatchWriter, + errorTooLongWriter: errorTooLongWriter, + } +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.oneTimeReader != nil { + buffer, err := c.oneTimeReader.Read(c.Conn) + if err != nil { + c.errReason = err + return 0, err + } + c.readBuffer = buffer + c.oneTimeReader = nil + } + + if !c.readBuffer.IsEmpty() { + nBytes, _ := c.readBuffer.Read(b) + if c.readBuffer.IsEmpty() { + c.readBuffer.Release() + c.readBuffer = nil + } + return nBytes, nil + } + + return c.Conn.Read(b) +} + +// Write implements io.Writer. +func (c *Conn) Write(b []byte) (int, error) { + if c.oneTimeWriter != nil { + err := c.oneTimeWriter.Write(c.Conn) + c.oneTimeWriter = nil + if err != nil { + return 0, err + } + } + + return c.Conn.Write(b) +} + +// Close implements net.Conn.Close(). +func (c *Conn) Close() error { + if c.oneTimeWriter != nil && c.errorWriter != nil { + // Connection is being closed but header wasn't sent. This means the client request + // is probably not valid. Sending back a server error header in this case. + + // Write response based on error reason + switch c.errReason { + case ErrHeaderMisMatch: + c.errorMismatchWriter.Write(c.Conn) + case ErrHeaderToLong: + c.errorTooLongWriter.Write(c.Conn) + default: + c.errorWriter.Write(c.Conn) + } + } + + return c.Conn.Close() +} + +func formResponseHeader(config *ResponseConfig) *HeaderWriter { + header := buf.New() + common.Must2(header.WriteString(strings.Join([]string{config.GetFullVersion(), config.GetStatusValue().Code, config.GetStatusValue().Reason}, " "))) + common.Must2(header.WriteString(CRLF)) + + headers := config.PickHeaders() + for _, h := range headers { + common.Must2(header.WriteString(h)) + common.Must2(header.WriteString(CRLF)) + } + if !config.HasHeader("Date") { + common.Must2(header.WriteString("Date: ")) + common.Must2(header.WriteString(time.Now().Format(http.TimeFormat))) + common.Must2(header.WriteString(CRLF)) + } + common.Must2(header.WriteString(CRLF)) + return &HeaderWriter{ + header: header, + } +} + +type Authenticator struct { + config *Config +} + +func (a Authenticator) GetClientWriter() *HeaderWriter { + header := buf.New() + config := a.config.Request + common.Must2(header.WriteString(strings.Join([]string{config.GetMethodValue(), config.PickURI(), config.GetFullVersion()}, " "))) + common.Must2(header.WriteString(CRLF)) + + headers := config.PickHeaders() + for _, h := range headers { + common.Must2(header.WriteString(h)) + common.Must2(header.WriteString(CRLF)) + } + common.Must2(header.WriteString(CRLF)) + return &HeaderWriter{ + header: header, + } +} + +func (a Authenticator) GetServerWriter() *HeaderWriter { + return formResponseHeader(a.config.Response) +} + +func (a Authenticator) Client(conn net.Conn) net.Conn { + if a.config.Request == nil && a.config.Response == nil { + return conn + } + var reader Reader = NoOpReader{} + if a.config.Request != nil { + reader = new(HeaderReader) + } + + var writer Writer = NoOpWriter{} + if a.config.Response != nil { + writer = a.GetClientWriter() + } + return NewConn(conn, reader, writer, NoOpWriter{}, NoOpWriter{}, NoOpWriter{}) +} + +func (a Authenticator) Server(conn net.Conn) net.Conn { + if a.config.Request == nil && a.config.Response == nil { + return conn + } + return NewConn(conn, new(HeaderReader).ExpectThisRequest(a.config.Request), a.GetServerWriter(), + formResponseHeader(resp400), + formResponseHeader(resp404), + formResponseHeader(resp400)) +} + +func NewAuthenticator(ctx context.Context, config *Config) (Authenticator, error) { + return Authenticator{ + config: config, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewAuthenticator(ctx, config.(*Config)) + })) +} diff --git a/transport/internet/headers/http/http_test.go b/transport/internet/headers/http/http_test.go new file mode 100644 index 00000000..6a5540df --- /dev/null +++ b/transport/internet/headers/http/http_test.go @@ -0,0 +1,305 @@ +package http_test + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "strings" + "testing" + "time" + + "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/transport/internet/headers/http" +) + +func TestReaderWriter(t *testing.T) { + cache := buf.New() + b := buf.New() + common.Must2(b.WriteString("abcd" + ENDING)) + writer := NewHeaderWriter(b) + err := writer.Write(cache) + common.Must(err) + if v := cache.Len(); v != 8 { + t.Error("cache len: ", v) + } + _, err = cache.Write([]byte{'e', 'f', 'g'}) + common.Must(err) + + reader := &HeaderReader{} + _, err = reader.Read(cache) + if err != nil && !strings.HasPrefix(err.Error(), "malformed HTTP request") { + t.Error("unknown error ", err) + } +} + +func TestRequestHeader(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Uri: []string{"/"}, + Header: []*Header{ + { + Name: "Test", + Value: []string{"Value"}, + }, + }, + }, + }) + common.Must(err) + + cache := buf.New() + err = auth.GetClientWriter().Write(cache) + common.Must(err) + + if cache.String() != "GET / HTTP/1.1\r\nTest: Value\r\n\r\n" { + t.Error("cache: ", cache.String()) + } +} + +func TestLongRequestHeader(t *testing.T) { + payload := make([]byte, buf.Size+2) + common.Must2(rand.Read(payload[:buf.Size-2])) + copy(payload[buf.Size-2:], ENDING) + payload = append(payload, []byte("abcd")...) + + reader := HeaderReader{} + _, err := reader.Read(bytes.NewReader(payload)) + + if err != nil && !(strings.HasPrefix(err.Error(), "invalid") || strings.HasPrefix(err.Error(), "malformed")) { + t.Error("unknown error ", err) + } +} + +func TestConnection(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + authConn := auth.Client(conn) + defer authConn.Close() + + authConn.Write([]byte("Test payload")) + authConn.Write([]byte("Test payload 2")) + + expectedResponse := "Test payloadTest payload 2" + actualResponse := make([]byte, 256) + deadline := time.Now().Add(time.Second * 5) + totalBytes := 0 + for { + n, err := authConn.Read(actualResponse[totalBytes:]) + common.Must(err) + totalBytes += n + if totalBytes >= len(expectedResponse) || time.Now().After(deadline) { + break + } + } + + if string(actualResponse[:totalBytes]) != expectedResponse { + t.Error("response: ", string(actualResponse[:totalBytes])) + } +} + +func TestConnectionInvPath(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + authR, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpathErr"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + authConn.Close() + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + authConn := authR.Client(conn) + defer authConn.Close() + + authConn.Write([]byte("Test payload")) + authConn.Write([]byte("Test payload 2")) + + expectedResponse := "Test payloadTest payload 2" + actualResponse := make([]byte, 256) + deadline := time.Now().Add(time.Second * 5) + totalBytes := 0 + for { + n, err := authConn.Read(actualResponse[totalBytes:]) + if err == nil { + t.Error("Error Expected", err) + } else { + return + } + totalBytes += n + if totalBytes >= len(expectedResponse) || time.Now().After(deadline) { + break + } + } +} + +func TestConnectionInvReq(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + authConn.Close() + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + conn.Write([]byte("ABCDEFGHIJKMLN\r\n\r\n")) + l, _, err := bufio.NewReader(conn).ReadLine() + common.Must(err) + if !strings.HasPrefix(string(l), "HTTP/1.1 400 Bad Request") { + t.Error("Resp to non http conn", string(l)) + } +} diff --git a/transport/internet/headers/http/linkedreadRequest.go b/transport/internet/headers/http/linkedreadRequest.go new file mode 100644 index 00000000..a6776095 --- /dev/null +++ b/transport/internet/headers/http/linkedreadRequest.go @@ -0,0 +1,11 @@ +package http + +import ( + "bufio" + "net/http" + + _ "unsafe" // required to use //go:linkname +) + +//go:linkname readRequest net/http.readRequest +func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *http.Request, err error) diff --git a/transport/internet/headers/http/resp.go b/transport/internet/headers/http/resp.go new file mode 100644 index 00000000..6050d639 --- /dev/null +++ b/transport/internet/headers/http/resp.go @@ -0,0 +1,49 @@ +package http + +var resp400 = &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "400", + Reason: "Bad Request", + }, + Header: []*Header{ + { + Name: "Connection", + Value: []string{"close"}, + }, + { + Name: "Cache-Control", + Value: []string{"private"}, + }, + { + Name: "Content-Length", + Value: []string{"0"}, + }, + }, +} + +var resp404 = &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + Header: []*Header{ + { + Name: "Connection", + Value: []string{"close"}, + }, + { + Name: "Cache-Control", + Value: []string{"private"}, + }, + { + Name: "Content-Length", + Value: []string{"0"}, + }, + }, +} diff --git a/transport/internet/headers/noop/config.pb.go b/transport/internet/headers/noop/config.pb.go new file mode 100644 index 00000000..fc6f3399 --- /dev/null +++ b/transport/internet/headers/noop/config.pb.go @@ -0,0 +1,200 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/noop/config.proto + +package noop + +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_transport_internet_headers_noop_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_transport_internet_headers_noop_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_transport_internet_headers_noop_config_proto_rawDescGZIP(), []int{0} +} + +type ConnectionConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ConnectionConfig) Reset() { + *x = ConnectionConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_noop_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnectionConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionConfig) ProtoMessage() {} + +func (x *ConnectionConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_noop_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 ConnectionConfig.ProtoReflect.Descriptor instead. +func (*ConnectionConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_noop_config_proto_rawDescGZIP(), []int{1} +} + +var File_transport_internet_headers_noop_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_noop_config_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x6e, 0x6f, 0x6f, + 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x24, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, + 0x6e, 0x6f, 0x6f, 0x70, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, + 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x42, 0x91, 0x01, 0x0a, 0x28, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x6e, 0x6f, 0x6f, 0x70, 0x50, + 0x01, 0x5a, 0x3c, 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, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x6e, 0x6f, 0x6f, 0x70, 0xaa, + 0x02, 0x24, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x2e, 0x4e, 0x6f, 0x6f, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_noop_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_noop_config_proto_rawDescData = file_transport_internet_headers_noop_config_proto_rawDesc +) + +func file_transport_internet_headers_noop_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_noop_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_noop_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_noop_config_proto_rawDescData) + }) + return file_transport_internet_headers_noop_config_proto_rawDescData +} + +var file_transport_internet_headers_noop_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_headers_noop_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.headers.noop.Config + (*ConnectionConfig)(nil), // 1: xray.transport.internet.headers.noop.ConnectionConfig +} +var file_transport_internet_headers_noop_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_noop_config_proto_init() } +func file_transport_internet_headers_noop_config_proto_init() { + if File_transport_internet_headers_noop_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_noop_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_transport_internet_headers_noop_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnectionConfig); 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_transport_internet_headers_noop_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_noop_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_noop_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_noop_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_noop_config_proto = out.File + file_transport_internet_headers_noop_config_proto_rawDesc = nil + file_transport_internet_headers_noop_config_proto_goTypes = nil + file_transport_internet_headers_noop_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/noop/config.proto b/transport/internet/headers/noop/config.proto new file mode 100644 index 00000000..0b63cc26 --- /dev/null +++ b/transport/internet/headers/noop/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.noop; +option csharp_namespace = "Xray.Transport.Internet.Headers.Noop"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/noop"; +option java_package = "com.xray.transport.internet.headers.noop"; +option java_multiple_files = true; + +message Config {} + +message ConnectionConfig {} diff --git a/transport/internet/headers/noop/noop.go b/transport/internet/headers/noop/noop.go new file mode 100644 index 00000000..76413612 --- /dev/null +++ b/transport/internet/headers/noop/noop.go @@ -0,0 +1,40 @@ +package noop + +import ( + "context" + "net" + + "github.com/xtls/xray-core/v1/common" +) + +type NoOpHeader struct{} + +func (NoOpHeader) Size() int32 { + return 0 +} + +// Serialize implements PacketHeader. +func (NoOpHeader) Serialize([]byte) {} + +func NewNoOpHeader(context.Context, interface{}) (interface{}, error) { + return NoOpHeader{}, nil +} + +type NoOpConnectionHeader struct{} + +func (NoOpConnectionHeader) Client(conn net.Conn) net.Conn { + return conn +} + +func (NoOpConnectionHeader) Server(conn net.Conn) net.Conn { + return conn +} + +func NewNoOpConnectionHeader(context.Context, interface{}) (interface{}, error) { + return NoOpConnectionHeader{}, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), NewNoOpHeader)) + common.Must(common.RegisterConfig((*ConnectionConfig)(nil), NewNoOpConnectionHeader)) +} diff --git a/transport/internet/headers/srtp/config.pb.go b/transport/internet/headers/srtp/config.pb.go new file mode 100644 index 00000000..e48cb176 --- /dev/null +++ b/transport/internet/headers/srtp/config.pb.go @@ -0,0 +1,208 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/srtp/config.proto + +package srtp + +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 + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Padding bool `protobuf:"varint,2,opt,name=padding,proto3" json:"padding,omitempty"` + Extension bool `protobuf:"varint,3,opt,name=extension,proto3" json:"extension,omitempty"` + CsrcCount uint32 `protobuf:"varint,4,opt,name=csrc_count,json=csrcCount,proto3" json:"csrc_count,omitempty"` + Marker bool `protobuf:"varint,5,opt,name=marker,proto3" json:"marker,omitempty"` + PayloadType uint32 `protobuf:"varint,6,opt,name=payload_type,json=payloadType,proto3" json:"payload_type,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_srtp_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_transport_internet_headers_srtp_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_transport_internet_headers_srtp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Config) GetPadding() bool { + if x != nil { + return x.Padding + } + return false +} + +func (x *Config) GetExtension() bool { + if x != nil { + return x.Extension + } + return false +} + +func (x *Config) GetCsrcCount() uint32 { + if x != nil { + return x.CsrcCount + } + return 0 +} + +func (x *Config) GetMarker() bool { + if x != nil { + return x.Marker + } + return false +} + +func (x *Config) GetPayloadType() uint32 { + if x != nil { + return x.PayloadType + } + return 0 +} + +var File_transport_internet_headers_srtp_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_srtp_config_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x72, 0x74, + 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x24, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, + 0x73, 0x72, 0x74, 0x70, 0x22, 0xb4, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, 0x72, 0x63, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, 0x73, 0x72, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x06, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x79, 0x70, 0x65, 0x42, 0x91, 0x01, 0x0a, 0x28, + 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x2e, 0x73, 0x72, 0x74, 0x70, 0x50, 0x01, 0x5a, 0x3c, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x2f, 0x73, 0x72, 0x74, 0x70, 0xaa, 0x02, 0x24, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x72, 0x74, 0x70, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_srtp_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_srtp_config_proto_rawDescData = file_transport_internet_headers_srtp_config_proto_rawDesc +) + +func file_transport_internet_headers_srtp_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_srtp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_srtp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_srtp_config_proto_rawDescData) + }) + return file_transport_internet_headers_srtp_config_proto_rawDescData +} + +var file_transport_internet_headers_srtp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_headers_srtp_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.headers.srtp.Config +} +var file_transport_internet_headers_srtp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_srtp_config_proto_init() } +func file_transport_internet_headers_srtp_config_proto_init() { + if File_transport_internet_headers_srtp_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_srtp_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_transport_internet_headers_srtp_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_srtp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_srtp_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_srtp_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_srtp_config_proto = out.File + file_transport_internet_headers_srtp_config_proto_rawDesc = nil + file_transport_internet_headers_srtp_config_proto_goTypes = nil + file_transport_internet_headers_srtp_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/srtp/config.proto b/transport/internet/headers/srtp/config.proto new file mode 100644 index 00000000..e7a4c662 --- /dev/null +++ b/transport/internet/headers/srtp/config.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.srtp; +option csharp_namespace = "Xray.Transport.Internet.Headers.Srtp"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/srtp"; +option java_package = "com.xray.transport.internet.headers.srtp"; +option java_multiple_files = true; + +message Config { + uint32 version = 1; + bool padding = 2; + bool extension = 3; + uint32 csrc_count = 4; + bool marker = 5; + uint32 payload_type = 6; +} diff --git a/transport/internet/headers/srtp/srtp.go b/transport/internet/headers/srtp/srtp.go new file mode 100644 index 00000000..50c10735 --- /dev/null +++ b/transport/internet/headers/srtp/srtp.go @@ -0,0 +1,37 @@ +package srtp + +import ( + "context" + "encoding/binary" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" +) + +type SRTP struct { + header uint16 + number uint16 +} + +func (*SRTP) Size() int32 { + return 4 +} + +// Serialize implements PacketHeader. +func (s *SRTP) Serialize(b []byte) { + s.number++ + binary.BigEndian.PutUint16(b, s.header) + binary.BigEndian.PutUint16(b[2:], s.number) +} + +// New returns a new SRTP instance based on the given config. +func New(ctx context.Context, config interface{}) (interface{}, error) { + return &SRTP{ + header: 0xB5E8, + number: dice.RollUint16(), + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), New)) +} diff --git a/transport/internet/headers/srtp/srtp_test.go b/transport/internet/headers/srtp/srtp_test.go new file mode 100644 index 00000000..f9230d3c --- /dev/null +++ b/transport/internet/headers/srtp/srtp_test.go @@ -0,0 +1,27 @@ +package srtp_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/internet/headers/srtp" +) + +func TestSRTPWrite(t *testing.T) { + content := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g'} + srtpRaw, err := New(context.Background(), &Config{}) + common.Must(err) + + srtp := srtpRaw.(*SRTP) + + payload := buf.New() + srtp.Serialize(payload.Extend(srtp.Size())) + payload.Write(content) + + expectedLen := int32(len(content)) + srtp.Size() + if payload.Len() != expectedLen { + t.Error("expected ", expectedLen, " of bytes, but got ", payload.Len()) + } +} diff --git a/transport/internet/headers/tls/config.pb.go b/transport/internet/headers/tls/config.pb.go new file mode 100644 index 00000000..c3ecaa88 --- /dev/null +++ b/transport/internet/headers/tls/config.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/tls/config.proto + +package tls + +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 PacketConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *PacketConfig) Reset() { + *x = PacketConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_tls_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PacketConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PacketConfig) ProtoMessage() {} + +func (x *PacketConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_tls_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 PacketConfig.ProtoReflect.Descriptor instead. +func (*PacketConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_tls_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_headers_tls_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_tls_config_proto_rawDesc = []byte{ + 0x0a, 0x2b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x74, 0x6c, 0x73, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x74, + 0x6c, 0x73, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x42, 0x8e, 0x01, 0x0a, 0x27, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, + 0x5a, 0x3b, 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, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, + 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x23, + 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, + 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_tls_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_tls_config_proto_rawDescData = file_transport_internet_headers_tls_config_proto_rawDesc +) + +func file_transport_internet_headers_tls_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_tls_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_tls_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_tls_config_proto_rawDescData) + }) + return file_transport_internet_headers_tls_config_proto_rawDescData +} + +var file_transport_internet_headers_tls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_headers_tls_config_proto_goTypes = []interface{}{ + (*PacketConfig)(nil), // 0: xray.transport.internet.headers.tls.PacketConfig +} +var file_transport_internet_headers_tls_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_tls_config_proto_init() } +func file_transport_internet_headers_tls_config_proto_init() { + if File_transport_internet_headers_tls_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_tls_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PacketConfig); 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_transport_internet_headers_tls_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_tls_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_tls_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_tls_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_tls_config_proto = out.File + file_transport_internet_headers_tls_config_proto_rawDesc = nil + file_transport_internet_headers_tls_config_proto_goTypes = nil + file_transport_internet_headers_tls_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/tls/config.proto b/transport/internet/headers/tls/config.proto new file mode 100644 index 00000000..a4d3fcd7 --- /dev/null +++ b/transport/internet/headers/tls/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.tls; +option csharp_namespace = "Xray.Transport.Internet.Headers.Tls"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/tls"; +option java_package = "com.xray.transport.internet.headers.tls"; +option java_multiple_files = true; + +message PacketConfig {} diff --git a/transport/internet/headers/tls/dtls.go b/transport/internet/headers/tls/dtls.go new file mode 100644 index 00000000..8483a9ef --- /dev/null +++ b/transport/internet/headers/tls/dtls.go @@ -0,0 +1,55 @@ +package tls + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" +) + +// DTLS writes header as DTLS. See https://tools.ietf.org/html/rfc6347 +type DTLS struct { + epoch uint16 + length uint16 + sequence uint32 +} + +// Size implements PacketHeader. +func (*DTLS) Size() int32 { + return 1 + 2 + 2 + 6 + 2 +} + +// Serialize implements PacketHeader. +func (d *DTLS) Serialize(b []byte) { + b[0] = 23 // application data + b[1] = 254 + b[2] = 253 + b[3] = byte(d.epoch >> 8) + b[4] = byte(d.epoch) + b[5] = 0 + b[6] = 0 + b[7] = byte(d.sequence >> 24) + b[8] = byte(d.sequence >> 16) + b[9] = byte(d.sequence >> 8) + b[10] = byte(d.sequence) + d.sequence++ + b[11] = byte(d.length >> 8) + b[12] = byte(d.length) + d.length += 17 + if d.length > 100 { + d.length -= 50 + } +} + +// New creates a new UTP header for the given config. +func New(ctx context.Context, config interface{}) (interface{}, error) { + return &DTLS{ + epoch: dice.RollUint16(), + sequence: 0, + length: 17, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*PacketConfig)(nil), New)) +} diff --git a/transport/internet/headers/tls/dtls_test.go b/transport/internet/headers/tls/dtls_test.go new file mode 100644 index 00000000..7ee0be1d --- /dev/null +++ b/transport/internet/headers/tls/dtls_test.go @@ -0,0 +1,26 @@ +package tls_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/internet/headers/tls" +) + +func TestDTLSWrite(t *testing.T) { + content := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g'} + dtlsRaw, err := New(context.Background(), &PacketConfig{}) + common.Must(err) + + dtls := dtlsRaw.(*DTLS) + + payload := buf.New() + dtls.Serialize(payload.Extend(dtls.Size())) + payload.Write(content) + + if payload.Len() != int32(len(content))+dtls.Size() { + t.Error("payload len: ", payload.Len(), " want ", int32(len(content))+dtls.Size()) + } +} diff --git a/transport/internet/headers/utp/config.pb.go b/transport/internet/headers/utp/config.pb.go new file mode 100644 index 00000000..00757f71 --- /dev/null +++ b/transport/internet/headers/utp/config.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/utp/config.proto + +package utp + +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 + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_utp_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_transport_internet_headers_utp_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_transport_internet_headers_utp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_transport_internet_headers_utp_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_utp_config_proto_rawDesc = []byte{ + 0x0a, 0x2b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x75, 0x74, 0x70, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x75, + 0x74, 0x70, 0x22, 0x22, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x8e, 0x01, 0x0a, 0x27, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x75, + 0x74, 0x70, 0x50, 0x01, 0x5a, 0x3b, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x75, 0x74, + 0x70, 0xaa, 0x02, 0x23, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x2e, 0x55, 0x74, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_utp_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_utp_config_proto_rawDescData = file_transport_internet_headers_utp_config_proto_rawDesc +) + +func file_transport_internet_headers_utp_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_utp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_utp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_utp_config_proto_rawDescData) + }) + return file_transport_internet_headers_utp_config_proto_rawDescData +} + +var file_transport_internet_headers_utp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_headers_utp_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.headers.utp.Config +} +var file_transport_internet_headers_utp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_utp_config_proto_init() } +func file_transport_internet_headers_utp_config_proto_init() { + if File_transport_internet_headers_utp_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_utp_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_transport_internet_headers_utp_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_utp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_utp_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_utp_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_utp_config_proto = out.File + file_transport_internet_headers_utp_config_proto_rawDesc = nil + file_transport_internet_headers_utp_config_proto_goTypes = nil + file_transport_internet_headers_utp_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/utp/config.proto b/transport/internet/headers/utp/config.proto new file mode 100644 index 00000000..a3d480fb --- /dev/null +++ b/transport/internet/headers/utp/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.utp; +option csharp_namespace = "Xray.Transport.Internet.Headers.Utp"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/utp"; +option java_package = "com.xray.transport.internet.headers.utp"; +option java_multiple_files = true; + +message Config { + uint32 version = 1; +} diff --git a/transport/internet/headers/utp/utp.go b/transport/internet/headers/utp/utp.go new file mode 100644 index 00000000..9709afe2 --- /dev/null +++ b/transport/internet/headers/utp/utp.go @@ -0,0 +1,39 @@ +package utp + +import ( + "context" + "encoding/binary" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" +) + +type UTP struct { + header byte + extension byte + connectionID uint16 +} + +func (*UTP) Size() int32 { + return 4 +} + +// Serialize implements PacketHeader. +func (u *UTP) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, u.connectionID) + b[2] = u.header + b[3] = u.extension +} + +// New creates a new UTP header for the given config. +func New(ctx context.Context, config interface{}) (interface{}, error) { + return &UTP{ + header: 1, + extension: 0, + connectionID: dice.RollUint16(), + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), New)) +} diff --git a/transport/internet/headers/utp/utp_test.go b/transport/internet/headers/utp/utp_test.go new file mode 100644 index 00000000..25af8897 --- /dev/null +++ b/transport/internet/headers/utp/utp_test.go @@ -0,0 +1,26 @@ +package utp_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/internet/headers/utp" +) + +func TestUTPWrite(t *testing.T) { + content := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g'} + utpRaw, err := New(context.Background(), &Config{}) + common.Must(err) + + utp := utpRaw.(*UTP) + + payload := buf.New() + utp.Serialize(payload.Extend(utp.Size())) + payload.Write(content) + + if payload.Len() != int32(len(content))+utp.Size() { + t.Error("unexpected payload length: ", payload.Len()) + } +} diff --git a/transport/internet/headers/wechat/config.pb.go b/transport/internet/headers/wechat/config.pb.go new file mode 100644 index 00000000..17635f05 --- /dev/null +++ b/transport/internet/headers/wechat/config.pb.go @@ -0,0 +1,149 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/wechat/config.proto + +package wechat + +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 VideoConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VideoConfig) Reset() { + *x = VideoConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_wechat_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VideoConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VideoConfig) ProtoMessage() {} + +func (x *VideoConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_wechat_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 VideoConfig.ProtoReflect.Descriptor instead. +func (*VideoConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_wechat_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_headers_wechat_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_wechat_config_proto_rawDesc = []byte{ + 0x0a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x77, 0x65, 0x63, + 0x68, 0x61, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x26, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x2e, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x22, 0x0d, 0x0a, 0x0b, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x97, 0x01, 0x0a, 0x2a, 0x63, 0x6f, 0x6d, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, + 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x50, 0x01, 0x5a, 0x3e, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x2f, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0xaa, 0x02, 0x26, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x57, 0x65, 0x63, 0x68, 0x61, + 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_wechat_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_wechat_config_proto_rawDescData = file_transport_internet_headers_wechat_config_proto_rawDesc +) + +func file_transport_internet_headers_wechat_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_wechat_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_wechat_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_wechat_config_proto_rawDescData) + }) + return file_transport_internet_headers_wechat_config_proto_rawDescData +} + +var file_transport_internet_headers_wechat_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_headers_wechat_config_proto_goTypes = []interface{}{ + (*VideoConfig)(nil), // 0: xray.transport.internet.headers.wechat.VideoConfig +} +var file_transport_internet_headers_wechat_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_wechat_config_proto_init() } +func file_transport_internet_headers_wechat_config_proto_init() { + if File_transport_internet_headers_wechat_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_wechat_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VideoConfig); 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_transport_internet_headers_wechat_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_wechat_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_wechat_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_wechat_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_wechat_config_proto = out.File + file_transport_internet_headers_wechat_config_proto_rawDesc = nil + file_transport_internet_headers_wechat_config_proto_goTypes = nil + file_transport_internet_headers_wechat_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/wechat/config.proto b/transport/internet/headers/wechat/config.proto new file mode 100644 index 00000000..5b268c55 --- /dev/null +++ b/transport/internet/headers/wechat/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.wechat; +option csharp_namespace = "Xray.Transport.Internet.Headers.Wechat"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/wechat"; +option java_package = "com.xray.transport.internet.headers.wechat"; +option java_multiple_files = true; + +message VideoConfig {} diff --git a/transport/internet/headers/wechat/wechat.go b/transport/internet/headers/wechat/wechat.go new file mode 100644 index 00000000..d7d71ef1 --- /dev/null +++ b/transport/internet/headers/wechat/wechat.go @@ -0,0 +1,43 @@ +package wechat + +import ( + "context" + "encoding/binary" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" +) + +type VideoChat struct { + sn uint32 +} + +func (vc *VideoChat) Size() int32 { + return 13 +} + +// Serialize implements PacketHeader. +func (vc *VideoChat) Serialize(b []byte) { + vc.sn++ + b[0] = 0xa1 + b[1] = 0x08 + binary.BigEndian.PutUint32(b[2:], vc.sn) // b[2:6] + b[6] = 0x00 + b[7] = 0x10 + b[8] = 0x11 + b[9] = 0x18 + b[10] = 0x30 + b[11] = 0x22 + b[12] = 0x30 +} + +// NewVideoChat returns a new VideoChat instance based on given config. +func NewVideoChat(ctx context.Context, config interface{}) (interface{}, error) { + return &VideoChat{ + sn: uint32(dice.RollUint16()), + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*VideoConfig)(nil), NewVideoChat)) +} diff --git a/transport/internet/headers/wechat/wechat_test.go b/transport/internet/headers/wechat/wechat_test.go new file mode 100644 index 00000000..4a254f0a --- /dev/null +++ b/transport/internet/headers/wechat/wechat_test.go @@ -0,0 +1,24 @@ +package wechat_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/internet/headers/wechat" +) + +func TestUTPWrite(t *testing.T) { + videoRaw, err := NewVideoChat(context.Background(), &VideoConfig{}) + common.Must(err) + + video := videoRaw.(*VideoChat) + + payload := buf.New() + video.Serialize(payload.Extend(video.Size())) + + if payload.Len() != video.Size() { + t.Error("expected payload size ", video.Size(), " but got ", payload.Len()) + } +} diff --git a/transport/internet/headers/wireguard/config.pb.go b/transport/internet/headers/wireguard/config.pb.go new file mode 100644 index 00000000..0c9ac3fc --- /dev/null +++ b/transport/internet/headers/wireguard/config.pb.go @@ -0,0 +1,150 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/headers/wireguard/config.proto + +package wireguard + +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 WireguardConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *WireguardConfig) Reset() { + *x = WireguardConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_headers_wireguard_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WireguardConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WireguardConfig) ProtoMessage() {} + +func (x *WireguardConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_wireguard_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 WireguardConfig.ProtoReflect.Descriptor instead. +func (*WireguardConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_wireguard_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_headers_wireguard_config_proto protoreflect.FileDescriptor + +var file_transport_internet_headers_wireguard_config_proto_rawDesc = []byte{ + 0x0a, 0x31, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x77, 0x69, 0x72, + 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x2e, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x22, 0x11, + 0x0a, 0x0f, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x42, 0xa0, 0x01, 0x0a, 0x2d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, + 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, + 0x61, 0x72, 0x64, 0x50, 0x01, 0x5a, 0x41, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x77, + 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0xaa, 0x02, 0x29, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_headers_wireguard_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_wireguard_config_proto_rawDescData = file_transport_internet_headers_wireguard_config_proto_rawDesc +) + +func file_transport_internet_headers_wireguard_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_wireguard_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_wireguard_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_headers_wireguard_config_proto_rawDescData) + }) + return file_transport_internet_headers_wireguard_config_proto_rawDescData +} + +var file_transport_internet_headers_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_headers_wireguard_config_proto_goTypes = []interface{}{ + (*WireguardConfig)(nil), // 0: xray.transport.internet.headers.wireguard.WireguardConfig +} +var file_transport_internet_headers_wireguard_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_headers_wireguard_config_proto_init() } +func file_transport_internet_headers_wireguard_config_proto_init() { + if File_transport_internet_headers_wireguard_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_headers_wireguard_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WireguardConfig); 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_transport_internet_headers_wireguard_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_wireguard_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_wireguard_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_wireguard_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_wireguard_config_proto = out.File + file_transport_internet_headers_wireguard_config_proto_rawDesc = nil + file_transport_internet_headers_wireguard_config_proto_goTypes = nil + file_transport_internet_headers_wireguard_config_proto_depIdxs = nil +} diff --git a/transport/internet/headers/wireguard/config.proto b/transport/internet/headers/wireguard/config.proto new file mode 100644 index 00000000..e917bf43 --- /dev/null +++ b/transport/internet/headers/wireguard/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.wireguard; +option csharp_namespace = "Xray.Transport.Internet.Headers.Wireguard"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/headers/wireguard"; +option java_package = "com.xray.transport.internet.headers.wireguard"; +option java_multiple_files = true; + +message WireguardConfig {} diff --git a/transport/internet/headers/wireguard/wireguard.go b/transport/internet/headers/wireguard/wireguard.go new file mode 100644 index 00000000..b7425bdf --- /dev/null +++ b/transport/internet/headers/wireguard/wireguard.go @@ -0,0 +1,30 @@ +package wireguard + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" +) + +type Wireguard struct{} + +func (Wireguard) Size() int32 { + return 4 +} + +// Serialize implements PacketHeader. +func (Wireguard) Serialize(b []byte) { + b[0] = 0x04 + b[1] = 0x00 + b[2] = 0x00 + b[3] = 0x00 +} + +// NewWireguard returns a new VideoChat instance based on given config. +func NewWireguard(ctx context.Context, config interface{}) (interface{}, error) { + return Wireguard{}, nil +} + +func init() { + common.Must(common.RegisterConfig((*WireguardConfig)(nil), NewWireguard)) +} diff --git a/transport/internet/http/config.go b/transport/internet/http/config.go new file mode 100644 index 00000000..dc9260ba --- /dev/null +++ b/transport/internet/http/config.go @@ -0,0 +1,49 @@ +// +build !confonly + +package http + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const protocolName = "http" + +func (c *Config) getHosts() []string { + if len(c.Host) == 0 { + return []string{"www.example.com"} + } + return c.Host +} + +func (c *Config) isValidHost(host string) bool { + hosts := c.getHosts() + for _, h := range hosts { + if h == host { + return true + } + } + return false +} + +func (c *Config) getRandomHost() string { + hosts := c.getHosts() + return hosts[dice.Roll(len(hosts))] +} + +func (c *Config) getNormalizedPath() string { + if c.Path == "" { + return "/" + } + if c.Path[0] != '/' { + return "/" + c.Path + } + return c.Path +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/http/config.pb.go b/transport/internet/http/config.pb.go new file mode 100644 index 00000000..44552c4b --- /dev/null +++ b/transport/internet/http/config.pb.go @@ -0,0 +1,165 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/http/config.proto + +package http + +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 + + Host []string `protobuf:"bytes,1,rep,name=host,proto3" json:"host,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_http_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_transport_internet_http_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_transport_internet_http_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetHost() []string { + if x != nil { + return x.Host + } + return nil +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +var File_transport_internet_http_config_proto protoreflect.FileDescriptor + +var file_transport_internet_http_config_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x68, 0x74, 0x74, 0x70, 0x22, 0x30, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x79, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x50, 0x01, 0x5a, 0x34, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x74, + 0x74, 0x70, 0xaa, 0x02, 0x1c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x74, 0x74, + 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_http_config_proto_rawDescOnce sync.Once + file_transport_internet_http_config_proto_rawDescData = file_transport_internet_http_config_proto_rawDesc +) + +func file_transport_internet_http_config_proto_rawDescGZIP() []byte { + file_transport_internet_http_config_proto_rawDescOnce.Do(func() { + file_transport_internet_http_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_http_config_proto_rawDescData) + }) + return file_transport_internet_http_config_proto_rawDescData +} + +var file_transport_internet_http_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_http_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.http.Config +} +var file_transport_internet_http_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_http_config_proto_init() } +func file_transport_internet_http_config_proto_init() { + if File_transport_internet_http_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_http_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_transport_internet_http_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_http_config_proto_goTypes, + DependencyIndexes: file_transport_internet_http_config_proto_depIdxs, + MessageInfos: file_transport_internet_http_config_proto_msgTypes, + }.Build() + File_transport_internet_http_config_proto = out.File + file_transport_internet_http_config_proto_rawDesc = nil + file_transport_internet_http_config_proto_goTypes = nil + file_transport_internet_http_config_proto_depIdxs = nil +} diff --git a/transport/internet/http/config.proto b/transport/internet/http/config.proto new file mode 100644 index 00000000..2b86e9c8 --- /dev/null +++ b/transport/internet/http/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.transport.internet.http; +option csharp_namespace = "Xray.Transport.Internet.Http"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/http"; +option java_package = "com.xray.transport.internet.http"; +option java_multiple_files = true; + +message Config { + repeated string host = 1; + string path = 2; +} diff --git a/transport/internet/http/dialer.go b/transport/internet/http/dialer.go new file mode 100644 index 00000000..59a13d4d --- /dev/null +++ b/transport/internet/http/dialer.go @@ -0,0 +1,138 @@ +// +build !confonly + +package http + +import ( + "context" + gotls "crypto/tls" + "net/http" + "net/url" + "sync" + + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/pipe" + "golang.org/x/net/http2" +) + +var ( + globalDialerMap map[net.Destination]*http.Client + globalDialerAccess sync.Mutex +) + +func getHTTPClient(_ context.Context, dest net.Destination, tlsSettings *tls.Config) (*http.Client, error) { + globalDialerAccess.Lock() + defer globalDialerAccess.Unlock() + + if globalDialerMap == nil { + globalDialerMap = make(map[net.Destination]*http.Client) + } + + if client, found := globalDialerMap[dest]; found { + return client, nil + } + + transport := &http2.Transport{ + DialTLS: func(network string, addr string, tlsConfig *gotls.Config) (net.Conn, error) { + rawHost, rawPort, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + if len(rawPort) == 0 { + rawPort = "443" + } + port, err := net.PortFromString(rawPort) + if err != nil { + return nil, err + } + address := net.ParseAddress(rawHost) + + pconn, err := internet.DialSystem(context.Background(), net.TCPDestination(address, port), nil) + if err != nil { + return nil, err + } + + cn := gotls.Client(pconn, tlsConfig) + if err := cn.Handshake(); err != nil { + return nil, err + } + if !tlsConfig.InsecureSkipVerify { + if err := cn.VerifyHostname(tlsConfig.ServerName); err != nil { + return nil, err + } + } + state := cn.ConnectionState() + if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + return nil, newError("http2: unexpected ALPN protocol " + p + "; want q" + http2.NextProtoTLS).AtError() + } + if !state.NegotiatedProtocolIsMutual { + return nil, newError("http2: could not negotiate protocol mutually").AtError() + } + return cn, nil + }, + TLSClientConfig: tlsSettings.GetTLSConfig(tls.WithDestination(dest)), + } + + client := &http.Client{ + Transport: transport, + } + + globalDialerMap[dest] = client + return client, nil +} + +// Dial dials a new TCP connection to the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + httpSettings := streamSettings.ProtocolSettings.(*Config) + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + return nil, newError("TLS must be enabled for http transport.").AtWarning() + } + client, err := getHTTPClient(ctx, dest, tlsConfig) + if err != nil { + return nil, err + } + + opts := pipe.OptionsFromContext(ctx) + preader, pwriter := pipe.New(opts...) + breader := &buf.BufferedReader{Reader: preader} + request := &http.Request{ + Method: "PUT", + Host: httpSettings.getRandomHost(), + Body: breader, + URL: &url.URL{ + Scheme: "https", + Host: dest.NetAddr(), + Path: httpSettings.getNormalizedPath(), + }, + Proto: "HTTP/2", + ProtoMajor: 2, + ProtoMinor: 0, + Header: make(http.Header), + } + // Disable any compression method from server. + request.Header.Set("Accept-Encoding", "identity") + + response, err := client.Do(request) + if err != nil { + return nil, newError("failed to dial to ", dest).Base(err).AtWarning() + } + if response.StatusCode != 200 { + return nil, newError("unexpected status", response.StatusCode).AtWarning() + } + + bwriter := buf.NewBufferedWriter(pwriter) + common.Must(bwriter.SetBuffered(false)) + return net.NewConnection( + net.ConnectionOutput(response.Body), + net.ConnectionInput(bwriter), + net.ConnectionOnClose(common.ChainedClosable{breader, bwriter, response.Body}), + ), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/http/errors.generated.go b/transport/internet/http/errors.generated.go new file mode 100644 index 00000000..b07b3fec --- /dev/null +++ b/transport/internet/http/errors.generated.go @@ -0,0 +1,9 @@ +package http + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/http/http.go b/transport/internet/http/http.go new file mode 100644 index 00000000..9dc78532 --- /dev/null +++ b/transport/internet/http/http.go @@ -0,0 +1,3 @@ +package http + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/http/http_test.go b/transport/internet/http/http_test.go new file mode 100644 index 00000000..5e954e6e --- /dev/null +++ b/transport/internet/http/http_test.go @@ -0,0 +1,94 @@ +package http_test + +import ( + "context" + "crypto/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "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/protocol/tls/cert" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/transport/internet" + . "github.com/xtls/xray-core/v1/transport/internet/http" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +func TestHTTPConnection(t *testing.T) { + port := tcp.PickPort() + + listener, err := Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "http", + ProtocolSettings: &Config{}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("www.example.com")))}, + }, + }, func(conn internet.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + if _, err := b.ReadFrom(conn); err != nil { + return + } + _, err := conn.Write(b.Bytes()) + common.Must(err) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + dctx := context.Background() + conn, err := Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "http", + ProtocolSettings: &Config{}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + ServerName: "www.example.com", + AllowInsecure: true, + }, + }) + common.Must(err) + defer conn.Close() + + const N = 1024 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + nBytes, err := conn.Write(b1) + common.Must(err) + if nBytes != N { + t.Error("write: ", nBytes) + } + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + nBytes, err = conn.Write(b1) + common.Must(err) + if nBytes != N { + t.Error("write: ", nBytes) + } + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} diff --git a/transport/internet/http/hub.go b/transport/internet/http/hub.go new file mode 100644 index 00000000..356679b5 --- /dev/null +++ b/transport/internet/http/hub.go @@ -0,0 +1,205 @@ +// +build !confonly + +package http + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + http_proto "github.com/xtls/xray-core/v1/common/protocol/http" + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +type Listener struct { + server *http.Server + handler internet.ConnHandler + local net.Addr + config *Config + locker *internet.FileLocker // for unix domain socket +} + +func (l *Listener) Addr() net.Addr { + return l.local +} + +func (l *Listener) Close() error { + if l.locker != nil { + fmt.Fprintln(os.Stderr, "RELEASE LOCK") + l.locker.Release() + } + return l.server.Close() +} + +type flushWriter struct { + w io.Writer + d *done.Instance +} + +func (fw flushWriter) Write(p []byte) (n int, err error) { + if fw.d.Done() { + return 0, io.ErrClosedPipe + } + + n, err = fw.w.Write(p) + if f, ok := fw.w.(http.Flusher); ok { + f.Flush() + } + return +} + +func (l *Listener) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + host := request.Host + if !l.config.isValidHost(host) { + writer.WriteHeader(404) + return + } + path := l.config.getNormalizedPath() + if !strings.HasPrefix(request.URL.Path, path) { + writer.WriteHeader(404) + return + } + + writer.Header().Set("Cache-Control", "no-store") + writer.WriteHeader(200) + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + remoteAddr := l.Addr() + dest, err := net.ParseDestination(request.RemoteAddr) + if err != nil { + newError("failed to parse request remote addr: ", request.RemoteAddr).Base(err).WriteToLog() + } else { + remoteAddr = &net.TCPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + } + } + + forwardedAddrs := http_proto.ParseXForwardedFor(request.Header) + if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddrs[0].IP(), + Port: int(0), + } + } + + done := done.New() + conn := net.NewConnection( + net.ConnectionOutput(request.Body), + net.ConnectionInput(flushWriter{w: writer, d: done}), + net.ConnectionOnClose(common.ChainedClosable{done, request.Body}), + net.ConnectionLocalAddr(l.Addr()), + net.ConnectionRemoteAddr(remoteAddr), + ) + l.handler(conn) + <-done.Wait() +} + +func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + httpSettings := streamSettings.ProtocolSettings.(*Config) + var listener *Listener + if port == net.Port(0) { // unix + listener = &Listener{ + handler: handler, + local: &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, + config: httpSettings, + } + } else { // tcp + listener = &Listener{ + handler: handler, + local: &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, + config: httpSettings, + } + } + + var server *http.Server + config := tls.ConfigFromStreamSettings(streamSettings) + if config == nil { + h2s := &http2.Server{} + + server = &http.Server{ + Addr: serial.Concat(address, ":", port), + Handler: h2c.NewHandler(listener, h2s), + ReadHeaderTimeout: time.Second * 4, + } + } else { + server = &http.Server{ + Addr: serial.Concat(address, ":", port), + TLSConfig: config.GetTLSConfig(tls.WithNextProto("h2")), + Handler: listener, + ReadHeaderTimeout: time.Second * 4, + } + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + + listener.server = server + go func() { + var streamListener net.Listener + var err error + if port == net.Port(0) { // unix + streamListener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + newError("failed to listen on ", address).Base(err).WriteToLog(session.ExportIDToError(ctx)) + return + } + locker := ctx.Value(address.Domain()) + if locker != nil { + listener.locker = locker.(*internet.FileLocker) + } + } else { // tcp + streamListener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + newError("failed to listen on ", address, ":", port).Base(err).WriteToLog(session.ExportIDToError(ctx)) + return + } + } + + if config == nil { + err = server.Serve(streamListener) + if err != nil { + newError("stoping serving H2C").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } else { + err = server.ServeTLS(streamListener, "", "") + if err != nil { + newError("stoping serving TLS").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } + }() + + return listener, nil +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/transport/internet/internet.go b/transport/internet/internet.go new file mode 100644 index 00000000..c880eca3 --- /dev/null +++ b/transport/internet/internet.go @@ -0,0 +1,3 @@ +package internet + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/kcp/config.go b/transport/internet/kcp/config.go new file mode 100644 index 00000000..e6af64ef --- /dev/null +++ b/transport/internet/kcp/config.go @@ -0,0 +1,110 @@ +// +build !confonly + +package kcp + +import ( + "crypto/cipher" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const protocolName = "mkcp" + +// GetMTUValue returns the value of MTU settings. +func (c *Config) GetMTUValue() uint32 { + if c == nil || c.Mtu == nil { + return 1350 + } + return c.Mtu.Value +} + +// GetTTIValue returns the value of TTI settings. +func (c *Config) GetTTIValue() uint32 { + if c == nil || c.Tti == nil { + return 50 + } + return c.Tti.Value +} + +// GetUplinkCapacityValue returns the value of UplinkCapacity settings. +func (c *Config) GetUplinkCapacityValue() uint32 { + if c == nil || c.UplinkCapacity == nil { + return 5 + } + return c.UplinkCapacity.Value +} + +// GetDownlinkCapacityValue returns the value of DownlinkCapacity settings. +func (c *Config) GetDownlinkCapacityValue() uint32 { + if c == nil || c.DownlinkCapacity == nil { + return 20 + } + return c.DownlinkCapacity.Value +} + +// GetWriteBufferSize returns the size of WriterBuffer in bytes. +func (c *Config) GetWriteBufferSize() uint32 { + if c == nil || c.WriteBuffer == nil { + return 2 * 1024 * 1024 + } + return c.WriteBuffer.Size +} + +// GetReadBufferSize returns the size of ReadBuffer in bytes. +func (c *Config) GetReadBufferSize() uint32 { + if c == nil || c.ReadBuffer == nil { + return 2 * 1024 * 1024 + } + return c.ReadBuffer.Size +} + +// GetSecurity returns the security settings. +func (c *Config) GetSecurity() (cipher.AEAD, error) { + if c.Seed != nil { + return NewAEADAESGCMBasedOnSeed(c.Seed.Seed), nil + } + return NewSimpleAuthenticator(), nil +} + +func (c *Config) GetPackerHeader() (internet.PacketHeader, error) { + if c.HeaderConfig != nil { + rawConfig, err := c.HeaderConfig.GetInstance() + if err != nil { + return nil, err + } + + return internet.CreatePacketHeader(rawConfig) + } + return nil, nil +} + +func (c *Config) GetSendingInFlightSize() uint32 { + size := c.GetUplinkCapacityValue() * 1024 * 1024 / c.GetMTUValue() / (1000 / c.GetTTIValue()) + if size < 8 { + size = 8 + } + return size +} + +func (c *Config) GetSendingBufferSize() uint32 { + return c.GetWriteBufferSize() / c.GetMTUValue() +} + +func (c *Config) GetReceivingInFlightSize() uint32 { + size := c.GetDownlinkCapacityValue() * 1024 * 1024 / c.GetMTUValue() / (1000 / c.GetTTIValue()) + if size < 8 { + size = 8 + } + return size +} + +func (c *Config) GetReceivingBufferSize() uint32 { + return c.GetReadBufferSize() / c.GetMTUValue() +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/kcp/config.pb.go b/transport/internet/kcp/config.pb.go new file mode 100644 index 00000000..e7c21cdf --- /dev/null +++ b/transport/internet/kcp/config.pb.go @@ -0,0 +1,774 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/kcp/config.proto + +package kcp + +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 + +// Maximum Transmission Unit, in bytes. +type MTU struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *MTU) Reset() { + *x = MTU{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MTU) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MTU) ProtoMessage() {} + +func (x *MTU) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 MTU.ProtoReflect.Descriptor instead. +func (*MTU) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *MTU) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Transmission Time Interview, in milli-sec. +type TTI struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *TTI) Reset() { + *x = TTI{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TTI) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTI) ProtoMessage() {} + +func (x *TTI) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 TTI.ProtoReflect.Descriptor instead. +func (*TTI) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{1} +} + +func (x *TTI) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Uplink capacity, in MB. +type UplinkCapacity struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *UplinkCapacity) Reset() { + *x = UplinkCapacity{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UplinkCapacity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UplinkCapacity) ProtoMessage() {} + +func (x *UplinkCapacity) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 UplinkCapacity.ProtoReflect.Descriptor instead. +func (*UplinkCapacity) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{2} +} + +func (x *UplinkCapacity) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Downlink capacity, in MB. +type DownlinkCapacity struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *DownlinkCapacity) Reset() { + *x = DownlinkCapacity{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DownlinkCapacity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownlinkCapacity) ProtoMessage() {} + +func (x *DownlinkCapacity) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 DownlinkCapacity.ProtoReflect.Descriptor instead. +func (*DownlinkCapacity) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{3} +} + +func (x *DownlinkCapacity) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +type WriteBuffer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Buffer size in bytes. + Size uint32 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` +} + +func (x *WriteBuffer) Reset() { + *x = WriteBuffer{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WriteBuffer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteBuffer) ProtoMessage() {} + +func (x *WriteBuffer) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 WriteBuffer.ProtoReflect.Descriptor instead. +func (*WriteBuffer) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{4} +} + +func (x *WriteBuffer) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type ReadBuffer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Buffer size in bytes. + Size uint32 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` +} + +func (x *ReadBuffer) Reset() { + *x = ReadBuffer{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReadBuffer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadBuffer) ProtoMessage() {} + +func (x *ReadBuffer) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 ReadBuffer.ProtoReflect.Descriptor instead. +func (*ReadBuffer) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ReadBuffer) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type ConnectionReuse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enable bool `protobuf:"varint,1,opt,name=enable,proto3" json:"enable,omitempty"` +} + +func (x *ConnectionReuse) Reset() { + *x = ConnectionReuse{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnectionReuse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionReuse) ProtoMessage() {} + +func (x *ConnectionReuse) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 ConnectionReuse.ProtoReflect.Descriptor instead. +func (*ConnectionReuse) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{6} +} + +func (x *ConnectionReuse) GetEnable() bool { + if x != nil { + return x.Enable + } + return false +} + +// Maximum Transmission Unit, in bytes. +type EncryptionSeed struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Seed string `protobuf:"bytes,1,opt,name=seed,proto3" json:"seed,omitempty"` +} + +func (x *EncryptionSeed) Reset() { + *x = EncryptionSeed{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EncryptionSeed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptionSeed) ProtoMessage() {} + +func (x *EncryptionSeed) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_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 EncryptionSeed.ProtoReflect.Descriptor instead. +func (*EncryptionSeed) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{7} +} + +func (x *EncryptionSeed) GetSeed() string { + if x != nil { + return x.Seed + } + return "" +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Mtu *MTU `protobuf:"bytes,1,opt,name=mtu,proto3" json:"mtu,omitempty"` + Tti *TTI `protobuf:"bytes,2,opt,name=tti,proto3" json:"tti,omitempty"` + UplinkCapacity *UplinkCapacity `protobuf:"bytes,3,opt,name=uplink_capacity,json=uplinkCapacity,proto3" json:"uplink_capacity,omitempty"` + DownlinkCapacity *DownlinkCapacity `protobuf:"bytes,4,opt,name=downlink_capacity,json=downlinkCapacity,proto3" json:"downlink_capacity,omitempty"` + Congestion bool `protobuf:"varint,5,opt,name=congestion,proto3" json:"congestion,omitempty"` + WriteBuffer *WriteBuffer `protobuf:"bytes,6,opt,name=write_buffer,json=writeBuffer,proto3" json:"write_buffer,omitempty"` + ReadBuffer *ReadBuffer `protobuf:"bytes,7,opt,name=read_buffer,json=readBuffer,proto3" json:"read_buffer,omitempty"` + HeaderConfig *serial.TypedMessage `protobuf:"bytes,8,opt,name=header_config,json=headerConfig,proto3" json:"header_config,omitempty"` + Seed *EncryptionSeed `protobuf:"bytes,10,opt,name=seed,proto3" json:"seed,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_kcp_config_proto_msgTypes[8] + 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_transport_internet_kcp_config_proto_msgTypes[8] + 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_transport_internet_kcp_config_proto_rawDescGZIP(), []int{8} +} + +func (x *Config) GetMtu() *MTU { + if x != nil { + return x.Mtu + } + return nil +} + +func (x *Config) GetTti() *TTI { + if x != nil { + return x.Tti + } + return nil +} + +func (x *Config) GetUplinkCapacity() *UplinkCapacity { + if x != nil { + return x.UplinkCapacity + } + return nil +} + +func (x *Config) GetDownlinkCapacity() *DownlinkCapacity { + if x != nil { + return x.DownlinkCapacity + } + return nil +} + +func (x *Config) GetCongestion() bool { + if x != nil { + return x.Congestion + } + return false +} + +func (x *Config) GetWriteBuffer() *WriteBuffer { + if x != nil { + return x.WriteBuffer + } + return nil +} + +func (x *Config) GetReadBuffer() *ReadBuffer { + if x != nil { + return x.ReadBuffer + } + return nil +} + +func (x *Config) GetHeaderConfig() *serial.TypedMessage { + if x != nil { + return x.HeaderConfig + } + return nil +} + +func (x *Config) GetSeed() *EncryptionSeed { + if x != nil { + return x.Seed + } + return nil +} + +var File_transport_internet_kcp_config_proto protoreflect.FileDescriptor + +var file_transport_internet_kcp_config_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x63, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1b, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, + 0x63, 0x70, 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, 0x1b, 0x0a, 0x03, 0x4d, 0x54, 0x55, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x1b, 0x0a, 0x03, 0x54, 0x54, 0x49, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x26, 0x0a, 0x0e, 0x55, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x28, 0x0a, 0x10, 0x44, 0x6f, 0x77, 0x6e, 0x6c, + 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x21, 0x0a, 0x0b, 0x57, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x22, 0x20, 0x0a, 0x0a, 0x52, 0x65, 0x61, 0x64, 0x42, 0x75, 0x66, 0x66, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x75, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x22, 0x24, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x65, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x73, 0x65, 0x65, 0x64, 0x22, 0xe7, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x32, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, 0x70, 0x2e, 0x4d, 0x54, + 0x55, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, 0x32, 0x0a, 0x03, 0x74, 0x74, 0x69, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, + 0x70, 0x2e, 0x54, 0x54, 0x49, 0x52, 0x03, 0x74, 0x74, 0x69, 0x12, 0x54, 0x0a, 0x0f, 0x75, 0x70, + 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, + 0x70, 0x2e, 0x55, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, + 0x52, 0x0e, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, + 0x12, 0x5a, 0x0a, 0x11, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x63, 0x61, 0x70, + 0x61, 0x63, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, 0x70, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, + 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, + 0x6c, 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, 0x0a, + 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0c, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, 0x70, + 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x52, 0x0b, 0x77, 0x72, + 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, 0x48, 0x0a, 0x0b, 0x72, 0x65, 0x61, + 0x64, 0x5f, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x6b, 0x63, 0x70, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x42, 0x75, 0x66, + 0x66, 0x65, 0x72, 0x12, 0x45, 0x0a, 0x0d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x08, 0x20, 0x01, 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, 0x0c, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3f, 0x0a, 0x04, 0x73, 0x65, + 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2e, 0x6b, 0x63, 0x70, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x65, 0x64, 0x52, 0x04, 0x73, 0x65, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x09, 0x10, + 0x0a, 0x42, 0x76, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x2e, 0x6b, 0x63, 0x70, 0x50, 0x01, 0x5a, 0x33, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x63, 0x70, 0xaa, 0x02, 0x1b, 0x58, 0x72, + 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x4b, 0x63, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_transport_internet_kcp_config_proto_rawDescOnce sync.Once + file_transport_internet_kcp_config_proto_rawDescData = file_transport_internet_kcp_config_proto_rawDesc +) + +func file_transport_internet_kcp_config_proto_rawDescGZIP() []byte { + file_transport_internet_kcp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_kcp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_kcp_config_proto_rawDescData) + }) + return file_transport_internet_kcp_config_proto_rawDescData +} + +var file_transport_internet_kcp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_transport_internet_kcp_config_proto_goTypes = []interface{}{ + (*MTU)(nil), // 0: xray.transport.internet.kcp.MTU + (*TTI)(nil), // 1: xray.transport.internet.kcp.TTI + (*UplinkCapacity)(nil), // 2: xray.transport.internet.kcp.UplinkCapacity + (*DownlinkCapacity)(nil), // 3: xray.transport.internet.kcp.DownlinkCapacity + (*WriteBuffer)(nil), // 4: xray.transport.internet.kcp.WriteBuffer + (*ReadBuffer)(nil), // 5: xray.transport.internet.kcp.ReadBuffer + (*ConnectionReuse)(nil), // 6: xray.transport.internet.kcp.ConnectionReuse + (*EncryptionSeed)(nil), // 7: xray.transport.internet.kcp.EncryptionSeed + (*Config)(nil), // 8: xray.transport.internet.kcp.Config + (*serial.TypedMessage)(nil), // 9: xray.common.serial.TypedMessage +} +var file_transport_internet_kcp_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.kcp.Config.mtu:type_name -> xray.transport.internet.kcp.MTU + 1, // 1: xray.transport.internet.kcp.Config.tti:type_name -> xray.transport.internet.kcp.TTI + 2, // 2: xray.transport.internet.kcp.Config.uplink_capacity:type_name -> xray.transport.internet.kcp.UplinkCapacity + 3, // 3: xray.transport.internet.kcp.Config.downlink_capacity:type_name -> xray.transport.internet.kcp.DownlinkCapacity + 4, // 4: xray.transport.internet.kcp.Config.write_buffer:type_name -> xray.transport.internet.kcp.WriteBuffer + 5, // 5: xray.transport.internet.kcp.Config.read_buffer:type_name -> xray.transport.internet.kcp.ReadBuffer + 9, // 6: xray.transport.internet.kcp.Config.header_config:type_name -> xray.common.serial.TypedMessage + 7, // 7: xray.transport.internet.kcp.Config.seed:type_name -> xray.transport.internet.kcp.EncryptionSeed + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_transport_internet_kcp_config_proto_init() } +func file_transport_internet_kcp_config_proto_init() { + if File_transport_internet_kcp_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_kcp_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MTU); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TTI); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UplinkCapacity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DownlinkCapacity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WriteBuffer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReadBuffer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnectionReuse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EncryptionSeed); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_kcp_config_proto_msgTypes[8].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_transport_internet_kcp_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_kcp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_kcp_config_proto_depIdxs, + MessageInfos: file_transport_internet_kcp_config_proto_msgTypes, + }.Build() + File_transport_internet_kcp_config_proto = out.File + file_transport_internet_kcp_config_proto_rawDesc = nil + file_transport_internet_kcp_config_proto_goTypes = nil + file_transport_internet_kcp_config_proto_depIdxs = nil +} diff --git a/transport/internet/kcp/config.proto b/transport/internet/kcp/config.proto new file mode 100644 index 00000000..c8761eda --- /dev/null +++ b/transport/internet/kcp/config.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +package xray.transport.internet.kcp; +option csharp_namespace = "Xray.Transport.Internet.Kcp"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/kcp"; +option java_package = "com.xray.transport.internet.kcp"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// Maximum Transmission Unit, in bytes. +message MTU { + uint32 value = 1; +} + +// Transmission Time Interview, in milli-sec. +message TTI { + uint32 value = 1; +} + +// Uplink capacity, in MB. +message UplinkCapacity { + uint32 value = 1; +} + +// Downlink capacity, in MB. +message DownlinkCapacity { + uint32 value = 1; +} + +message WriteBuffer { + // Buffer size in bytes. + uint32 size = 1; +} + +message ReadBuffer { + // Buffer size in bytes. + uint32 size = 1; +} + +message ConnectionReuse { + bool enable = 1; +} + +// Maximum Transmission Unit, in bytes. +message EncryptionSeed { + string seed = 1; +} + +message Config { + MTU mtu = 1; + TTI tti = 2; + UplinkCapacity uplink_capacity = 3; + DownlinkCapacity downlink_capacity = 4; + bool congestion = 5; + WriteBuffer write_buffer = 6; + ReadBuffer read_buffer = 7; + xray.common.serial.TypedMessage header_config = 8; + reserved 9; + EncryptionSeed seed = 10; +} diff --git a/transport/internet/kcp/connection.go b/transport/internet/kcp/connection.go new file mode 100644 index 00000000..561afd3a --- /dev/null +++ b/transport/internet/kcp/connection.go @@ -0,0 +1,663 @@ +// +build !confonly + +package kcp + +import ( + "bytes" + "io" + "net" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/signal/semaphore" +) + +var ( + ErrIOTimeout = newError("Read/Write timeout") + ErrClosedListener = newError("Listener closed.") + ErrClosedConnection = newError("Connection closed.") +) + +// State of the connection +type State int32 + +// Is returns true if current State is one of the candidates. +func (s State) Is(states ...State) bool { + for _, state := range states { + if s == state { + return true + } + } + return false +} + +const ( + StateActive State = 0 // Connection is active + StateReadyToClose State = 1 // Connection is closed locally + StatePeerClosed State = 2 // Connection is closed on remote + StateTerminating State = 3 // Connection is ready to be destroyed locally + StatePeerTerminating State = 4 // Connection is ready to be destroyed on remote + StateTerminated State = 5 // Connection is destroyed. +) + +func nowMillisec() int64 { + now := time.Now() + return now.Unix()*1000 + int64(now.Nanosecond()/1000000) +} + +type RoundTripInfo struct { + sync.RWMutex + variation uint32 + srtt uint32 + rto uint32 + minRtt uint32 + updatedTimestamp uint32 +} + +func (info *RoundTripInfo) UpdatePeerRTO(rto uint32, current uint32) { + info.Lock() + defer info.Unlock() + + if current-info.updatedTimestamp < 3000 { + return + } + + info.updatedTimestamp = current + info.rto = rto +} + +func (info *RoundTripInfo) Update(rtt uint32, current uint32) { + if rtt > 0x7FFFFFFF { + return + } + info.Lock() + defer info.Unlock() + + // https://tools.ietf.org/html/rfc6298 + if info.srtt == 0 { + info.srtt = rtt + info.variation = rtt / 2 + } else { + delta := rtt - info.srtt + if info.srtt > rtt { + delta = info.srtt - rtt + } + info.variation = (3*info.variation + delta) / 4 + info.srtt = (7*info.srtt + rtt) / 8 + if info.srtt < info.minRtt { + info.srtt = info.minRtt + } + } + var rto uint32 + if info.minRtt < 4*info.variation { + rto = info.srtt + 4*info.variation + } else { + rto = info.srtt + info.variation + } + + if rto > 10000 { + rto = 10000 + } + info.rto = rto * 5 / 4 + info.updatedTimestamp = current +} + +func (info *RoundTripInfo) Timeout() uint32 { + info.RLock() + defer info.RUnlock() + + return info.rto +} + +func (info *RoundTripInfo) SmoothedTime() uint32 { + info.RLock() + defer info.RUnlock() + + return info.srtt +} + +type Updater struct { + interval int64 + shouldContinue func() bool + shouldTerminate func() bool + updateFunc func() + notifier *semaphore.Instance +} + +func NewUpdater(interval uint32, shouldContinue func() bool, shouldTerminate func() bool, updateFunc func()) *Updater { + u := &Updater{ + interval: int64(time.Duration(interval) * time.Millisecond), + shouldContinue: shouldContinue, + shouldTerminate: shouldTerminate, + updateFunc: updateFunc, + notifier: semaphore.New(1), + } + return u +} + +func (u *Updater) WakeUp() { + select { + case <-u.notifier.Wait(): + go u.run() + default: + } +} + +func (u *Updater) run() { + defer u.notifier.Signal() + + if u.shouldTerminate() { + return + } + ticker := time.NewTicker(u.Interval()) + for u.shouldContinue() { + u.updateFunc() + <-ticker.C + } + ticker.Stop() +} + +func (u *Updater) Interval() time.Duration { + return time.Duration(atomic.LoadInt64(&u.interval)) +} + +func (u *Updater) SetInterval(d time.Duration) { + atomic.StoreInt64(&u.interval, int64(d)) +} + +type ConnMetadata struct { + LocalAddr net.Addr + RemoteAddr net.Addr + Conversation uint16 +} + +// Connection is a KCP connection over UDP. +type Connection struct { + meta ConnMetadata + closer io.Closer + rd time.Time + wd time.Time // write deadline + since int64 + dataInput *signal.Notifier + dataOutput *signal.Notifier + Config *Config + + state State + stateBeginTime uint32 + lastIncomingTime uint32 + lastPingTime uint32 + + mss uint32 + roundTrip *RoundTripInfo + + receivingWorker *ReceivingWorker + sendingWorker *SendingWorker + + output SegmentWriter + + dataUpdater *Updater + pingUpdater *Updater +} + +// NewConnection create a new KCP connection between local and remote. +func NewConnection(meta ConnMetadata, writer PacketWriter, closer io.Closer, config *Config) *Connection { + newError("#", meta.Conversation, " creating connection to ", meta.RemoteAddr).WriteToLog() + + conn := &Connection{ + meta: meta, + closer: closer, + since: nowMillisec(), + dataInput: signal.NewNotifier(), + dataOutput: signal.NewNotifier(), + Config: config, + output: NewRetryableWriter(NewSegmentWriter(writer)), + mss: config.GetMTUValue() - uint32(writer.Overhead()) - DataSegmentOverhead, + roundTrip: &RoundTripInfo{ + rto: 100, + minRtt: config.GetTTIValue(), + }, + } + + conn.receivingWorker = NewReceivingWorker(conn) + conn.sendingWorker = NewSendingWorker(conn) + + isTerminating := func() bool { + return conn.State().Is(StateTerminating, StateTerminated) + } + isTerminated := func() bool { + return conn.State() == StateTerminated + } + conn.dataUpdater = NewUpdater( + config.GetTTIValue(), + func() bool { + return !isTerminating() && (conn.sendingWorker.UpdateNecessary() || conn.receivingWorker.UpdateNecessary()) + }, + isTerminating, + conn.updateTask) + conn.pingUpdater = NewUpdater( + 5000, // 5 seconds + func() bool { return !isTerminated() }, + isTerminated, + conn.updateTask) + conn.pingUpdater.WakeUp() + + return conn +} + +func (c *Connection) Elapsed() uint32 { + return uint32(nowMillisec() - c.since) +} + +// ReadMultiBuffer implements buf.Reader. +func (c *Connection) ReadMultiBuffer() (buf.MultiBuffer, error) { + if c == nil { + return nil, io.EOF + } + + for { + if c.State().Is(StateReadyToClose, StateTerminating, StateTerminated) { + return nil, io.EOF + } + mb := c.receivingWorker.ReadMultiBuffer() + if !mb.IsEmpty() { + c.dataUpdater.WakeUp() + return mb, nil + } + + if c.State() == StatePeerTerminating { + return nil, io.EOF + } + + if err := c.waitForDataInput(); err != nil { + return nil, err + } + } +} + +func (c *Connection) waitForDataInput() error { + for i := 0; i < 16; i++ { + select { + case <-c.dataInput.Wait(): + return nil + default: + runtime.Gosched() + } + } + + duration := time.Second * 16 + if !c.rd.IsZero() { + duration = time.Until(c.rd) + if duration < 0 { + return ErrIOTimeout + } + } + + timeout := time.NewTimer(duration) + defer timeout.Stop() + + select { + case <-c.dataInput.Wait(): + case <-timeout.C: + if !c.rd.IsZero() && c.rd.Before(time.Now()) { + return ErrIOTimeout + } + } + + return nil +} + +// Read implements the Conn Read method. +func (c *Connection) Read(b []byte) (int, error) { + if c == nil { + return 0, io.EOF + } + + for { + if c.State().Is(StateReadyToClose, StateTerminating, StateTerminated) { + return 0, io.EOF + } + nBytes := c.receivingWorker.Read(b) + if nBytes > 0 { + c.dataUpdater.WakeUp() + return nBytes, nil + } + + if err := c.waitForDataInput(); err != nil { + return 0, err + } + } +} + +func (c *Connection) waitForDataOutput() error { + for i := 0; i < 16; i++ { + select { + case <-c.dataOutput.Wait(): + return nil + default: + runtime.Gosched() + } + } + + duration := time.Second * 16 + if !c.wd.IsZero() { + duration = time.Until(c.wd) + if duration < 0 { + return ErrIOTimeout + } + } + + timeout := time.NewTimer(duration) + defer timeout.Stop() + + select { + case <-c.dataOutput.Wait(): + case <-timeout.C: + if !c.wd.IsZero() && c.wd.Before(time.Now()) { + return ErrIOTimeout + } + } + + return nil +} + +// Write implements io.Writer. +func (c *Connection) Write(b []byte) (int, error) { + reader := bytes.NewReader(b) + if err := c.writeMultiBufferInternal(reader); err != nil { + return 0, err + } + return len(b), nil +} + +// WriteMultiBuffer implements buf.Writer. +func (c *Connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + reader := &buf.MultiBufferContainer{ + MultiBuffer: mb, + } + defer reader.Close() + + return c.writeMultiBufferInternal(reader) +} + +func (c *Connection) writeMultiBufferInternal(reader io.Reader) error { + updatePending := false + defer func() { + if updatePending { + c.dataUpdater.WakeUp() + } + }() + + var b *buf.Buffer + defer b.Release() + + for { + for { + if c == nil || c.State() != StateActive { + return io.ErrClosedPipe + } + + if b == nil { + b = buf.New() + _, err := b.ReadFrom(io.LimitReader(reader, int64(c.mss))) + if err != nil { + return nil + } + } + + if !c.sendingWorker.Push(b) { + break + } + updatePending = true + b = nil + } + + if updatePending { + c.dataUpdater.WakeUp() + updatePending = false + } + + if err := c.waitForDataOutput(); err != nil { + return err + } + } +} + +func (c *Connection) SetState(state State) { + current := c.Elapsed() + atomic.StoreInt32((*int32)(&c.state), int32(state)) + atomic.StoreUint32(&c.stateBeginTime, current) + newError("#", c.meta.Conversation, " entering state ", state, " at ", current).AtDebug().WriteToLog() + + switch state { + case StateReadyToClose: + c.receivingWorker.CloseRead() + case StatePeerClosed: + c.sendingWorker.CloseWrite() + case StateTerminating: + c.receivingWorker.CloseRead() + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + case StatePeerTerminating: + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + case StateTerminated: + c.receivingWorker.CloseRead() + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + c.dataUpdater.WakeUp() + c.pingUpdater.WakeUp() + go c.Terminate() + } +} + +// Close closes the connection. +func (c *Connection) Close() error { + if c == nil { + return ErrClosedConnection + } + + c.dataInput.Signal() + c.dataOutput.Signal() + + switch c.State() { + case StateReadyToClose, StateTerminating, StateTerminated: + return ErrClosedConnection + case StateActive: + c.SetState(StateReadyToClose) + case StatePeerClosed: + c.SetState(StateTerminating) + case StatePeerTerminating: + c.SetState(StateTerminated) + } + + newError("#", c.meta.Conversation, " closing connection to ", c.meta.RemoteAddr).WriteToLog() + + return nil +} + +// LocalAddr returns the local network address. The Addr returned is shared by all invocations of LocalAddr, so do not modify it. +func (c *Connection) LocalAddr() net.Addr { + if c == nil { + return nil + } + return c.meta.LocalAddr +} + +// RemoteAddr returns the remote network address. The Addr returned is shared by all invocations of RemoteAddr, so do not modify it. +func (c *Connection) RemoteAddr() net.Addr { + if c == nil { + return nil + } + return c.meta.RemoteAddr +} + +// SetDeadline sets the deadline associated with the listener. A zero time value disables the deadline. +func (c *Connection) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +// SetReadDeadline implements the Conn SetReadDeadline method. +func (c *Connection) SetReadDeadline(t time.Time) error { + if c == nil || c.State() != StateActive { + return ErrClosedConnection + } + c.rd = t + return nil +} + +// SetWriteDeadline implements the Conn SetWriteDeadline method. +func (c *Connection) SetWriteDeadline(t time.Time) error { + if c == nil || c.State() != StateActive { + return ErrClosedConnection + } + c.wd = t + return nil +} + +// kcp update, input loop +func (c *Connection) updateTask() { + c.flush() +} + +func (c *Connection) Terminate() { + if c == nil { + return + } + newError("#", c.meta.Conversation, " terminating connection to ", c.RemoteAddr()).WriteToLog() + + // v.SetState(StateTerminated) + c.dataInput.Signal() + c.dataOutput.Signal() + + c.closer.Close() + c.sendingWorker.Release() + c.receivingWorker.Release() +} + +func (c *Connection) HandleOption(opt SegmentOption) { + if (opt & SegmentOptionClose) == SegmentOptionClose { + c.OnPeerClosed() + } +} + +func (c *Connection) OnPeerClosed() { + switch c.State() { + case StateReadyToClose: + c.SetState(StateTerminating) + case StateActive: + c.SetState(StatePeerClosed) + } +} + +// Input when you received a low level packet (eg. UDP packet), call it +func (c *Connection) Input(segments []Segment) { + current := c.Elapsed() + atomic.StoreUint32(&c.lastIncomingTime, current) + + for _, seg := range segments { + if seg.Conversation() != c.meta.Conversation { + break + } + + switch seg := seg.(type) { + case *DataSegment: + c.HandleOption(seg.Option) + c.receivingWorker.ProcessSegment(seg) + if c.receivingWorker.IsDataAvailable() { + c.dataInput.Signal() + } + c.dataUpdater.WakeUp() + case *AckSegment: + c.HandleOption(seg.Option) + c.sendingWorker.ProcessSegment(current, seg, c.roundTrip.Timeout()) + c.dataOutput.Signal() + c.dataUpdater.WakeUp() + case *CmdOnlySegment: + c.HandleOption(seg.Option) + if seg.Command() == CommandTerminate { + switch c.State() { + case StateActive, StatePeerClosed: + c.SetState(StatePeerTerminating) + case StateReadyToClose: + c.SetState(StateTerminating) + case StateTerminating: + c.SetState(StateTerminated) + } + } + if seg.Option == SegmentOptionClose || seg.Command() == CommandTerminate { + c.dataInput.Signal() + c.dataOutput.Signal() + } + c.sendingWorker.ProcessReceivingNext(seg.ReceivingNext) + c.receivingWorker.ProcessSendingNext(seg.SendingNext) + c.roundTrip.UpdatePeerRTO(seg.PeerRTO, current) + seg.Release() + default: + } + } +} + +func (c *Connection) flush() { + current := c.Elapsed() + + if c.State() == StateTerminated { + return + } + if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 30000 { + c.Close() + } + if c.State() == StateReadyToClose && c.sendingWorker.IsEmpty() { + c.SetState(StateTerminating) + } + + if c.State() == StateTerminating { + newError("#", c.meta.Conversation, " sending terminating cmd.").AtDebug().WriteToLog() + c.Ping(current, CommandTerminate) + + if current-atomic.LoadUint32(&c.stateBeginTime) > 8000 { + c.SetState(StateTerminated) + } + return + } + if c.State() == StatePeerTerminating && current-atomic.LoadUint32(&c.stateBeginTime) > 4000 { + c.SetState(StateTerminating) + } + + if c.State() == StateReadyToClose && current-atomic.LoadUint32(&c.stateBeginTime) > 15000 { + c.SetState(StateTerminating) + } + + // flush acknowledges + c.receivingWorker.Flush(current) + c.sendingWorker.Flush(current) + + if current-atomic.LoadUint32(&c.lastPingTime) >= 3000 { + c.Ping(current, CommandPing) + } +} + +func (c *Connection) State() State { + return State(atomic.LoadInt32((*int32)(&c.state))) +} + +func (c *Connection) Ping(current uint32, cmd Command) { + seg := NewCmdOnlySegment() + seg.Conv = c.meta.Conversation + seg.Cmd = cmd + seg.ReceivingNext = c.receivingWorker.NextNumber() + seg.SendingNext = c.sendingWorker.FirstUnacknowledged() + seg.PeerRTO = c.roundTrip.Timeout() + if c.State() == StateReadyToClose { + seg.Option = SegmentOptionClose + } + c.output.Write(seg) + atomic.StoreUint32(&c.lastPingTime, current) + seg.Release() +} diff --git a/transport/internet/kcp/connection_test.go b/transport/internet/kcp/connection_test.go new file mode 100644 index 00000000..f6d64f8e --- /dev/null +++ b/transport/internet/kcp/connection_test.go @@ -0,0 +1,38 @@ +package kcp_test + +import ( + "io" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/internet/kcp" +) + +type NoOpCloser int + +func (NoOpCloser) Close() error { + return nil +} + +func TestConnectionReadTimeout(t *testing.T) { + conn := NewConnection(ConnMetadata{Conversation: 1}, &KCPPacketWriter{ + Writer: buf.DiscardBytes, + }, NoOpCloser(0), &Config{}) + conn.SetReadDeadline(time.Now().Add(time.Second)) + + b := make([]byte, 1024) + nBytes, err := conn.Read(b) + if nBytes != 0 || err == nil { + t.Error("unexpected read: ", nBytes, err) + } + + conn.Terminate() +} + +func TestConnectionInterface(t *testing.T) { + _ = (io.Writer)(new(Connection)) + _ = (io.Reader)(new(Connection)) + _ = (buf.Reader)(new(Connection)) + _ = (buf.Writer)(new(Connection)) +} diff --git a/transport/internet/kcp/crypt.go b/transport/internet/kcp/crypt.go new file mode 100644 index 00000000..7ccb737c --- /dev/null +++ b/transport/internet/kcp/crypt.go @@ -0,0 +1,78 @@ +// +build !confonly + +package kcp + +import ( + "crypto/cipher" + "encoding/binary" + "hash/fnv" + + "github.com/xtls/xray-core/v1/common" +) + +// SimpleAuthenticator is a legacy AEAD used for KCP encryption. +type SimpleAuthenticator struct{} + +// NewSimpleAuthenticator creates a new SimpleAuthenticator +func NewSimpleAuthenticator() cipher.AEAD { + return &SimpleAuthenticator{} +} + +// NonceSize implements cipher.AEAD.NonceSize(). +func (*SimpleAuthenticator) NonceSize() int { + return 0 +} + +// Overhead implements cipher.AEAD.NonceSize(). +func (*SimpleAuthenticator) Overhead() int { + return 6 +} + +// Seal implements cipher.AEAD.Seal(). +func (a *SimpleAuthenticator) Seal(dst, nonce, plain, extra []byte) []byte { + dst = append(dst, 0, 0, 0, 0, 0, 0) // 4 bytes for hash, and then 2 bytes for length + binary.BigEndian.PutUint16(dst[4:], uint16(len(plain))) + dst = append(dst, plain...) + + fnvHash := fnv.New32a() + common.Must2(fnvHash.Write(dst[4:])) + fnvHash.Sum(dst[:0]) + + dstLen := len(dst) + xtra := 4 - dstLen%4 + if xtra != 4 { + dst = append(dst, make([]byte, xtra)...) + } + xorfwd(dst) + if xtra != 4 { + dst = dst[:dstLen] + } + return dst +} + +// Open implements cipher.AEAD.Open(). +func (a *SimpleAuthenticator) Open(dst, nonce, cipherText, extra []byte) ([]byte, error) { + dst = append(dst, cipherText...) + dstLen := len(dst) + xtra := 4 - dstLen%4 + if xtra != 4 { + dst = append(dst, make([]byte, xtra)...) + } + xorbkd(dst) + if xtra != 4 { + dst = dst[:dstLen] + } + + fnvHash := fnv.New32a() + common.Must2(fnvHash.Write(dst[4:])) + if binary.BigEndian.Uint32(dst[:4]) != fnvHash.Sum32() { + return nil, newError("invalid auth") + } + + length := binary.BigEndian.Uint16(dst[4:6]) + if len(dst)-6 != int(length) { + return nil, newError("invalid auth") + } + + return dst[6:], nil +} diff --git a/transport/internet/kcp/crypt_test.go b/transport/internet/kcp/crypt_test.go new file mode 100644 index 00000000..8a61c8f5 --- /dev/null +++ b/transport/internet/kcp/crypt_test.go @@ -0,0 +1,38 @@ +package kcp_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/xtls/xray-core/v1/common" + . "github.com/xtls/xray-core/v1/transport/internet/kcp" +) + +func TestSimpleAuthenticator(t *testing.T) { + cache := make([]byte, 512) + + payload := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g'} + + auth := NewSimpleAuthenticator() + b := auth.Seal(cache[:0], nil, payload, nil) + c, err := auth.Open(cache[:0], nil, b, nil) + common.Must(err) + if r := cmp.Diff(c, payload); r != "" { + t.Error(r) + } +} + +func TestSimpleAuthenticator2(t *testing.T) { + cache := make([]byte, 512) + + payload := []byte{'a', 'b'} + + auth := NewSimpleAuthenticator() + b := auth.Seal(cache[:0], nil, payload, nil) + c, err := auth.Open(cache[:0], nil, b, nil) + common.Must(err) + if r := cmp.Diff(c, payload); r != "" { + t.Error(r) + } +} diff --git a/transport/internet/kcp/cryptreal.go b/transport/internet/kcp/cryptreal.go new file mode 100644 index 00000000..51aa0fef --- /dev/null +++ b/transport/internet/kcp/cryptreal.go @@ -0,0 +1,15 @@ +package kcp + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + + "github.com/xtls/xray-core/v1/common" +) + +func NewAEADAESGCMBasedOnSeed(seed string) cipher.AEAD { + hashedSeed := sha256.Sum256([]byte(seed)) + aesBlock := common.Must2(aes.NewCipher(hashedSeed[:16])).(cipher.Block) + return common.Must2(cipher.NewGCM(aesBlock)).(cipher.AEAD) +} diff --git a/transport/internet/kcp/dialer.go b/transport/internet/kcp/dialer.go new file mode 100644 index 00000000..3ee506a4 --- /dev/null +++ b/transport/internet/kcp/dialer.go @@ -0,0 +1,102 @@ +// +build !confonly + +package kcp + +import ( + "context" + "io" + "sync/atomic" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/dice" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +var ( + globalConv = uint32(dice.RollUint16()) +) + +func fetchInput(_ context.Context, input io.Reader, reader PacketReader, conn *Connection) { + cache := make(chan *buf.Buffer, 1024) + go func() { + for { + payload := buf.New() + if _, err := payload.ReadFrom(input); err != nil { + payload.Release() + close(cache) + return + } + select { + case cache <- payload: + default: + payload.Release() + } + } + }() + + for payload := range cache { + segments := reader.Read(payload.Bytes()) + payload.Release() + if len(segments) > 0 { + conn.Input(segments) + } + } +} + +// DialKCP dials a new KCP connections to the specific destination. +func DialKCP(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + dest.Network = net.Network_UDP + newError("dialing mKCP to ", dest).WriteToLog() + + rawConn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, newError("failed to dial to dest: ", err).AtWarning().Base(err) + } + + kcpSettings := streamSettings.ProtocolSettings.(*Config) + + header, err := kcpSettings.GetPackerHeader() + if err != nil { + return nil, newError("failed to create packet header").Base(err) + } + security, err := kcpSettings.GetSecurity() + if err != nil { + return nil, newError("failed to create security").Base(err) + } + reader := &KCPPacketReader{ + Header: header, + Security: security, + } + writer := &KCPPacketWriter{ + Header: header, + Security: security, + Writer: rawConn, + } + + conv := uint16(atomic.AddUint32(&globalConv, 1)) + session := NewConnection(ConnMetadata{ + LocalAddr: rawConn.LocalAddr(), + RemoteAddr: rawConn.RemoteAddr(), + Conversation: conv, + }, writer, rawConn, kcpSettings) + + go fetchInput(ctx, rawConn, reader, session) + + var iConn internet.Connection = session + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + iConn = tls.Client(iConn, config.GetTLSConfig(tls.WithDestination(dest))) + } else if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + iConn = xtls.Client(iConn, config.GetXTLSConfig(xtls.WithDestination(dest))) + } + + return iConn, nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, DialKCP)) +} diff --git a/transport/internet/kcp/errors.generated.go b/transport/internet/kcp/errors.generated.go new file mode 100644 index 00000000..604d4fe4 --- /dev/null +++ b/transport/internet/kcp/errors.generated.go @@ -0,0 +1,9 @@ +package kcp + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/kcp/io.go b/transport/internet/kcp/io.go new file mode 100644 index 00000000..3a92a404 --- /dev/null +++ b/transport/internet/kcp/io.go @@ -0,0 +1,97 @@ +// +build !confonly + +package kcp + +import ( + "crypto/cipher" + "crypto/rand" + "io" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/transport/internet" +) + +type PacketReader interface { + Read([]byte) []Segment +} + +type PacketWriter interface { + Overhead() int + io.Writer +} + +type KCPPacketReader struct { + Security cipher.AEAD + Header internet.PacketHeader +} + +func (r *KCPPacketReader) Read(b []byte) []Segment { + if r.Header != nil { + if int32(len(b)) <= r.Header.Size() { + return nil + } + b = b[r.Header.Size():] + } + if r.Security != nil { + nonceSize := r.Security.NonceSize() + overhead := r.Security.Overhead() + if len(b) <= nonceSize+overhead { + return nil + } + out, err := r.Security.Open(b[nonceSize:nonceSize], b[:nonceSize], b[nonceSize:], nil) + if err != nil { + return nil + } + b = out + } + var result []Segment + for len(b) > 0 { + seg, x := ReadSegment(b) + if seg == nil { + break + } + result = append(result, seg) + b = x + } + return result +} + +type KCPPacketWriter struct { + Header internet.PacketHeader + Security cipher.AEAD + Writer io.Writer +} + +func (w *KCPPacketWriter) Overhead() int { + overhead := 0 + if w.Header != nil { + overhead += int(w.Header.Size()) + } + if w.Security != nil { + overhead += w.Security.Overhead() + } + return overhead +} + +func (w *KCPPacketWriter) Write(b []byte) (int, error) { + bb := buf.StackNew() + defer bb.Release() + + if w.Header != nil { + w.Header.Serialize(bb.Extend(w.Header.Size())) + } + if w.Security != nil { + nonceSize := w.Security.NonceSize() + common.Must2(bb.ReadFullFrom(rand.Reader, int32(nonceSize))) + nonce := bb.BytesFrom(int32(-nonceSize)) + + encrypted := bb.Extend(int32(w.Security.Overhead() + len(b))) + w.Security.Seal(encrypted[:0], nonce, b, nil) + } else { + bb.Write(b) + } + + _, err := w.Writer.Write(bb.Bytes()) + return len(b), err +} diff --git a/transport/internet/kcp/io_test.go b/transport/internet/kcp/io_test.go new file mode 100644 index 00000000..d15dd77b --- /dev/null +++ b/transport/internet/kcp/io_test.go @@ -0,0 +1,36 @@ +package kcp_test + +import ( + "testing" + + . "github.com/xtls/xray-core/v1/transport/internet/kcp" +) + +func TestKCPPacketReader(t *testing.T) { + reader := KCPPacketReader{ + Security: &SimpleAuthenticator{}, + } + + testCases := []struct { + Input []byte + Output []Segment + }{ + { + Input: []byte{}, + Output: nil, + }, + { + Input: []byte{1}, + Output: nil, + }, + } + + for _, testCase := range testCases { + seg := reader.Read(testCase.Input) + if testCase.Output == nil && seg != nil { + t.Errorf("Expect nothing returned, but actually %v", seg) + } else if testCase.Output != nil && seg == nil { + t.Errorf("Expect some output, but got nil") + } + } +} diff --git a/transport/internet/kcp/kcp.go b/transport/internet/kcp/kcp.go new file mode 100644 index 00000000..ebcf7c52 --- /dev/null +++ b/transport/internet/kcp/kcp.go @@ -0,0 +1,8 @@ +// Package kcp - A Fast and Reliable ARQ Protocol +// +// Acknowledgement: +// skywind3000@github for inventing the KCP protocol +// xtaci@github for translating to Golang +package kcp + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/kcp/kcp_test.go b/transport/internet/kcp/kcp_test.go new file mode 100644 index 00000000..0557664b --- /dev/null +++ b/transport/internet/kcp/kcp_test.go @@ -0,0 +1,85 @@ +package kcp_test + +import ( + "context" + "crypto/rand" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sync/errgroup" + + "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/transport/internet" + . "github.com/xtls/xray-core/v1/transport/internet/kcp" +) + +func TestDialAndListen(t *testing.T) { + listerner, err := NewListener(context.Background(), net.LocalHostIP, net.Port(0), &internet.MemoryStreamConfig{ + ProtocolName: "mkcp", + ProtocolSettings: &Config{}, + }, func(conn internet.Connection) { + go func(c internet.Connection) { + payload := make([]byte, 4096) + for { + nBytes, err := c.Read(payload) + if err != nil { + break + } + for idx, b := range payload[:nBytes] { + payload[idx] = b ^ 'c' + } + c.Write(payload[:nBytes]) + } + c.Close() + }(conn) + }) + common.Must(err) + defer listerner.Close() + + port := net.Port(listerner.Addr().(*net.UDPAddr).Port) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(func() error { + clientConn, err := DialKCP(context.Background(), net.UDPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "mkcp", + ProtocolSettings: &Config{}, + }) + if err != nil { + return err + } + defer clientConn.Close() + + clientSend := make([]byte, 1024*1024) + rand.Read(clientSend) + go clientConn.Write(clientSend) + + clientReceived := make([]byte, 1024*1024) + common.Must2(io.ReadFull(clientConn, clientReceived)) + + clientExpected := make([]byte, 1024*1024) + for idx, b := range clientSend { + clientExpected[idx] = b ^ 'c' + } + if r := cmp.Diff(clientReceived, clientExpected); r != "" { + return errors.New(r) + } + return nil + }) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } + + for i := 0; i < 60 && listerner.ActiveConnections() > 0; i++ { + time.Sleep(500 * time.Millisecond) + } + if v := listerner.ActiveConnections(); v != 0 { + t.Error("active connections: ", v) + } +} diff --git a/transport/internet/kcp/listener.go b/transport/internet/kcp/listener.go new file mode 100644 index 00000000..88073207 --- /dev/null +++ b/transport/internet/kcp/listener.go @@ -0,0 +1,206 @@ +// +build !confonly + +package kcp + +import ( + "context" + "crypto/cipher" + gotls "crypto/tls" + "sync" + + goxtls "github.com/xtls/go" + + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/udp" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +type ConnectionID struct { + Remote net.Address + Port net.Port + Conv uint16 +} + +// Listener defines a server listening for connections +type Listener struct { + sync.Mutex + sessions map[ConnectionID]*Connection + hub *udp.Hub + tlsConfig *gotls.Config + xtlsConfig *goxtls.Config + config *Config + reader PacketReader + header internet.PacketHeader + security cipher.AEAD + addConn internet.ConnHandler +} + +func NewListener(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (*Listener, error) { + kcpSettings := streamSettings.ProtocolSettings.(*Config) + header, err := kcpSettings.GetPackerHeader() + if err != nil { + return nil, newError("failed to create packet header").Base(err).AtError() + } + security, err := kcpSettings.GetSecurity() + if err != nil { + return nil, newError("failed to create security").Base(err).AtError() + } + l := &Listener{ + header: header, + security: security, + reader: &KCPPacketReader{ + Header: header, + Security: security, + }, + sessions: make(map[ConnectionID]*Connection), + config: kcpSettings, + addConn: addConn, + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + l.tlsConfig = config.GetTLSConfig() + } + if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + l.xtlsConfig = config.GetXTLSConfig() + } + + hub, err := udp.ListenUDP(ctx, address, port, streamSettings, udp.HubCapacity(1024)) + if err != nil { + return nil, err + } + l.Lock() + l.hub = hub + l.Unlock() + newError("listening on ", address, ":", port).WriteToLog() + + go l.handlePackets() + + return l, nil +} + +func (l *Listener) handlePackets() { + receive := l.hub.Receive() + for payload := range receive { + l.OnReceive(payload.Payload, payload.Source) + } +} + +func (l *Listener) OnReceive(payload *buf.Buffer, src net.Destination) { + segments := l.reader.Read(payload.Bytes()) + payload.Release() + + if len(segments) == 0 { + newError("discarding invalid payload from ", src).WriteToLog() + return + } + + conv := segments[0].Conversation() + cmd := segments[0].Command() + + id := ConnectionID{ + Remote: src.Address, + Port: src.Port, + Conv: conv, + } + + l.Lock() + defer l.Unlock() + + conn, found := l.sessions[id] + + if !found { + if cmd == CommandTerminate { + return + } + writer := &Writer{ + id: id, + hub: l.hub, + dest: src, + listener: l, + } + remoteAddr := &net.UDPAddr{ + IP: src.Address.IP(), + Port: int(src.Port), + } + localAddr := l.hub.Addr() + conn = NewConnection(ConnMetadata{ + LocalAddr: localAddr, + RemoteAddr: remoteAddr, + Conversation: conv, + }, &KCPPacketWriter{ + Header: l.header, + Security: l.security, + Writer: writer, + }, writer, l.config) + var netConn internet.Connection = conn + if l.tlsConfig != nil { + netConn = tls.Server(conn, l.tlsConfig) + } else if l.xtlsConfig != nil { + netConn = xtls.Server(conn, l.xtlsConfig) + } + + l.addConn(netConn) + l.sessions[id] = conn + } + conn.Input(segments) +} + +func (l *Listener) Remove(id ConnectionID) { + l.Lock() + delete(l.sessions, id) + l.Unlock() +} + +// Close stops listening on the UDP address. Already Accepted connections are not closed. +func (l *Listener) Close() error { + l.hub.Close() + + l.Lock() + defer l.Unlock() + + for _, conn := range l.sessions { + go conn.Terminate() + } + + return nil +} + +func (l *Listener) ActiveConnections() int { + l.Lock() + defer l.Unlock() + + return len(l.sessions) +} + +// Addr returns the listener's network address, The Addr returned is shared by all invocations of Addr, so do not modify it. +func (l *Listener) Addr() net.Addr { + return l.hub.Addr() +} + +type Writer struct { + id ConnectionID + dest net.Destination + hub *udp.Hub + listener *Listener +} + +func (w *Writer) Write(payload []byte) (int, error) { + return w.hub.WriteTo(payload, w.dest) +} + +func (w *Writer) Close() error { + w.listener.Remove(w.id) + return nil +} + +func ListenKCP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + return NewListener(ctx, address, port, streamSettings, addConn) +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenKCP)) +} diff --git a/transport/internet/kcp/output.go b/transport/internet/kcp/output.go new file mode 100644 index 00000000..087ee017 --- /dev/null +++ b/transport/internet/kcp/output.go @@ -0,0 +1,56 @@ +// +build !confonly + +package kcp + +import ( + "io" + "sync" + + "github.com/xtls/xray-core/v1/common/retry" + + "github.com/xtls/xray-core/v1/common/buf" +) + +type SegmentWriter interface { + Write(seg Segment) error +} + +type SimpleSegmentWriter struct { + sync.Mutex + buffer *buf.Buffer + writer io.Writer +} + +func NewSegmentWriter(writer io.Writer) SegmentWriter { + return &SimpleSegmentWriter{ + writer: writer, + buffer: buf.New(), + } +} + +func (w *SimpleSegmentWriter) Write(seg Segment) error { + w.Lock() + defer w.Unlock() + + w.buffer.Clear() + rawBytes := w.buffer.Extend(seg.ByteSize()) + seg.Serialize(rawBytes) + _, err := w.writer.Write(w.buffer.Bytes()) + return err +} + +type RetryableWriter struct { + writer SegmentWriter +} + +func NewRetryableWriter(writer SegmentWriter) SegmentWriter { + return &RetryableWriter{ + writer: writer, + } +} + +func (w *RetryableWriter) Write(seg Segment) error { + return retry.Timed(5, 100).On(func() error { + return w.writer.Write(seg) + }) +} diff --git a/transport/internet/kcp/receiving.go b/transport/internet/kcp/receiving.go new file mode 100644 index 00000000..c21091b2 --- /dev/null +++ b/transport/internet/kcp/receiving.go @@ -0,0 +1,260 @@ +// +build !confonly + +package kcp + +import ( + "sync" + + "github.com/xtls/xray-core/v1/common/buf" +) + +type ReceivingWindow struct { + cache map[uint32]*DataSegment +} + +func NewReceivingWindow() *ReceivingWindow { + return &ReceivingWindow{ + cache: make(map[uint32]*DataSegment), + } +} + +func (w *ReceivingWindow) Set(id uint32, value *DataSegment) bool { + _, f := w.cache[id] + if f { + return false + } + w.cache[id] = value + return true +} + +func (w *ReceivingWindow) Has(id uint32) bool { + _, f := w.cache[id] + return f +} + +func (w *ReceivingWindow) Remove(id uint32) *DataSegment { + v, f := w.cache[id] + if !f { + return nil + } + delete(w.cache, id) + return v +} + +type AckList struct { + writer SegmentWriter + timestamps []uint32 + numbers []uint32 + nextFlush []uint32 + + flushCandidates []uint32 + dirty bool +} + +func NewAckList(writer SegmentWriter) *AckList { + return &AckList{ + writer: writer, + timestamps: make([]uint32, 0, 128), + numbers: make([]uint32, 0, 128), + nextFlush: make([]uint32, 0, 128), + flushCandidates: make([]uint32, 0, 128), + } +} + +func (l *AckList) Add(number uint32, timestamp uint32) { + l.timestamps = append(l.timestamps, timestamp) + l.numbers = append(l.numbers, number) + l.nextFlush = append(l.nextFlush, 0) + l.dirty = true +} + +func (l *AckList) Clear(una uint32) { + count := 0 + for i := 0; i < len(l.numbers); i++ { + if l.numbers[i] < una { + continue + } + if i != count { + l.numbers[count] = l.numbers[i] + l.timestamps[count] = l.timestamps[i] + l.nextFlush[count] = l.nextFlush[i] + } + count++ + } + if count < len(l.numbers) { + l.numbers = l.numbers[:count] + l.timestamps = l.timestamps[:count] + l.nextFlush = l.nextFlush[:count] + l.dirty = true + } +} + +func (l *AckList) Flush(current uint32, rto uint32) { + l.flushCandidates = l.flushCandidates[:0] + + seg := NewAckSegment() + for i := 0; i < len(l.numbers); i++ { + if l.nextFlush[i] > current { + if len(l.flushCandidates) < cap(l.flushCandidates) { + l.flushCandidates = append(l.flushCandidates, l.numbers[i]) + } + continue + } + seg.PutNumber(l.numbers[i]) + seg.PutTimestamp(l.timestamps[i]) + timeout := rto / 2 + if timeout < 20 { + timeout = 20 + } + l.nextFlush[i] = current + timeout + + if seg.IsFull() { + l.writer.Write(seg) + seg.Release() + seg = NewAckSegment() + l.dirty = false + } + } + + if l.dirty || !seg.IsEmpty() { + for _, number := range l.flushCandidates { + if seg.IsFull() { + break + } + seg.PutNumber(number) + } + l.writer.Write(seg) + l.dirty = false + } + + seg.Release() +} + +type ReceivingWorker struct { + sync.RWMutex + conn *Connection + leftOver buf.MultiBuffer + window *ReceivingWindow + acklist *AckList + nextNumber uint32 + windowSize uint32 +} + +func NewReceivingWorker(kcp *Connection) *ReceivingWorker { + worker := &ReceivingWorker{ + conn: kcp, + window: NewReceivingWindow(), + windowSize: kcp.Config.GetReceivingInFlightSize(), + } + worker.acklist = NewAckList(worker) + return worker +} + +func (w *ReceivingWorker) Release() { + w.Lock() + buf.ReleaseMulti(w.leftOver) + w.leftOver = nil + w.Unlock() +} + +func (w *ReceivingWorker) ProcessSendingNext(number uint32) { + w.Lock() + defer w.Unlock() + + w.acklist.Clear(number) +} + +func (w *ReceivingWorker) ProcessSegment(seg *DataSegment) { + w.Lock() + defer w.Unlock() + + number := seg.Number + idx := number - w.nextNumber + if idx >= w.windowSize { + return + } + w.acklist.Clear(seg.SendingNext) + w.acklist.Add(number, seg.Timestamp) + + if !w.window.Set(seg.Number, seg) { + seg.Release() + } +} + +func (w *ReceivingWorker) ReadMultiBuffer() buf.MultiBuffer { + if w.leftOver != nil { + mb := w.leftOver + w.leftOver = nil + return mb + } + + mb := make(buf.MultiBuffer, 0, 32) + + w.Lock() + defer w.Unlock() + for { + seg := w.window.Remove(w.nextNumber) + if seg == nil { + break + } + w.nextNumber++ + mb = append(mb, seg.Detach()) + seg.Release() + } + + return mb +} + +func (w *ReceivingWorker) Read(b []byte) int { + mb := w.ReadMultiBuffer() + if mb.IsEmpty() { + return 0 + } + mb, nBytes := buf.SplitBytes(mb, b) + if !mb.IsEmpty() { + w.leftOver = mb + } + return nBytes +} + +func (w *ReceivingWorker) IsDataAvailable() bool { + w.RLock() + defer w.RUnlock() + return w.window.Has(w.nextNumber) +} + +func (w *ReceivingWorker) NextNumber() uint32 { + w.RLock() + defer w.RUnlock() + + return w.nextNumber +} + +func (w *ReceivingWorker) Flush(current uint32) { + w.Lock() + defer w.Unlock() + + w.acklist.Flush(current, w.conn.roundTrip.Timeout()) +} + +func (w *ReceivingWorker) Write(seg Segment) error { + ackSeg := seg.(*AckSegment) + ackSeg.Conv = w.conn.meta.Conversation + ackSeg.ReceivingNext = w.nextNumber + ackSeg.ReceivingWindow = w.nextNumber + w.windowSize + ackSeg.Option = 0 + if w.conn.State() == StateReadyToClose { + ackSeg.Option = SegmentOptionClose + } + return w.conn.output.Write(ackSeg) +} + +func (*ReceivingWorker) CloseRead() { +} + +func (w *ReceivingWorker) UpdateNecessary() bool { + w.RLock() + defer w.RUnlock() + + return len(w.acklist.numbers) > 0 +} diff --git a/transport/internet/kcp/segment.go b/transport/internet/kcp/segment.go new file mode 100644 index 00000000..9743061c --- /dev/null +++ b/transport/internet/kcp/segment.go @@ -0,0 +1,305 @@ +// +build !confonly + +package kcp + +import ( + "encoding/binary" + + "github.com/xtls/xray-core/v1/common/buf" +) + +// Command is a KCP command that indicate the purpose of a Segment. +type Command byte + +const ( + // CommandACK indicates an AckSegment. + CommandACK Command = 0 + // CommandData indicates a DataSegment. + CommandData Command = 1 + // CommandTerminate indicates that peer terminates the connection. + CommandTerminate Command = 2 + // CommandPing indicates a ping. + CommandPing Command = 3 +) + +type SegmentOption byte + +const ( + SegmentOptionClose SegmentOption = 1 +) + +type Segment interface { + Release() + Conversation() uint16 + Command() Command + ByteSize() int32 + Serialize([]byte) + parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) +} + +const ( + DataSegmentOverhead = 18 +) + +type DataSegment struct { + Conv uint16 + Option SegmentOption + Timestamp uint32 + Number uint32 + SendingNext uint32 + + payload *buf.Buffer + timeout uint32 + transmit uint32 +} + +func NewDataSegment() *DataSegment { + return new(DataSegment) +} + +func (s *DataSegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Option = opt + if len(buf) < 15 { + return false, nil + } + s.Timestamp = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.Number = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.SendingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + dataLen := int(binary.BigEndian.Uint16(buf)) + buf = buf[2:] + + if len(buf) < dataLen { + return false, nil + } + s.Data().Clear() + s.Data().Write(buf[:dataLen]) + buf = buf[dataLen:] + + return true, buf +} + +func (s *DataSegment) Conversation() uint16 { + return s.Conv +} + +func (*DataSegment) Command() Command { + return CommandData +} + +func (s *DataSegment) Detach() *buf.Buffer { + r := s.payload + s.payload = nil + return r +} + +func (s *DataSegment) Data() *buf.Buffer { + if s.payload == nil { + s.payload = buf.New() + } + return s.payload +} + +func (s *DataSegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(CommandData) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.Timestamp) + binary.BigEndian.PutUint32(b[8:], s.Number) + binary.BigEndian.PutUint32(b[12:], s.SendingNext) + binary.BigEndian.PutUint16(b[16:], uint16(s.payload.Len())) + copy(b[18:], s.payload.Bytes()) +} + +func (s *DataSegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 + 2 + s.payload.Len() +} + +func (s *DataSegment) Release() { + s.payload.Release() + s.payload = nil +} + +type AckSegment struct { + Conv uint16 + Option SegmentOption + ReceivingWindow uint32 + ReceivingNext uint32 + Timestamp uint32 + NumberList []uint32 +} + +const ackNumberLimit = 128 + +func NewAckSegment() *AckSegment { + return new(AckSegment) +} + +func (s *AckSegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Option = opt + if len(buf) < 13 { + return false, nil + } + + s.ReceivingWindow = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.ReceivingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.Timestamp = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + count := int(buf[0]) + buf = buf[1:] + + if len(buf) < count*4 { + return false, nil + } + for i := 0; i < count; i++ { + s.PutNumber(binary.BigEndian.Uint32(buf)) + buf = buf[4:] + } + + return true, buf +} + +func (s *AckSegment) Conversation() uint16 { + return s.Conv +} + +func (*AckSegment) Command() Command { + return CommandACK +} + +func (s *AckSegment) PutTimestamp(timestamp uint32) { + if timestamp-s.Timestamp < 0x7FFFFFFF { + s.Timestamp = timestamp + } +} + +func (s *AckSegment) PutNumber(number uint32) { + s.NumberList = append(s.NumberList, number) +} + +func (s *AckSegment) IsFull() bool { + return len(s.NumberList) == ackNumberLimit +} + +func (s *AckSegment) IsEmpty() bool { + return len(s.NumberList) == 0 +} + +func (s *AckSegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 + 1 + int32(len(s.NumberList)*4) +} + +func (s *AckSegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(CommandACK) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.ReceivingWindow) + binary.BigEndian.PutUint32(b[8:], s.ReceivingNext) + binary.BigEndian.PutUint32(b[12:], s.Timestamp) + b[16] = byte(len(s.NumberList)) + n := 17 + for _, number := range s.NumberList { + binary.BigEndian.PutUint32(b[n:], number) + n += 4 + } +} + +func (s *AckSegment) Release() {} + +type CmdOnlySegment struct { + Conv uint16 + Cmd Command + Option SegmentOption + SendingNext uint32 + ReceivingNext uint32 + PeerRTO uint32 +} + +func NewCmdOnlySegment() *CmdOnlySegment { + return new(CmdOnlySegment) +} + +func (s *CmdOnlySegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Cmd = cmd + s.Option = opt + + if len(buf) < 12 { + return false, nil + } + + s.SendingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.ReceivingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.PeerRTO = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + return true, buf +} + +func (s *CmdOnlySegment) Conversation() uint16 { + return s.Conv +} + +func (s *CmdOnlySegment) Command() Command { + return s.Cmd +} + +func (*CmdOnlySegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 +} + +func (s *CmdOnlySegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(s.Cmd) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.SendingNext) + binary.BigEndian.PutUint32(b[8:], s.ReceivingNext) + binary.BigEndian.PutUint32(b[12:], s.PeerRTO) +} + +func (*CmdOnlySegment) Release() {} + +func ReadSegment(buf []byte) (Segment, []byte) { + if len(buf) < 4 { + return nil, nil + } + + conv := binary.BigEndian.Uint16(buf) + buf = buf[2:] + + cmd := Command(buf[0]) + opt := SegmentOption(buf[1]) + buf = buf[2:] + + var seg Segment + switch cmd { + case CommandData: + seg = NewDataSegment() + case CommandACK: + seg = NewAckSegment() + default: + seg = NewCmdOnlySegment() + } + + valid, extra := seg.parse(conv, cmd, opt, buf) + if !valid { + return nil, nil + } + return seg, extra +} diff --git a/transport/internet/kcp/segment_test.go b/transport/internet/kcp/segment_test.go new file mode 100644 index 00000000..5d099ce1 --- /dev/null +++ b/transport/internet/kcp/segment_test.go @@ -0,0 +1,107 @@ +package kcp_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + . "github.com/xtls/xray-core/v1/transport/internet/kcp" +) + +func TestBadSegment(t *testing.T) { + seg, buf := ReadSegment(nil) + if seg != nil { + t.Error("non-nil seg") + } + if len(buf) != 0 { + t.Error("buf len: ", len(buf)) + } +} + +func TestDataSegment(t *testing.T) { + seg := &DataSegment{ + Conv: 1, + Timestamp: 3, + Number: 4, + SendingNext: 5, + } + seg.Data().Write([]byte{'a', 'b', 'c', 'd'}) + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*DataSegment) + if r := cmp.Diff(seg2, seg, cmpopts.IgnoreUnexported(DataSegment{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(seg2.Data().Bytes(), seg.Data().Bytes()); r != "" { + t.Error(r) + } +} + +func Test1ByteDataSegment(t *testing.T) { + seg := &DataSegment{ + Conv: 1, + Timestamp: 3, + Number: 4, + SendingNext: 5, + } + seg.Data().WriteByte('a') + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*DataSegment) + if r := cmp.Diff(seg2, seg, cmpopts.IgnoreUnexported(DataSegment{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(seg2.Data().Bytes(), seg.Data().Bytes()); r != "" { + t.Error(r) + } +} + +func TestACKSegment(t *testing.T) { + seg := &AckSegment{ + Conv: 1, + ReceivingWindow: 2, + ReceivingNext: 3, + Timestamp: 10, + NumberList: []uint32{1, 3, 5, 7, 9}, + } + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*AckSegment) + if r := cmp.Diff(seg2, seg); r != "" { + t.Error(r) + } +} + +func TestCmdSegment(t *testing.T) { + seg := &CmdOnlySegment{ + Conv: 1, + Cmd: CommandPing, + Option: SegmentOptionClose, + SendingNext: 11, + ReceivingNext: 13, + PeerRTO: 15, + } + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*CmdOnlySegment) + if r := cmp.Diff(seg2, seg); r != "" { + t.Error(r) + } +} diff --git a/transport/internet/kcp/sending.go b/transport/internet/kcp/sending.go new file mode 100644 index 00000000..84e15d2c --- /dev/null +++ b/transport/internet/kcp/sending.go @@ -0,0 +1,366 @@ +// +build !confonly + +package kcp + +import ( + "container/list" + "sync" + + "github.com/xtls/xray-core/v1/common/buf" +) + +type SendingWindow struct { + cache *list.List + totalInFlightSize uint32 + writer SegmentWriter + onPacketLoss func(uint32) +} + +func NewSendingWindow(writer SegmentWriter, onPacketLoss func(uint32)) *SendingWindow { + window := &SendingWindow{ + cache: list.New(), + writer: writer, + onPacketLoss: onPacketLoss, + } + return window +} + +func (sw *SendingWindow) Release() { + if sw == nil { + return + } + for sw.cache.Len() > 0 { + seg := sw.cache.Front().Value.(*DataSegment) + seg.Release() + sw.cache.Remove(sw.cache.Front()) + } +} + +func (sw *SendingWindow) Len() uint32 { + return uint32(sw.cache.Len()) +} + +func (sw *SendingWindow) IsEmpty() bool { + return sw.cache.Len() == 0 +} + +func (sw *SendingWindow) Push(number uint32, b *buf.Buffer) { + seg := NewDataSegment() + seg.Number = number + seg.payload = b + + sw.cache.PushBack(seg) +} + +func (sw *SendingWindow) FirstNumber() uint32 { + return sw.cache.Front().Value.(*DataSegment).Number +} + +func (sw *SendingWindow) Clear(una uint32) { + for !sw.IsEmpty() { + seg := sw.cache.Front().Value.(*DataSegment) + if seg.Number >= una { + break + } + seg.Release() + sw.cache.Remove(sw.cache.Front()) + } +} + +func (sw *SendingWindow) HandleFastAck(number uint32, rto uint32) { + if sw.IsEmpty() { + return + } + + sw.Visit(func(seg *DataSegment) bool { + if number == seg.Number || number-seg.Number > 0x7FFFFFFF { + return false + } + + if seg.transmit > 0 && seg.timeout > rto/3 { + seg.timeout -= rto / 3 + } + return true + }) +} + +func (sw *SendingWindow) Visit(visitor func(seg *DataSegment) bool) { + if sw.IsEmpty() { + return + } + + for e := sw.cache.Front(); e != nil; e = e.Next() { + seg := e.Value.(*DataSegment) + if !visitor(seg) { + break + } + } +} + +func (sw *SendingWindow) Flush(current uint32, rto uint32, maxInFlightSize uint32) { + if sw.IsEmpty() { + return + } + + var lost uint32 + var inFlightSize uint32 + + sw.Visit(func(segment *DataSegment) bool { + if current-segment.timeout >= 0x7FFFFFFF { + return true + } + if segment.transmit == 0 { + // First time + sw.totalInFlightSize++ + } else { + lost++ + } + segment.timeout = current + rto + + segment.Timestamp = current + segment.transmit++ + sw.writer.Write(segment) + inFlightSize++ + return inFlightSize < maxInFlightSize + }) + + if sw.onPacketLoss != nil && inFlightSize > 0 && sw.totalInFlightSize != 0 { + rate := lost * 100 / sw.totalInFlightSize + sw.onPacketLoss(rate) + } +} + +func (sw *SendingWindow) Remove(number uint32) bool { + if sw.IsEmpty() { + return false + } + + for e := sw.cache.Front(); e != nil; e = e.Next() { + seg := e.Value.(*DataSegment) + if seg.Number > number { + return false + } else if seg.Number == number { + if sw.totalInFlightSize > 0 { + sw.totalInFlightSize-- + } + seg.Release() + sw.cache.Remove(e) + return true + } + } + + return false +} + +type SendingWorker struct { + sync.RWMutex + conn *Connection + window *SendingWindow + firstUnacknowledged uint32 + nextNumber uint32 + remoteNextNumber uint32 + controlWindow uint32 + fastResend uint32 + windowSize uint32 + firstUnacknowledgedUpdated bool + closed bool +} + +func NewSendingWorker(kcp *Connection) *SendingWorker { + worker := &SendingWorker{ + conn: kcp, + fastResend: 2, + remoteNextNumber: 32, + controlWindow: kcp.Config.GetSendingInFlightSize(), + windowSize: kcp.Config.GetSendingBufferSize(), + } + worker.window = NewSendingWindow(worker, worker.OnPacketLoss) + return worker +} + +func (w *SendingWorker) Release() { + w.Lock() + w.window.Release() + w.closed = true + w.Unlock() +} + +func (w *SendingWorker) ProcessReceivingNext(nextNumber uint32) { + w.Lock() + defer w.Unlock() + + w.ProcessReceivingNextWithoutLock(nextNumber) +} + +func (w *SendingWorker) ProcessReceivingNextWithoutLock(nextNumber uint32) { + w.window.Clear(nextNumber) + w.FindFirstUnacknowledged() +} + +func (w *SendingWorker) FindFirstUnacknowledged() { + first := w.firstUnacknowledged + if !w.window.IsEmpty() { + w.firstUnacknowledged = w.window.FirstNumber() + } else { + w.firstUnacknowledged = w.nextNumber + } + if first != w.firstUnacknowledged { + w.firstUnacknowledgedUpdated = true + } +} + +func (w *SendingWorker) processAck(number uint32) bool { + // number < v.firstUnacknowledged || number >= v.nextNumber + if number-w.firstUnacknowledged > 0x7FFFFFFF || number-w.nextNumber < 0x7FFFFFFF { + return false + } + + removed := w.window.Remove(number) + if removed { + w.FindFirstUnacknowledged() + } + return removed +} + +func (w *SendingWorker) ProcessSegment(current uint32, seg *AckSegment, rto uint32) { + defer seg.Release() + + w.Lock() + defer w.Unlock() + + if w.closed { + return + } + + if w.remoteNextNumber < seg.ReceivingWindow { + w.remoteNextNumber = seg.ReceivingWindow + } + w.ProcessReceivingNextWithoutLock(seg.ReceivingNext) + + if seg.IsEmpty() { + return + } + + var maxack uint32 + var maxackRemoved bool + for _, number := range seg.NumberList { + removed := w.processAck(number) + if maxack < number { + maxack = number + maxackRemoved = removed + } + } + + if maxackRemoved { + w.window.HandleFastAck(maxack, rto) + if current-seg.Timestamp < 10000 { + w.conn.roundTrip.Update(current-seg.Timestamp, current) + } + } +} + +func (w *SendingWorker) Push(b *buf.Buffer) bool { + w.Lock() + defer w.Unlock() + + if w.closed { + return false + } + + if w.window.Len() > w.windowSize { + return false + } + + w.window.Push(w.nextNumber, b) + w.nextNumber++ + return true +} + +func (w *SendingWorker) Write(seg Segment) error { + dataSeg := seg.(*DataSegment) + + dataSeg.Conv = w.conn.meta.Conversation + dataSeg.SendingNext = w.firstUnacknowledged + dataSeg.Option = 0 + if w.conn.State() == StateReadyToClose { + dataSeg.Option = SegmentOptionClose + } + + return w.conn.output.Write(dataSeg) +} + +func (w *SendingWorker) OnPacketLoss(lossRate uint32) { + if !w.conn.Config.Congestion || w.conn.roundTrip.Timeout() == 0 { + return + } + + if lossRate >= 15 { + w.controlWindow = 3 * w.controlWindow / 4 + } else if lossRate <= 5 { + w.controlWindow += w.controlWindow / 4 + } + if w.controlWindow < 16 { + w.controlWindow = 16 + } + if w.controlWindow > 2*w.conn.Config.GetSendingInFlightSize() { + w.controlWindow = 2 * w.conn.Config.GetSendingInFlightSize() + } +} + +func (w *SendingWorker) Flush(current uint32) { + w.Lock() + + if w.closed { + w.Unlock() + return + } + + cwnd := w.conn.Config.GetSendingInFlightSize() + if cwnd > w.remoteNextNumber-w.firstUnacknowledged { + cwnd = w.remoteNextNumber - w.firstUnacknowledged + } + if w.conn.Config.Congestion && cwnd > w.controlWindow { + cwnd = w.controlWindow + } + + cwnd *= 20 // magic + + if !w.window.IsEmpty() { + w.window.Flush(current, w.conn.roundTrip.Timeout(), cwnd) + w.firstUnacknowledgedUpdated = false + } + + updated := w.firstUnacknowledgedUpdated + w.firstUnacknowledgedUpdated = false + + w.Unlock() + + if updated { + w.conn.Ping(current, CommandPing) + } +} + +func (w *SendingWorker) CloseWrite() { + w.Lock() + defer w.Unlock() + + w.window.Clear(0xFFFFFFFF) +} + +func (w *SendingWorker) IsEmpty() bool { + w.RLock() + defer w.RUnlock() + + return w.window.IsEmpty() +} + +func (w *SendingWorker) UpdateNecessary() bool { + return !w.IsEmpty() +} + +func (w *SendingWorker) FirstUnacknowledged() uint32 { + w.RLock() + defer w.RUnlock() + + return w.firstUnacknowledged +} diff --git a/transport/internet/kcp/xor.go b/transport/internet/kcp/xor.go new file mode 100644 index 00000000..6de9d295 --- /dev/null +++ b/transport/internet/kcp/xor.go @@ -0,0 +1,17 @@ +// +build !amd64 + +package kcp + +// xorfwd performs XOR forwards in words, x[i] ^= x[i-4], i from 0 to len +func xorfwd(x []byte) { + for i := 4; i < len(x); i++ { + x[i] ^= x[i-4] + } +} + +// xorbkd performs XOR backwords in words, x[i] ^= x[i-4], i from len to 0 +func xorbkd(x []byte) { + for i := len(x) - 1; i >= 4; i-- { + x[i] ^= x[i-4] + } +} diff --git a/transport/internet/kcp/xor_amd64.go b/transport/internet/kcp/xor_amd64.go new file mode 100644 index 00000000..94a4dfc8 --- /dev/null +++ b/transport/internet/kcp/xor_amd64.go @@ -0,0 +1,7 @@ +package kcp + +//go:noescape +func xorfwd(x []byte) + +//go:noescape +func xorbkd(x []byte) diff --git a/transport/internet/kcp/xor_amd64.s b/transport/internet/kcp/xor_amd64.s new file mode 100644 index 00000000..0c2759d7 --- /dev/null +++ b/transport/internet/kcp/xor_amd64.s @@ -0,0 +1,47 @@ +#include "textflag.h" + +// func xorfwd(x []byte) +TEXT ·xorfwd(SB),NOSPLIT,$0 + MOVQ x+0(FP), SI // x[i] + MOVQ x_len+8(FP), CX // x.len + MOVQ x+0(FP), DI + ADDQ $4, DI // x[i+4] + SUBQ $4, CX +xorfwdloop: + MOVL (SI), AX + XORL AX, (DI) + ADDQ $4, SI + ADDQ $4, DI + SUBQ $4, CX + + CMPL CX, $0 + JE xorfwddone + + JMP xorfwdloop +xorfwddone: + RET + +// func xorbkd(x []byte) +TEXT ·xorbkd(SB),NOSPLIT,$0 + MOVQ x+0(FP), SI + MOVQ x_len+8(FP), CX // x.len + MOVQ x+0(FP), DI + ADDQ CX, SI // x[-8] + SUBQ $8, SI + ADDQ CX, DI // x[-4] + SUBQ $4, DI + SUBQ $4, CX +xorbkdloop: + MOVL (SI), AX + XORL AX, (DI) + SUBQ $4, SI + SUBQ $4, DI + SUBQ $4, CX + + CMPL CX, $0 + JE xorbkddone + + JMP xorbkdloop + +xorbkddone: + RET diff --git a/transport/internet/memory_settings.go b/transport/internet/memory_settings.go new file mode 100644 index 00000000..3c4770d2 --- /dev/null +++ b/transport/internet/memory_settings.go @@ -0,0 +1,38 @@ +package internet + +// MemoryStreamConfig is a parsed form of StreamConfig. This is used to reduce number of Protobuf parsing. +type MemoryStreamConfig struct { + ProtocolName string + ProtocolSettings interface{} + SecurityType string + SecuritySettings interface{} + SocketSettings *SocketConfig +} + +// ToMemoryStreamConfig converts a StreamConfig to MemoryStreamConfig. It returns a default non-nil MemoryStreamConfig for nil input. +func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) { + ets, err := s.GetEffectiveTransportSettings() + if err != nil { + return nil, err + } + + mss := &MemoryStreamConfig{ + ProtocolName: s.GetEffectiveProtocol(), + ProtocolSettings: ets, + } + + if s != nil { + mss.SocketSettings = s.SocketSettings + } + + if s != nil && s.HasSecuritySettings() { + ess, err := s.GetEffectiveSecuritySettings() + if err != nil { + return nil, err + } + mss.SecurityType = s.SecurityType + mss.SecuritySettings = ess + } + + return mss, nil +} diff --git a/transport/internet/quic/config.go b/transport/internet/quic/config.go new file mode 100644 index 00000000..79e7d132 --- /dev/null +++ b/transport/internet/quic/config.go @@ -0,0 +1,49 @@ +// +build !confonly + +package quic + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol" + "github.com/xtls/xray-core/v1/transport/internet" + "golang.org/x/crypto/chacha20poly1305" +) + +func getAuth(config *Config) (cipher.AEAD, error) { + security := config.Security.GetSecurityType() + if security == protocol.SecurityType_NONE { + return nil, nil + } + + salted := []byte(config.Key + "xray-quic-salt") + key := sha256.Sum256(salted) + + if security == protocol.SecurityType_AES128_GCM { + block, err := aes.NewCipher(key[:16]) + common.Must(err) + return cipher.NewGCM(block) + } + + if security == protocol.SecurityType_CHACHA20_POLY1305 { + return chacha20poly1305.New(key[:]) + } + + return nil, newError("unsupported security type") +} + +func getHeader(config *Config) (internet.PacketHeader, error) { + if config.Header == nil { + return nil, nil + } + + msg, err := config.Header.GetInstance() + if err != nil { + return nil, err + } + + return internet.CreatePacketHeader(msg) +} diff --git a/transport/internet/quic/config.pb.go b/transport/internet/quic/config.pb.go new file mode 100644 index 00000000..b58ab3f5 --- /dev/null +++ b/transport/internet/quic/config.pb.go @@ -0,0 +1,190 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/quic/config.proto + +package quic + +import ( + proto "github.com/golang/protobuf/proto" + protocol "github.com/xtls/xray-core/v1/common/protocol" + 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 + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Security *protocol.SecurityConfig `protobuf:"bytes,2,opt,name=security,proto3" json:"security,omitempty"` + Header *serial.TypedMessage `protobuf:"bytes,3,opt,name=header,proto3" json:"header,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_quic_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_transport_internet_quic_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_transport_internet_quic_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Config) GetSecurity() *protocol.SecurityConfig { + if x != nil { + return x.Security + } + return nil +} + +func (x *Config) GetHeader() *serial.TypedMessage { + if x != nil { + return x.Header + } + return nil +} + +var File_transport_internet_quic_config_proto protoreflect.FileDescriptor + +var file_transport_internet_quic_config_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x71, 0x75, 0x69, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x71, 0x75, 0x69, 0x63, 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, 0x1a, 0x1d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x96, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x73, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, + 0x03, 0x20, 0x01, 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, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x42, + 0x79, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x71, + 0x75, 0x69, 0x63, 0x50, 0x01, 0x5a, 0x34, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x71, 0x75, 0x69, 0x63, 0xaa, 0x02, 0x1c, 0x58, 0x72, + 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_transport_internet_quic_config_proto_rawDescOnce sync.Once + file_transport_internet_quic_config_proto_rawDescData = file_transport_internet_quic_config_proto_rawDesc +) + +func file_transport_internet_quic_config_proto_rawDescGZIP() []byte { + file_transport_internet_quic_config_proto_rawDescOnce.Do(func() { + file_transport_internet_quic_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_quic_config_proto_rawDescData) + }) + return file_transport_internet_quic_config_proto_rawDescData +} + +var file_transport_internet_quic_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_quic_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.quic.Config + (*protocol.SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig + (*serial.TypedMessage)(nil), // 2: xray.common.serial.TypedMessage +} +var file_transport_internet_quic_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.quic.Config.security:type_name -> xray.common.protocol.SecurityConfig + 2, // 1: xray.transport.internet.quic.Config.header:type_name -> xray.common.serial.TypedMessage + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_transport_internet_quic_config_proto_init() } +func file_transport_internet_quic_config_proto_init() { + if File_transport_internet_quic_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_quic_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_transport_internet_quic_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_quic_config_proto_goTypes, + DependencyIndexes: file_transport_internet_quic_config_proto_depIdxs, + MessageInfos: file_transport_internet_quic_config_proto_msgTypes, + }.Build() + File_transport_internet_quic_config_proto = out.File + file_transport_internet_quic_config_proto_rawDesc = nil + file_transport_internet_quic_config_proto_goTypes = nil + file_transport_internet_quic_config_proto_depIdxs = nil +} diff --git a/transport/internet/quic/config.proto b/transport/internet/quic/config.proto new file mode 100644 index 00000000..92888d24 --- /dev/null +++ b/transport/internet/quic/config.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.transport.internet.quic; +option csharp_namespace = "Xray.Transport.Internet.Quic"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/quic"; +option java_package = "com.xray.transport.internet.quic"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; +import "common/protocol/headers.proto"; + +message Config { + string key = 1; + xray.common.protocol.SecurityConfig security = 2; + xray.common.serial.TypedMessage header = 3; +} diff --git a/transport/internet/quic/conn.go b/transport/internet/quic/conn.go new file mode 100644 index 00000000..23f3c279 --- /dev/null +++ b/transport/internet/quic/conn.go @@ -0,0 +1,187 @@ +// +build !confonly + +package quic + +import ( + "crypto/cipher" + "crypto/rand" + "errors" + "time" + + "github.com/lucas-clemente/quic-go" + "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/transport/internet" +) + +type sysConn struct { + conn net.PacketConn + header internet.PacketHeader + auth cipher.AEAD +} + +func wrapSysConn(rawConn net.PacketConn, config *Config) (*sysConn, error) { + header, err := getHeader(config) + if err != nil { + return nil, err + } + auth, err := getAuth(config) + if err != nil { + return nil, err + } + return &sysConn{ + conn: rawConn, + header: header, + auth: auth, + }, nil +} + +var errInvalidPacket = errors.New("invalid packet") + +func (c *sysConn) readFromInternal(p []byte) (int, net.Addr, error) { + buffer := getBuffer() + defer putBuffer(buffer) + + nBytes, addr, err := c.conn.ReadFrom(buffer) + if err != nil { + return 0, nil, err + } + + payload := buffer[:nBytes] + if c.header != nil { + if len(payload) <= int(c.header.Size()) { + return 0, nil, errInvalidPacket + } + payload = payload[c.header.Size():] + } + + if c.auth == nil { + n := copy(p, payload) + return n, addr, nil + } + + if len(payload) <= c.auth.NonceSize() { + return 0, nil, errInvalidPacket + } + + nonce := payload[:c.auth.NonceSize()] + payload = payload[c.auth.NonceSize():] + + p, err = c.auth.Open(p[:0], nonce, payload, nil) + if err != nil { + return 0, nil, errInvalidPacket + } + + return len(p), addr, nil +} + +func (c *sysConn) ReadFrom(p []byte) (int, net.Addr, error) { + if c.header == nil && c.auth == nil { + return c.conn.ReadFrom(p) + } + + for { + n, addr, err := c.readFromInternal(p) + if err != nil && err != errInvalidPacket { + return 0, nil, err + } + if err == nil { + return n, addr, nil + } + } +} + +func (c *sysConn) WriteTo(p []byte, addr net.Addr) (int, error) { + if c.header == nil && c.auth == nil { + return c.conn.WriteTo(p, addr) + } + + buffer := getBuffer() + defer putBuffer(buffer) + + payload := buffer + n := 0 + if c.header != nil { + c.header.Serialize(payload) + n = int(c.header.Size()) + } + + if c.auth == nil { + nBytes := copy(payload[n:], p) + n += nBytes + } else { + nounce := payload[n : n+c.auth.NonceSize()] + common.Must2(rand.Read(nounce)) + n += c.auth.NonceSize() + pp := c.auth.Seal(payload[:n], nounce, p, nil) + n = len(pp) + } + + return c.conn.WriteTo(payload[:n], addr) +} + +func (c *sysConn) Close() error { + return c.conn.Close() +} + +func (c *sysConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *sysConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *sysConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *sysConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +type interConn struct { + stream quic.Stream + local net.Addr + remote net.Addr +} + +func (c *interConn) Read(b []byte) (int, error) { + return c.stream.Read(b) +} + +func (c *interConn) WriteMultiBuffer(mb buf.MultiBuffer) error { + mb = buf.Compact(mb) + mb, err := buf.WriteMultiBuffer(c, mb) + buf.ReleaseMulti(mb) + return err +} + +func (c *interConn) Write(b []byte) (int, error) { + return c.stream.Write(b) +} + +func (c *interConn) Close() error { + return c.stream.Close() +} + +func (c *interConn) LocalAddr() net.Addr { + return c.local +} + +func (c *interConn) RemoteAddr() net.Addr { + return c.remote +} + +func (c *interConn) SetDeadline(t time.Time) error { + return c.stream.SetDeadline(t) +} + +func (c *interConn) SetReadDeadline(t time.Time) error { + return c.stream.SetReadDeadline(t) +} + +func (c *interConn) SetWriteDeadline(t time.Time) error { + return c.stream.SetWriteDeadline(t) +} diff --git a/transport/internet/quic/dialer.go b/transport/internet/quic/dialer.go new file mode 100644 index 00000000..30029d5c --- /dev/null +++ b/transport/internet/quic/dialer.go @@ -0,0 +1,218 @@ +// +build !confonly + +package quic + +import ( + "context" + "sync" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/task" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +type sessionContext struct { + rawConn *sysConn + session quic.Session +} + +var errSessionClosed = newError("session closed") + +func (c *sessionContext) openStream(destAddr net.Addr) (*interConn, error) { + if !isActive(c.session) { + return nil, errSessionClosed + } + + stream, err := c.session.OpenStream() + if err != nil { + return nil, err + } + + conn := &interConn{ + stream: stream, + local: c.session.LocalAddr(), + remote: destAddr, + } + + return conn, nil +} + +type clientSessions struct { + access sync.Mutex + sessions map[net.Destination][]*sessionContext + cleanup *task.Periodic +} + +func isActive(s quic.Session) bool { + select { + case <-s.Context().Done(): + return false + default: + return true + } +} + +func removeInactiveSessions(sessions []*sessionContext) []*sessionContext { + activeSessions := make([]*sessionContext, 0, len(sessions)) + for _, s := range sessions { + if isActive(s.session) { + activeSessions = append(activeSessions, s) + continue + } + if err := s.session.CloseWithError(0, ""); err != nil { + newError("failed to close session").Base(err).WriteToLog() + } + if err := s.rawConn.Close(); err != nil { + newError("failed to close raw connection").Base(err).WriteToLog() + } + } + + if len(activeSessions) < len(sessions) { + return activeSessions + } + + return sessions +} + +func openStream(sessions []*sessionContext, destAddr net.Addr) *interConn { + for _, s := range sessions { + if !isActive(s.session) { + continue + } + + conn, err := s.openStream(destAddr) + if err != nil { + continue + } + + return conn + } + + return nil +} + +func (s *clientSessions) cleanSessions() error { + s.access.Lock() + defer s.access.Unlock() + + if len(s.sessions) == 0 { + return nil + } + + newSessionMap := make(map[net.Destination][]*sessionContext) + + for dest, sessions := range s.sessions { + sessions = removeInactiveSessions(sessions) + if len(sessions) > 0 { + newSessionMap[dest] = sessions + } + } + + s.sessions = newSessionMap + return nil +} + +func (s *clientSessions) openConnection(destAddr net.Addr, config *Config, tlsConfig *tls.Config, sockopt *internet.SocketConfig) (internet.Connection, error) { + s.access.Lock() + defer s.access.Unlock() + + if s.sessions == nil { + s.sessions = make(map[net.Destination][]*sessionContext) + } + + dest := net.DestinationFromAddr(destAddr) + + var sessions []*sessionContext + if s, found := s.sessions[dest]; found { + sessions = s + } + + if true { + conn := openStream(sessions, destAddr) + if conn != nil { + return conn, nil + } + } + + sessions = removeInactiveSessions(sessions) + + rawConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, sockopt) + if err != nil { + return nil, err + } + + quicConfig := &quic.Config{ + ConnectionIDLength: 12, + HandshakeTimeout: time.Second * 8, + MaxIdleTimeout: time.Second * 30, + } + + conn, err := wrapSysConn(rawConn, config) + if err != nil { + rawConn.Close() + return nil, err + } + + session, err := quic.DialContext(context.Background(), conn, destAddr, "", tlsConfig.GetTLSConfig(tls.WithDestination(dest)), quicConfig) + if err != nil { + conn.Close() + return nil, err + } + + context := &sessionContext{ + session: session, + rawConn: conn, + } + s.sessions[dest] = append(sessions, context) + return context.openStream(destAddr) +} + +var client clientSessions + +func init() { + client.sessions = make(map[net.Destination][]*sessionContext) + client.cleanup = &task.Periodic{ + Interval: time.Minute, + Execute: client.cleanSessions, + } + common.Must(client.cleanup.Start()) +} + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + tlsConfig = &tls.Config{ + ServerName: internalDomain, + AllowInsecure: true, + } + } + + var destAddr *net.UDPAddr + if dest.Address.Family().IsIP() { + destAddr = &net.UDPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + } + } else { + addr, err := net.ResolveUDPAddr("udp", dest.NetAddr()) + if err != nil { + return nil, err + } + destAddr = addr + } + + config := streamSettings.ProtocolSettings.(*Config) + + return client.openConnection(destAddr, config, tlsConfig, streamSettings.SocketSettings) +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/quic/errors.generated.go b/transport/internet/quic/errors.generated.go new file mode 100644 index 00000000..b64d6f39 --- /dev/null +++ b/transport/internet/quic/errors.generated.go @@ -0,0 +1,9 @@ +package quic + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/quic/hub.go b/transport/internet/quic/hub.go new file mode 100644 index 00000000..73fb7700 --- /dev/null +++ b/transport/internet/quic/hub.go @@ -0,0 +1,140 @@ +// +build !confonly + +package quic + +import ( + "context" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/common/signal/done" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +// Listener is an internet.Listener that listens for TCP connections. +type Listener struct { + rawConn *sysConn + listener quic.Listener + done *done.Instance + addConn internet.ConnHandler +} + +func (l *Listener) acceptStreams(session quic.Session) { + for { + stream, err := session.AcceptStream(context.Background()) + if err != nil { + newError("failed to accept stream").Base(err).WriteToLog() + select { + case <-session.Context().Done(): + return + case <-l.done.Wait(): + if err := session.CloseWithError(0, ""); err != nil { + newError("failed to close session").Base(err).WriteToLog() + } + return + default: + time.Sleep(time.Second) + continue + } + } + + conn := &interConn{ + stream: stream, + local: session.LocalAddr(), + remote: session.RemoteAddr(), + } + + l.addConn(conn) + } +} + +func (l *Listener) keepAccepting() { + for { + conn, err := l.listener.Accept(context.Background()) + if err != nil { + newError("failed to accept QUIC sessions").Base(err).WriteToLog() + if l.done.Done() { + break + } + time.Sleep(time.Second) + continue + } + go l.acceptStreams(conn) + } +} + +// Addr implements internet.Listener.Addr. +func (l *Listener) Addr() net.Addr { + return l.listener.Addr() +} + +// Close implements internet.Listener.Close. +func (l *Listener) Close() error { + l.done.Close() + l.listener.Close() + l.rawConn.Close() + return nil +} + +// Listen creates a new Listener based on configurations. +func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + if address.Family().IsDomain() { + return nil, newError("domain address is not allows for listening quic") + } + + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + tlsConfig = &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.DNSNames(internalDomain), cert.CommonName(internalDomain)))}, + } + } + + config := streamSettings.ProtocolSettings.(*Config) + rawConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + + if err != nil { + return nil, err + } + + quicConfig := &quic.Config{ + ConnectionIDLength: 12, + HandshakeTimeout: time.Second * 8, + MaxIdleTimeout: time.Second * 45, + MaxIncomingStreams: 32, + MaxIncomingUniStreams: -1, + } + + conn, err := wrapSysConn(rawConn, config) + if err != nil { + conn.Close() + return nil, err + } + + qListener, err := quic.Listen(conn, tlsConfig.GetTLSConfig(), quicConfig) + if err != nil { + conn.Close() + return nil, err + } + + listener := &Listener{ + done: done.New(), + rawConn: conn, + listener: qListener, + addConn: handler, + } + + go listener.keepAccepting() + + return listener, nil +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/transport/internet/quic/pool.go b/transport/internet/quic/pool.go new file mode 100644 index 00000000..2809520a --- /dev/null +++ b/transport/internet/quic/pool.go @@ -0,0 +1,23 @@ +// +build !confonly + +package quic + +import ( + "sync" + + "github.com/xtls/xray-core/v1/common/bytespool" +) + +var pool *sync.Pool + +func init() { + pool = bytespool.GetPool(2048) +} + +func getBuffer() []byte { + return pool.Get().([]byte) +} + +func putBuffer(p []byte) { + pool.Put(p) +} diff --git a/transport/internet/quic/quic.go b/transport/internet/quic/quic.go new file mode 100644 index 00000000..47c3cc94 --- /dev/null +++ b/transport/internet/quic/quic.go @@ -0,0 +1,25 @@ +// +build !confonly + +package quic + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +// Here is some modification needs to be done before update quic vendor. +// * use bytespool in buffer_pool.go +// * set MaxReceivePacketSize to 1452 - 32 (16 bytes auth, 16 bytes head) +// +// + +const protocolName = "quic" +const internalDomain = "quic.internal.example.com" + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/quic/quic_test.go b/transport/internet/quic/quic_test.go new file mode 100644 index 00000000..defa80a6 --- /dev/null +++ b/transport/internet/quic/quic_test.go @@ -0,0 +1,223 @@ +package quic_test + +import ( + "context" + "crypto/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "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/protocol" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/common/serial" + "github.com/xtls/xray-core/v1/testing/servers/udp" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/headers/wireguard" + "github.com/xtls/xray-core/v1/transport/internet/quic" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +func TestQuicConnection(t *testing.T) { + port := udp.PickPort() + + listener, err := quic.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{ + tls.ParseCertificate( + cert.MustGenerate(nil, + cert.DNSNames("www.example.com"), + ), + ), + }, + }, + }, func(conn internet.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + return + } + common.Must2(conn.Write(b.Bytes())) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + dctx := context.Background() + conn, err := quic.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + ServerName: "www.example.com", + AllowInsecure: true, + }, + }) + common.Must(err) + defer conn.Close() + + const N = 1024 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} + +func TestQuicConnectionWithoutTLS(t *testing.T) { + port := udp.PickPort() + + listener, err := quic.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{}, + }, func(conn internet.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + return + } + common.Must2(conn.Write(b.Bytes())) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + dctx := context.Background() + conn, err := quic.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{}, + }) + common.Must(err) + defer conn.Close() + + const N = 1024 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} + +func TestQuicConnectionAuthHeader(t *testing.T) { + port := udp.PickPort() + + listener, err := quic.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{ + Header: serial.ToTypedMessage(&wireguard.WireguardConfig{}), + Key: "abcd", + Security: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }, + }, func(conn internet.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + return + } + common.Must2(conn.Write(b.Bytes())) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + dctx := context.Background() + conn, err := quic.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "quic", + ProtocolSettings: &quic.Config{ + Header: serial.ToTypedMessage(&wireguard.WireguardConfig{}), + Key: "abcd", + Security: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }, + }) + common.Must(err) + defer conn.Close() + + const N = 1024 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} diff --git a/transport/internet/sockopt.go b/transport/internet/sockopt.go new file mode 100644 index 00000000..7facf30f --- /dev/null +++ b/transport/internet/sockopt.go @@ -0,0 +1,19 @@ +package internet + +func isTCPSocket(network string) bool { + switch network { + case "tcp", "tcp4", "tcp6": + return true + default: + return false + } +} + +func isUDPSocket(network string) bool { + switch network { + case "udp", "udp4", "udp6": + return true + default: + return false + } +} diff --git a/transport/internet/sockopt_darwin.go b/transport/internet/sockopt_darwin.go new file mode 100644 index 00000000..e6281923 --- /dev/null +++ b/transport/internet/sockopt_darwin.go @@ -0,0 +1,60 @@ +package internet + +import ( + "syscall" +) + +const ( + // TCP_FASTOPEN is the socket option on darwin for TCP fast open. + TCP_FASTOPEN = 0x105 + // TCP_FASTOPEN_SERVER is the value to enable TCP fast open on darwin for server connections. + TCP_FASTOPEN_SERVER = 0x01 + // TCP_FASTOPEN_CLIENT is the value to enable TCP fast open on darwin for client connections. + TCP_FASTOPEN_CLIENT = 0x02 +) + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, TCP_FASTOPEN_CLIENT); err != nil { + return err + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, 0); err != nil { + return err + } + } + } + + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, TCP_FASTOPEN_SERVER); err != nil { + return err + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, TCP_FASTOPEN, 0); err != nil { + return err + } + } + } + + return nil +} + +func bindAddr(fd uintptr, address []byte, port uint32) error { + return nil +} + +func setReuseAddr(fd uintptr) error { + return nil +} + +func setReusePort(fd uintptr) error { + return nil +} diff --git a/transport/internet/sockopt_freebsd.go b/transport/internet/sockopt_freebsd.go new file mode 100644 index 00000000..48c5eda6 --- /dev/null +++ b/transport/internet/sockopt_freebsd.go @@ -0,0 +1,230 @@ +package internet + +import ( + "encoding/binary" + "net" + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + sysPFINOUT = 0x0 + sysPFIN = 0x1 + sysPFOUT = 0x2 + sysPFFWD = 0x3 + sysDIOCNATLOOK = 0xc04c4417 +) + +type pfiocNatlook struct { + Saddr [16]byte /* pf_addr */ + Daddr [16]byte /* pf_addr */ + Rsaddr [16]byte /* pf_addr */ + Rdaddr [16]byte /* pf_addr */ + Sport uint16 + Dport uint16 + Rsport uint16 + Rdport uint16 + Af uint8 + Proto uint8 + Direction uint8 + Pad [1]byte +} + +const ( + sizeofPfiocNatlook = 0x4c + soReUsePort = 0x00000200 + soReUsePortLB = 0x00010000 +) + +func ioctl(s uintptr, ioc int, b []byte) error { + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, s, uintptr(ioc), uintptr(unsafe.Pointer(&b[0]))); errno != 0 { + return error(errno) + } + return nil +} +func (nl *pfiocNatlook) rdPort() int { + return int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&nl.Rdport))[:])) +} + +func (nl *pfiocNatlook) setPort(remote, local int) { + binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&nl.Sport))[:], uint16(remote)) + binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&nl.Dport))[:], uint16(local)) +} + +// OriginalDst uses ioctl to read original destination from /dev/pf +func OriginalDst(la, ra net.Addr) (net.IP, int, error) { + f, err := os.Open("/dev/pf") + if err != nil { + return net.IP{}, -1, newError("failed to open device /dev/pf").Base(err) + } + defer f.Close() + fd := f.Fd() + b := make([]byte, sizeofPfiocNatlook) + nl := (*pfiocNatlook)(unsafe.Pointer(&b[0])) + var raIP, laIP net.IP + var raPort, laPort int + switch la.(type) { + case *net.TCPAddr: + raIP = ra.(*net.TCPAddr).IP + laIP = la.(*net.TCPAddr).IP + raPort = ra.(*net.TCPAddr).Port + laPort = la.(*net.TCPAddr).Port + nl.Proto = syscall.IPPROTO_TCP + case *net.UDPAddr: + raIP = ra.(*net.UDPAddr).IP + laIP = la.(*net.UDPAddr).IP + raPort = ra.(*net.UDPAddr).Port + laPort = la.(*net.UDPAddr).Port + nl.Proto = syscall.IPPROTO_UDP + } + if raIP.To4() != nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("127.0.0.1") + } + copy(nl.Saddr[:net.IPv4len], raIP.To4()) + copy(nl.Daddr[:net.IPv4len], laIP.To4()) + nl.Af = syscall.AF_INET + } + if raIP.To16() != nil && raIP.To4() == nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("::1") + } + copy(nl.Saddr[:], raIP) + copy(nl.Daddr[:], laIP) + nl.Af = syscall.AF_INET6 + } + nl.setPort(raPort, laPort) + ioc := uintptr(sysDIOCNATLOOK) + for _, dir := range []byte{sysPFOUT, sysPFIN} { + nl.Direction = dir + err = ioctl(fd, int(ioc), b) + if err == nil || err != syscall.ENOENT { + break + } + } + if err != nil { + return net.IP{}, -1, os.NewSyscallError("ioctl", err) + } + + odPort := nl.rdPort() + var odIP net.IP + switch nl.Af { + case syscall.AF_INET: + odIP = make(net.IP, net.IPv4len) + copy(odIP, nl.Rdaddr[:net.IPv4len]) + case syscall.AF_INET6: + odIP = make(net.IP, net.IPv6len) + copy(odIP, nl.Rdaddr[:]) + } + return odIP, odPort, nil +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_USER_COOKIE, int(config.Mark)); err != nil { + return newError("failed to set SO_USER_COOKIE").Base(err) + } + } + + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, 1); err != nil { + return newError("failed to set TCP_FASTOPEN_CONNECT=1").Base(err) + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, 0); err != nil { + return newError("failed to set TCP_FASTOPEN_CONNECT=0").Base(err) + } + } + } + + if config.Tproxy.IsEnabled() { + ip, _, _ := net.SplitHostPort(address) + if net.ParseIP(ip).To4() != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BINDANY, 1); err != nil { + return newError("failed to set outbound IP_BINDANY").Base(err) + } + } else { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BINDANY, 1); err != nil { + return newError("failed to set outbound IPV6_BINDANY").Base(err) + } + } + } + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_USER_COOKIE, int(config.Mark)); err != nil { + return newError("failed to set SO_USER_COOKIE").Base(err) + } + } + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, 1); err != nil { + return newError("failed to set TCP_FASTOPEN=1").Base(err) + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, 0); err != nil { + return newError("failed to set TCP_FASTOPEN=0").Base(err) + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BINDANY, 1); err != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BINDANY, 1); err != nil { + return newError("failed to set inbound IP_BINDANY").Base(err) + } + } + } + + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + setReuseAddr(fd) + setReusePort(fd) + + var sockaddr syscall.Sockaddr + + switch len(ip) { + case net.IPv4len: + a4 := &syscall.SockaddrInet4{ + Port: int(port), + } + copy(a4.Addr[:], ip) + sockaddr = a4 + case net.IPv6len: + a6 := &syscall.SockaddrInet6{ + Port: int(port), + } + copy(a6.Addr[:], ip) + sockaddr = a6 + default: + return newError("unexpected length of ip") + } + + return syscall.Bind(int(fd), sockaddr) +} + +func setReuseAddr(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return newError("failed to set SO_REUSEADDR").Base(err).AtWarning() + } + return nil +} + +func setReusePort(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, soReUsePortLB, 1); err != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, soReUsePort, 1); err != nil { + return newError("failed to set SO_REUSEPORT").Base(err).AtWarning() + } + } + return nil +} diff --git a/transport/internet/sockopt_linux.go b/transport/internet/sockopt_linux.go new file mode 100644 index 00000000..9c1c8f31 --- /dev/null +++ b/transport/internet/sockopt_linux.go @@ -0,0 +1,120 @@ +package internet + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +const ( + // For incoming connections. + TCP_FASTOPEN = 23 + // For out-going connections. + TCP_FASTOPEN_CONNECT = 30 +) + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + setReuseAddr(fd) + setReusePort(fd) + + var sockaddr syscall.Sockaddr + + switch len(ip) { + case net.IPv4len: + a4 := &syscall.SockaddrInet4{ + Port: int(port), + } + copy(a4.Addr[:], ip) + sockaddr = a4 + case net.IPv6len: + a6 := &syscall.SockaddrInet6{ + Port: int(port), + } + copy(a6.Addr[:], ip) + sockaddr = a6 + default: + return newError("unexpected length of ip") + } + + return syscall.Bind(int(fd), sockaddr) +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.Mark)); err != nil { + return newError("failed to set SO_MARK").Base(err) + } + } + + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN_CONNECT, 1); err != nil { + return newError("failed to set TCP_FASTOPEN_CONNECT=1").Base(err) + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN_CONNECT, 0); err != nil { + return newError("failed to set TCP_FASTOPEN_CONNECT=0").Base(err) + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + return newError("failed to set IP_TRANSPARENT").Base(err) + } + } + + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.Mark)); err != nil { + return newError("failed to set SO_MARK").Base(err) + } + } + if isTCPSocket(network) { + switch config.Tfo { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN, 1); err != nil { + return newError("failed to set TCP_FASTOPEN=1").Base(err) + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, TCP_FASTOPEN, 0); err != nil { + return newError("failed to set TCP_FASTOPEN=0").Base(err) + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + return newError("failed to set IP_TRANSPARENT").Base(err) + } + } + + if config.ReceiveOriginalDestAddress && isUDPSocket(network) { + err1 := syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) + err2 := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) + if err1 != nil && err2 != nil { + return err1 + } + } + + return nil +} + +func setReuseAddr(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return newError("failed to set SO_REUSEADDR").Base(err).AtWarning() + } + return nil +} + +func setReusePort(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + return newError("failed to set SO_REUSEPORT").Base(err).AtWarning() + } + return nil +} diff --git a/transport/internet/sockopt_linux_test.go b/transport/internet/sockopt_linux_test.go new file mode 100644 index 00000000..40745d22 --- /dev/null +++ b/transport/internet/sockopt_linux_test.go @@ -0,0 +1,42 @@ +package internet_test + +import ( + "context" + "syscall" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + . "github.com/xtls/xray-core/v1/transport/internet" +) + +func TestSockOptMark(t *testing.T) { + t.Skip("requires CAP_NET_ADMIN") + + tcpServer := tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + const mark = 1 + dialer := DefaultSystemDialer{} + conn, err := dialer.Dial(context.Background(), nil, dest, &SocketConfig{Mark: mark}) + common.Must(err) + defer conn.Close() + + rawConn, err := conn.(*net.TCPConn).SyscallConn() + common.Must(err) + err = rawConn.Control(func(fd uintptr) { + m, err := syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK) + common.Must(err) + if mark != m { + t.Fatal("unexpected connection mark", m, " want ", mark) + } + }) + common.Must(err) +} diff --git a/transport/internet/sockopt_other.go b/transport/internet/sockopt_other.go new file mode 100644 index 00000000..ac61296b --- /dev/null +++ b/transport/internet/sockopt_other.go @@ -0,0 +1,23 @@ +// +build js dragonfly netbsd openbsd solaris + +package internet + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + return nil +} + +func setReuseAddr(fd uintptr) error { + return nil +} + +func setReusePort(fd uintptr) error { + return nil +} diff --git a/transport/internet/sockopt_test.go b/transport/internet/sockopt_test.go new file mode 100644 index 00000000..a1944cbd --- /dev/null +++ b/transport/internet/sockopt_test.go @@ -0,0 +1,40 @@ +package internet_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + . "github.com/xtls/xray-core/v1/transport/internet" +) + +func TestTCPFastOpen(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.StartContext(context.Background(), &SocketConfig{Tfo: SocketConfig_Enable}) + common.Must(err) + defer tcpServer.Close() + + ctx := context.Background() + dialer := DefaultSystemDialer{} + conn, err := dialer.Dial(ctx, nil, dest, &SocketConfig{ + Tfo: SocketConfig_Enable, + }) + common.Must(err) + defer conn.Close() + + _, err = conn.Write([]byte("abcd")) + common.Must(err) + + b := buf.New() + common.Must2(b.ReadFrom(conn)) + if r := cmp.Diff(b.Bytes(), []byte("abcd")); r != "" { + t.Fatal(r) + } +} diff --git a/transport/internet/sockopt_windows.go b/transport/internet/sockopt_windows.go new file mode 100644 index 00000000..cb6292bc --- /dev/null +++ b/transport/internet/sockopt_windows.go @@ -0,0 +1,56 @@ +package internet + +import ( + "syscall" +) + +const ( + TCP_FASTOPEN = 15 +) + +func setTFO(fd syscall.Handle, settings SocketConfig_TCPFastOpenState) error { + switch settings { + case SocketConfig_Enable: + if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, TCP_FASTOPEN, 1); err != nil { + return err + } + case SocketConfig_Disable: + if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, TCP_FASTOPEN, 0); err != nil { + return err + } + } + return nil +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + if err := setTFO(syscall.Handle(fd), config.Tfo); err != nil { + return err + } + + } + + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + if err := setTFO(syscall.Handle(fd), config.Tfo); err != nil { + return err + } + } + + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + return nil +} + +func setReuseAddr(fd uintptr) error { + return nil +} + +func setReusePort(fd uintptr) error { + return nil +} diff --git a/transport/internet/system_dialer.go b/transport/internet/system_dialer.go new file mode 100644 index 00000000..4a1052eb --- /dev/null +++ b/transport/internet/system_dialer.go @@ -0,0 +1,185 @@ +package internet + +import ( + "context" + "syscall" + "time" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/session" +) + +var ( + effectiveSystemDialer SystemDialer = &DefaultSystemDialer{} +) + +type SystemDialer interface { + Dial(ctx context.Context, source net.Address, destination net.Destination, sockopt *SocketConfig) (net.Conn, error) +} + +type DefaultSystemDialer struct { + controllers []controller +} + +func resolveSrcAddr(network net.Network, src net.Address) net.Addr { + if src == nil || src == net.AnyIP { + return nil + } + + if network == net.Network_TCP { + return &net.TCPAddr{ + IP: src.IP(), + Port: 0, + } + } + + return &net.UDPAddr{ + IP: src.IP(), + Port: 0, + } +} + +func hasBindAddr(sockopt *SocketConfig) bool { + return sockopt != nil && len(sockopt.BindAddress) > 0 && sockopt.BindPort > 0 +} + +func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + if dest.Network == net.Network_UDP && !hasBindAddr(sockopt) { + srcAddr := resolveSrcAddr(net.Network_UDP, src) + if srcAddr == nil { + srcAddr = &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + packetConn, err := ListenSystemPacket(ctx, srcAddr, sockopt) + if err != nil { + return nil, err + } + destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr()) + if err != nil { + return nil, err + } + return &packetConnWrapper{ + conn: packetConn, + dest: destAddr, + }, nil + } + + dialer := &net.Dialer{ + Timeout: time.Second * 16, + DualStack: true, + LocalAddr: resolveSrcAddr(dest.Network, src), + } + + if sockopt != nil || len(d.controllers) > 0 { + dialer.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if sockopt != nil { + if err := applyOutboundSocketOptions(network, address, fd, sockopt); err != nil { + newError("failed to apply socket options").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + if dest.Network == net.Network_UDP && hasBindAddr(sockopt) { + if err := bindAddr(fd, sockopt.BindAddress, sockopt.BindPort); err != nil { + newError("failed to bind source address to ", sockopt.BindAddress).Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } + } + + for _, ctl := range d.controllers { + if err := ctl(network, address, fd); err != nil { + newError("failed to apply external controller").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } + }) + } + } + + return dialer.DialContext(ctx, dest.Network.SystemString(), dest.NetAddr()) +} + +type packetConnWrapper struct { + conn net.PacketConn + dest net.Addr +} + +func (c *packetConnWrapper) Close() error { + return c.conn.Close() +} + +func (c *packetConnWrapper) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *packetConnWrapper) RemoteAddr() net.Addr { + return c.dest +} + +func (c *packetConnWrapper) Write(p []byte) (int, error) { + return c.conn.WriteTo(p, c.dest) +} + +func (c *packetConnWrapper) Read(p []byte) (int, error) { + n, _, err := c.conn.ReadFrom(p) + return n, err +} + +func (c *packetConnWrapper) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *packetConnWrapper) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *packetConnWrapper) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +type SystemDialerAdapter interface { + Dial(network string, address string) (net.Conn, error) +} + +type SimpleSystemDialer struct { + adapter SystemDialerAdapter +} + +func WithAdapter(dialer SystemDialerAdapter) SystemDialer { + return &SimpleSystemDialer{ + adapter: dialer, + } +} + +func (v *SimpleSystemDialer) Dial(ctx context.Context, src net.Address, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + return v.adapter.Dial(dest.Network.SystemString(), dest.NetAddr()) +} + +// UseAlternativeSystemDialer replaces the current system dialer with a given one. +// Caller must ensure there is no race condition. +// +// xray:api:stable +func UseAlternativeSystemDialer(dialer SystemDialer) { + if dialer == nil { + effectiveSystemDialer = &DefaultSystemDialer{} + } + effectiveSystemDialer = dialer +} + +// RegisterDialerController adds a controller to the effective system dialer. +// The controller can be used to operate on file descriptors before they are put into use. +// It only works when effective dialer is the default dialer. +// +// xray:api:beta +func RegisterDialerController(ctl func(network, address string, fd uintptr) error) error { + if ctl == nil { + return newError("nil listener controller") + } + + dialer, ok := effectiveSystemDialer.(*DefaultSystemDialer) + if !ok { + return newError("RegisterListenerController not supported in custom dialer") + } + + dialer.controllers = append(dialer.controllers, ctl) + return nil +} diff --git a/transport/internet/system_listener.go b/transport/internet/system_listener.go new file mode 100644 index 00000000..54ea4cfe --- /dev/null +++ b/transport/internet/system_listener.go @@ -0,0 +1,105 @@ +package internet + +import ( + "context" + "runtime" + "syscall" + + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/session" +) + +var ( + effectiveListener = DefaultListener{} +) + +type controller func(network, address string, fd uintptr) error + +type DefaultListener struct { + controllers []controller +} + +func getControlFunc(ctx context.Context, sockopt *SocketConfig, controllers []controller) func(network, address string, c syscall.RawConn) error { + return func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if sockopt != nil { + if err := applyInboundSocketOptions(network, fd, sockopt); err != nil { + newError("failed to apply socket options to incoming connection").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } + + setReusePort(fd) + + for _, controller := range controllers { + if err := controller(network, address, fd); err != nil { + newError("failed to apply external controller").Base(err).WriteToLog(session.ExportIDToError(ctx)) + } + } + }) + } +} + +func (dl *DefaultListener) Listen(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.Listener, error) { + var lc net.ListenConfig + var l net.Listener + var err error + var network, address string + switch addr := addr.(type) { + case *net.TCPAddr: + network = addr.Network() + address = addr.String() + lc.Control = getControlFunc(ctx, sockopt, dl.controllers) + case *net.UnixAddr: + lc.Control = nil + network = addr.Network() + address = addr.Name + if runtime.GOOS == "linux" && address[0] == '@' { + // linux abstract unix domain socket is lockfree + if len(address) > 1 && address[1] == '@' { + // but may need padding to work with haproxy + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, address[1:]) + address = string(fullAddr) + } + } else { + // normal unix domain socket needs lock + locker := &FileLocker{ + path: address + ".lock", + } + err := locker.Acquire() + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, address, locker) + } + } + + l, err = lc.Listen(ctx, network, address) + if sockopt != nil && sockopt.AcceptProxyProtocol { + policyFunc := func(upstream net.Addr) (proxyproto.Policy, error) { return proxyproto.REQUIRE, nil } + l = &proxyproto.Listener{Listener: l, Policy: policyFunc} + } + return l, err +} + +func (dl *DefaultListener) ListenPacket(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.PacketConn, error) { + var lc net.ListenConfig + + lc.Control = getControlFunc(ctx, sockopt, dl.controllers) + + return lc.ListenPacket(ctx, addr.Network(), addr.String()) +} + +// RegisterListenerController adds a controller to the effective system listener. +// The controller can be used to operate on file descriptors before they are put into use. +// +// xray:api:beta +func RegisterListenerController(controller func(network, address string, fd uintptr) error) error { + if controller == nil { + return newError("nil listener controller") + } + + effectiveListener.controllers = append(effectiveListener.controllers, controller) + return nil +} diff --git a/transport/internet/system_listener_test.go b/transport/internet/system_listener_test.go new file mode 100644 index 00000000..cdb90c18 --- /dev/null +++ b/transport/internet/system_listener_test.go @@ -0,0 +1,29 @@ +package internet_test + +import ( + "context" + "net" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func TestRegisterListenerController(t *testing.T) { + var gotFd uintptr + + common.Must(internet.RegisterListenerController(func(network string, addr string, fd uintptr) error { + gotFd = fd + return nil + })) + + conn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: net.IPv4zero, + }, nil) + common.Must(err) + common.Must(conn.Close()) + + if gotFd == 0 { + t.Error("expected none-zero fd, but actually 0") + } +} diff --git a/transport/internet/tcp/config.go b/transport/internet/tcp/config.go new file mode 100644 index 00000000..7b42b682 --- /dev/null +++ b/transport/internet/tcp/config.go @@ -0,0 +1,16 @@ +// +build !confonly + +package tcp + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const protocolName = "tcp" + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/tcp/config.pb.go b/transport/internet/tcp/config.pb.go new file mode 100644 index 00000000..28d715a3 --- /dev/null +++ b/transport/internet/tcp/config.pb.go @@ -0,0 +1,176 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/tcp/config.proto + +package tcp + +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 + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + HeaderSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=header_settings,json=headerSettings,proto3" json:"header_settings,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,3,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_tcp_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_transport_internet_tcp_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_transport_internet_tcp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetHeaderSettings() *serial.TypedMessage { + if x != nil { + return x.HeaderSettings + } + return nil +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +var File_transport_internet_tcp_config_proto protoreflect.FileDescriptor + +var file_transport_internet_tcp_config_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x63, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1b, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, + 0x63, 0x70, 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, 0x8d, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x49, 0x0a, 0x0f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 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, 0x0e, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4a, + 0x04, 0x08, 0x01, 0x10, 0x02, 0x42, 0x76, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x63, 0x70, 0x50, 0x01, 0x5a, 0x33, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x63, 0x70, 0xaa, + 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x63, 0x70, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_tcp_config_proto_rawDescOnce sync.Once + file_transport_internet_tcp_config_proto_rawDescData = file_transport_internet_tcp_config_proto_rawDesc +) + +func file_transport_internet_tcp_config_proto_rawDescGZIP() []byte { + file_transport_internet_tcp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tcp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_tcp_config_proto_rawDescData) + }) + return file_transport_internet_tcp_config_proto_rawDescData +} + +var file_transport_internet_tcp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_tcp_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.tcp.Config + (*serial.TypedMessage)(nil), // 1: xray.common.serial.TypedMessage +} +var file_transport_internet_tcp_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.tcp.Config.header_settings: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_transport_internet_tcp_config_proto_init() } +func file_transport_internet_tcp_config_proto_init() { + if File_transport_internet_tcp_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_tcp_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_transport_internet_tcp_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tcp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tcp_config_proto_depIdxs, + MessageInfos: file_transport_internet_tcp_config_proto_msgTypes, + }.Build() + File_transport_internet_tcp_config_proto = out.File + file_transport_internet_tcp_config_proto_rawDesc = nil + file_transport_internet_tcp_config_proto_goTypes = nil + file_transport_internet_tcp_config_proto_depIdxs = nil +} diff --git a/transport/internet/tcp/config.proto b/transport/internet/tcp/config.proto new file mode 100644 index 00000000..7ded6421 --- /dev/null +++ b/transport/internet/tcp/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.transport.internet.tcp; +option csharp_namespace = "Xray.Transport.Internet.Tcp"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/tcp"; +option java_package = "com.xray.transport.internet.tcp"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +message Config { + reserved 1; + xray.common.serial.TypedMessage header_settings = 2; + bool accept_proxy_protocol = 3; +} diff --git a/transport/internet/tcp/dialer.go b/transport/internet/tcp/dialer.go new file mode 100644 index 00000000..388aa90e --- /dev/null +++ b/transport/internet/tcp/dialer.go @@ -0,0 +1,56 @@ +// +build !confonly + +package tcp + +import ( + "context" + + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +// Dial dials a new TCP connection to the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + newError("dialing TCP to ", dest).WriteToLog(session.ExportIDToError(ctx)) + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + tlsConfig := config.GetTLSConfig(tls.WithDestination(dest)) + /* + if config.IsExperiment8357() { + conn = tls.UClient(conn, tlsConfig) + } else { + conn = tls.Client(conn, tlsConfig) + } + */ + conn = tls.Client(conn, tlsConfig) + } else if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + xtlsConfig := config.GetXTLSConfig(xtls.WithDestination(dest)) + conn = xtls.Client(conn, xtlsConfig) + } + + tcpSettings := streamSettings.ProtocolSettings.(*Config) + if tcpSettings.HeaderSettings != nil { + headerConfig, err := tcpSettings.HeaderSettings.GetInstance() + if err != nil { + return nil, newError("failed to get header settings").Base(err).AtError() + } + auth, err := internet.CreateConnectionAuthenticator(headerConfig) + if err != nil { + return nil, newError("failed to create header authenticator").Base(err).AtError() + } + conn = auth.Client(conn) + } + return internet.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/tcp/errors.generated.go b/transport/internet/tcp/errors.generated.go new file mode 100644 index 00000000..590fc4c7 --- /dev/null +++ b/transport/internet/tcp/errors.generated.go @@ -0,0 +1,9 @@ +package tcp + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tcp/hub.go b/transport/internet/tcp/hub.go new file mode 100644 index 00000000..f367b628 --- /dev/null +++ b/transport/internet/tcp/hub.go @@ -0,0 +1,143 @@ +// +build !confonly + +package tcp + +import ( + "context" + gotls "crypto/tls" + "strings" + "time" + + goxtls "github.com/xtls/go" + + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +// Listener is an internet.Listener that listens for TCP connections. +type Listener struct { + listener net.Listener + tlsConfig *gotls.Config + xtlsConfig *goxtls.Config + authConfig internet.ConnectionAuthenticator + config *Config + addConn internet.ConnHandler + locker *internet.FileLocker // for unix domain socket +} + +// ListenTCP creates a new Listener based on configurations. +func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + l := &Listener{ + addConn: handler, + } + tcpSettings := streamSettings.ProtocolSettings.(*Config) + l.config = tcpSettings + if l.config != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol + } + var listener net.Listener + var err error + if port == net.Port(0) { //unix + listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, newError("failed to listen Unix Doman Socket on ", address).Base(err) + } + newError("listening Unix Domain Socket on ", address).WriteToLog(session.ExportIDToError(ctx)) + locker := ctx.Value(address.Domain()) + if locker != nil { + l.locker = locker.(*internet.FileLocker) + } + } else { + listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, newError("failed to listen TCP on ", address, ":", port).Base(err) + } + newError("listening TCP on ", address, ":", port).WriteToLog(session.ExportIDToError(ctx)) + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + + l.listener = listener + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + l.tlsConfig = config.GetTLSConfig(tls.WithNextProto("h2")) + } + if config := xtls.ConfigFromStreamSettings(streamSettings); config != nil { + l.xtlsConfig = config.GetXTLSConfig(xtls.WithNextProto("h2")) + } + + if tcpSettings.HeaderSettings != nil { + headerConfig, err := tcpSettings.HeaderSettings.GetInstance() + if err != nil { + return nil, newError("invalid header settings").Base(err).AtError() + } + auth, err := internet.CreateConnectionAuthenticator(headerConfig) + if err != nil { + return nil, newError("invalid header settings.").Base(err).AtError() + } + l.authConfig = auth + } + + go l.keepAccepting() + return l, nil +} + +func (v *Listener) keepAccepting() { + for { + conn, err := v.listener.Accept() + if err != nil { + errStr := err.Error() + if strings.Contains(errStr, "closed") { + break + } + newError("failed to accepted raw connections").Base(err).AtWarning().WriteToLog() + if strings.Contains(errStr, "too many") { + time.Sleep(time.Millisecond * 500) + } + continue + } + + if v.tlsConfig != nil { + conn = tls.Server(conn, v.tlsConfig) + } else if v.xtlsConfig != nil { + conn = xtls.Server(conn, v.xtlsConfig) + } + if v.authConfig != nil { + conn = v.authConfig.Server(conn) + } + + v.addConn(internet.Connection(conn)) + } +} + +// Addr implements internet.Listener.Addr. +func (v *Listener) Addr() net.Addr { + return v.listener.Addr() +} + +// Close implements internet.Listener.Close. +func (v *Listener) Close() error { + if v.locker != nil { + v.locker.Release() + } + return v.listener.Close() +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenTCP)) +} diff --git a/transport/internet/tcp/sockopt_freebsd.go b/transport/internet/tcp/sockopt_freebsd.go new file mode 100644 index 00000000..70bb3549 --- /dev/null +++ b/transport/internet/tcp/sockopt_freebsd.go @@ -0,0 +1,24 @@ +// +build freebsd +// +build !confonly + +package tcp + +import ( + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// GetOriginalDestination from tcp conn +func GetOriginalDestination(conn internet.Connection) (net.Destination, error) { + la := conn.LocalAddr() + ra := conn.RemoteAddr() + ip, port, err := internet.OriginalDst(la, ra) + if err != nil { + return net.Destination{}, newError("failed to get destination").Base(err) + } + dest := net.TCPDestination(net.IPAddress(ip), net.Port(port)) + if !dest.IsValid() { + return net.Destination{}, newError("failed to parse destination.") + } + return dest, nil +} diff --git a/transport/internet/tcp/sockopt_linux.go b/transport/internet/tcp/sockopt_linux.go new file mode 100644 index 00000000..d53c5352 --- /dev/null +++ b/transport/internet/tcp/sockopt_linux.go @@ -0,0 +1,42 @@ +// +build linux +// +build !confonly + +package tcp + +import ( + "syscall" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const SO_ORIGINAL_DST = 80 + +func GetOriginalDestination(conn internet.Connection) (net.Destination, error) { + sysrawconn, f := conn.(syscall.Conn) + if !f { + return net.Destination{}, newError("unable to get syscall.Conn") + } + rawConn, err := sysrawconn.SyscallConn() + if err != nil { + return net.Destination{}, newError("failed to get sys fd").Base(err) + } + var dest net.Destination + err = rawConn.Control(func(fd uintptr) { + addr, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST) + if err != nil { + newError("failed to call getsockopt").Base(err).WriteToLog() + return + } + ip := net.IPAddress(addr.Multiaddr[4:8]) + port := uint16(addr.Multiaddr[2])<<8 + uint16(addr.Multiaddr[3]) + dest = net.TCPDestination(ip, net.Port(port)) + }) + if err != nil { + return net.Destination{}, newError("failed to control connection").Base(err) + } + if !dest.IsValid() { + return net.Destination{}, newError("failed to call getsockopt") + } + return dest, nil +} diff --git a/transport/internet/tcp/sockopt_linux_test.go b/transport/internet/tcp/sockopt_linux_test.go new file mode 100644 index 00000000..0b56f27c --- /dev/null +++ b/transport/internet/tcp/sockopt_linux_test.go @@ -0,0 +1,32 @@ +// +build linux + +package tcp_test + +import ( + "context" + "strings" + "testing" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/testing/servers/tcp" + "github.com/xtls/xray-core/v1/transport/internet" + . "github.com/xtls/xray-core/v1/transport/internet/tcp" +) + +func TestGetOriginalDestination(t *testing.T) { + tcpServer := tcp.Server{} + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + config, err := internet.ToMemoryStreamConfig(nil) + common.Must(err) + conn, err := Dial(context.Background(), dest, config) + common.Must(err) + defer conn.Close() + + originalDest, err := GetOriginalDestination(conn) + if !(dest == originalDest || strings.Contains(err.Error(), "failed to call getsockopt")) { + t.Error("unexpected state") + } +} diff --git a/transport/internet/tcp/sockopt_other.go b/transport/internet/tcp/sockopt_other.go new file mode 100644 index 00000000..2c2075be --- /dev/null +++ b/transport/internet/tcp/sockopt_other.go @@ -0,0 +1,13 @@ +// +build !linux,!freebsd +// +build !confonly + +package tcp + +import ( + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func GetOriginalDestination(conn internet.Connection) (net.Destination, error) { + return net.Destination{}, nil +} diff --git a/transport/internet/tcp/tcp.go b/transport/internet/tcp/tcp.go new file mode 100644 index 00000000..d528c367 --- /dev/null +++ b/transport/internet/tcp/tcp.go @@ -0,0 +1,3 @@ +package tcp + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/tcp_hub.go b/transport/internet/tcp_hub.go new file mode 100644 index 00000000..5cece5c0 --- /dev/null +++ b/transport/internet/tcp_hub.go @@ -0,0 +1,92 @@ +package internet + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/net" +) + +var ( + transportListenerCache = make(map[string]ListenFunc) +) + +func RegisterTransportListener(protocol string, listener ListenFunc) error { + if _, found := transportListenerCache[protocol]; found { + return newError(protocol, " listener already registered.").AtError() + } + transportListenerCache[protocol] = listener + return nil +} + +type ConnHandler func(Connection) + +type ListenFunc func(ctx context.Context, address net.Address, port net.Port, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) + +type Listener interface { + Close() error + Addr() net.Addr +} + +// ListenUnix is the UDS version of ListenTCP +func ListenUnix(ctx context.Context, address net.Address, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) { + if settings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, newError("failed to create default unix stream settings").Base(err) + } + settings = s + } + + protocol := settings.ProtocolName + listenFunc := transportListenerCache[protocol] + if listenFunc == nil { + return nil, newError(protocol, " unix istener not registered.").AtError() + } + listener, err := listenFunc(ctx, address, net.Port(0), settings, handler) + if err != nil { + return nil, newError("failed to listen on unix address: ", address).Base(err) + } + return listener, nil +} +func ListenTCP(ctx context.Context, address net.Address, port net.Port, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) { + if settings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, newError("failed to create default stream settings").Base(err) + } + settings = s + } + + if address.Family().IsDomain() && address.Domain() == "localhost" { + address = net.LocalHostIP + } + + if address.Family().IsDomain() { + return nil, newError("domain address is not allowed for listening: ", address.Domain()) + } + + protocol := settings.ProtocolName + listenFunc := transportListenerCache[protocol] + if listenFunc == nil { + return nil, newError(protocol, " listener not registered.").AtError() + } + listener, err := listenFunc(ctx, address, port, settings, handler) + if err != nil { + return nil, newError("failed to listen on address: ", address, ":", port).Base(err) + } + return listener, nil +} + +// ListenSystem listens on a local address for incoming TCP connections. +// +// xray:api:beta +func ListenSystem(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.Listener, error) { + return effectiveListener.Listen(ctx, addr, sockopt) +} + +// ListenSystemPacket listens on a local address for incoming UDP connections. +// +// xray:api:beta +func ListenSystemPacket(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.PacketConn, error) { + return effectiveListener.ListenPacket(ctx, addr, sockopt) +} diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go new file mode 100644 index 00000000..adca4a2b --- /dev/null +++ b/transport/internet/tls/config.go @@ -0,0 +1,251 @@ +// +build !confonly + +package tls + +import ( + "crypto/tls" + "crypto/x509" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/transport/internet" +) + +var ( + globalSessionCache = tls.NewLRUClientSessionCache(128) +) + +const exp8357 = "experiment:8357" + +// ParseCertificate converts a cert.Certificate to Certificate. +func ParseCertificate(c *cert.Certificate) *Certificate { + if c != nil { + certPEM, keyPEM := c.ToPEM() + return &Certificate{ + Certificate: certPEM, + Key: keyPEM, + } + } + return nil +} + +func (c *Config) loadSelfCertPool() (*x509.CertPool, error) { + root := x509.NewCertPool() + for _, cert := range c.Certificate { + if !root.AppendCertsFromPEM(cert.Certificate) { + return nil, newError("failed to append cert").AtWarning() + } + } + return root, nil +} + +// BuildCertificates builds a list of TLS certificates from proto definition. +func (c *Config) BuildCertificates() []tls.Certificate { + certs := make([]tls.Certificate, 0, len(c.Certificate)) + for _, entry := range c.Certificate { + if entry.Usage != Certificate_ENCIPHERMENT { + continue + } + keyPair, err := tls.X509KeyPair(entry.Certificate, entry.Key) + if err != nil { + newError("ignoring invalid X509 key pair").Base(err).AtWarning().WriteToLog() + continue + } + certs = append(certs, keyPair) + } + return certs +} + +func isCertificateExpired(c *tls.Certificate) bool { + if c.Leaf == nil && len(c.Certificate) > 0 { + if pc, err := x509.ParseCertificate(c.Certificate[0]); err == nil { + c.Leaf = pc + } + } + + // If leaf is not there, the certificate is probably not used yet. We trust user to provide a valid certificate. + return c.Leaf != nil && c.Leaf.NotAfter.Before(time.Now().Add(-time.Minute)) +} + +func issueCertificate(rawCA *Certificate, domain string) (*tls.Certificate, error) { + parent, err := cert.ParseCertificate(rawCA.Certificate, rawCA.Key) + if err != nil { + return nil, newError("failed to parse raw certificate").Base(err) + } + newCert, err := cert.Generate(parent, cert.CommonName(domain), cert.DNSNames(domain)) + if err != nil { + return nil, newError("failed to generate new certificate for ", domain).Base(err) + } + newCertPEM, newKeyPEM := newCert.ToPEM() + cert, err := tls.X509KeyPair(newCertPEM, newKeyPEM) + return &cert, err +} + +func (c *Config) getCustomCA() []*Certificate { + certs := make([]*Certificate, 0, len(c.Certificate)) + for _, certificate := range c.Certificate { + if certificate.Usage == Certificate_AUTHORITY_ISSUE { + certs = append(certs, certificate) + } + } + return certs +} + +func getGetCertificateFunc(c *tls.Config, ca []*Certificate) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + var access sync.RWMutex + + return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + domain := hello.ServerName + certExpired := false + + access.RLock() + certificate, found := c.NameToCertificate[domain] + access.RUnlock() + + if found { + if !isCertificateExpired(certificate) { + return certificate, nil + } + certExpired = true + } + + if certExpired { + newCerts := make([]tls.Certificate, 0, len(c.Certificates)) + + access.Lock() + for _, certificate := range c.Certificates { + if !isCertificateExpired(&certificate) { + newCerts = append(newCerts, certificate) + } + } + + c.Certificates = newCerts + access.Unlock() + } + + var issuedCertificate *tls.Certificate + + // Create a new certificate from existing CA if possible + for _, rawCert := range ca { + if rawCert.Usage == Certificate_AUTHORITY_ISSUE { + newCert, err := issueCertificate(rawCert, domain) + if err != nil { + newError("failed to issue new certificate for ", domain).Base(err).WriteToLog() + continue + } + + access.Lock() + c.Certificates = append(c.Certificates, *newCert) + issuedCertificate = &c.Certificates[len(c.Certificates)-1] + access.Unlock() + break + } + } + + if issuedCertificate == nil { + return nil, newError("failed to create a new certificate for ", domain) + } + + access.Lock() + c.BuildNameToCertificate() + access.Unlock() + + return issuedCertificate, nil + } +} + +func (c *Config) IsExperiment8357() bool { + return strings.HasPrefix(c.ServerName, exp8357) +} + +func (c *Config) parseServerName() string { + if c.IsExperiment8357() { + return c.ServerName[len(exp8357):] + } + + return c.ServerName +} + +// GetTLSConfig converts this Config into tls.Config. +func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { + root, err := c.getCertPool() + if err != nil { + newError("failed to load system root certificate").AtError().Base(err).WriteToLog() + } + + if c == nil { + return &tls.Config{ + ClientSessionCache: globalSessionCache, + RootCAs: root, + InsecureSkipVerify: false, + NextProtos: nil, + SessionTicketsDisabled: false, + } + } + + config := &tls.Config{ + ClientSessionCache: globalSessionCache, + RootCAs: root, + InsecureSkipVerify: c.AllowInsecure, + NextProtos: c.NextProtocol, + SessionTicketsDisabled: c.DisableSessionResumption, + } + + for _, opt := range opts { + opt(config) + } + + config.Certificates = c.BuildCertificates() + config.BuildNameToCertificate() + + caCerts := c.getCustomCA() + if len(caCerts) > 0 { + config.GetCertificate = getGetCertificateFunc(config, caCerts) + } + + if sn := c.parseServerName(); len(sn) > 0 { + config.ServerName = sn + } + + if len(config.NextProtos) == 0 { + config.NextProtos = []string{"h2", "http/1.1"} + } + + return config +} + +// Option for building TLS config. +type Option func(*tls.Config) + +// WithDestination sets the server name in TLS config. +func WithDestination(dest net.Destination) Option { + return func(config *tls.Config) { + if dest.Address.Family().IsDomain() && config.ServerName == "" { + config.ServerName = dest.Address.Domain() + } + } +} + +// WithNextProto sets the ALPN values in TLS config. +func WithNextProto(protocol ...string) Option { + return func(config *tls.Config) { + if len(config.NextProtos) == 0 { + config.NextProtos = protocol + } + } +} + +// ConfigFromStreamSettings fetches Config from stream settings. Nil if not found. +func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config { + if settings == nil { + return nil + } + config, ok := settings.SecuritySettings.(*Config) + if !ok { + return nil + } + return config +} diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go new file mode 100644 index 00000000..38092740 --- /dev/null +++ b/transport/internet/tls/config.pb.go @@ -0,0 +1,377 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/tls/config.proto + +package tls + +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 Certificate_Usage int32 + +const ( + Certificate_ENCIPHERMENT Certificate_Usage = 0 + Certificate_AUTHORITY_VERIFY Certificate_Usage = 1 + Certificate_AUTHORITY_ISSUE Certificate_Usage = 2 +) + +// Enum value maps for Certificate_Usage. +var ( + Certificate_Usage_name = map[int32]string{ + 0: "ENCIPHERMENT", + 1: "AUTHORITY_VERIFY", + 2: "AUTHORITY_ISSUE", + } + Certificate_Usage_value = map[string]int32{ + "ENCIPHERMENT": 0, + "AUTHORITY_VERIFY": 1, + "AUTHORITY_ISSUE": 2, + } +) + +func (x Certificate_Usage) Enum() *Certificate_Usage { + p := new(Certificate_Usage) + *p = x + return p +} + +func (x Certificate_Usage) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Certificate_Usage) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_tls_config_proto_enumTypes[0].Descriptor() +} + +func (Certificate_Usage) Type() protoreflect.EnumType { + return &file_transport_internet_tls_config_proto_enumTypes[0] +} + +func (x Certificate_Usage) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Certificate_Usage.Descriptor instead. +func (Certificate_Usage) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Certificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // TLS certificate in x509 format. + Certificate []byte `protobuf:"bytes,1,opt,name=Certificate,proto3" json:"Certificate,omitempty"` + // TLS key in x509 format. + Key []byte `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"` + Usage Certificate_Usage `protobuf:"varint,3,opt,name=usage,proto3,enum=xray.transport.internet.tls.Certificate_Usage" json:"usage,omitempty"` +} + +func (x *Certificate) Reset() { + *x = Certificate{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_tls_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Certificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Certificate) ProtoMessage() {} + +func (x *Certificate) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tls_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 Certificate.ProtoReflect.Descriptor instead. +func (*Certificate) Descriptor() ([]byte, []int) { + return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Certificate) GetCertificate() []byte { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Certificate) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *Certificate) GetUsage() Certificate_Usage { + if x != nil { + return x.Usage + } + return Certificate_ENCIPHERMENT +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Whether or not to allow self-signed certificates. + AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` + // Whether or not to allow insecure cipher suites. + AllowInsecureCiphers bool `protobuf:"varint,5,opt,name=allow_insecure_ciphers,json=allowInsecureCiphers,proto3" json:"allow_insecure_ciphers,omitempty"` + // List of certificates to be served on server. + Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` + // Override server name. + ServerName string `protobuf:"bytes,3,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` + // Lists of string as ALPN values. + NextProtocol []string `protobuf:"bytes,4,rep,name=next_protocol,json=nextProtocol,proto3" json:"next_protocol,omitempty"` + // Whether or not to disable session (ticket) resumption. + DisableSessionResumption bool `protobuf:"varint,6,opt,name=disable_session_resumption,json=disableSessionResumption,proto3" json:"disable_session_resumption,omitempty"` + // If true, root certificates on the system will not be loaded for + // verification. + DisableSystemRoot bool `protobuf:"varint,7,opt,name=disable_system_root,json=disableSystemRoot,proto3" json:"disable_system_root,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_tls_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_transport_internet_tls_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_transport_internet_tls_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetAllowInsecure() bool { + if x != nil { + return x.AllowInsecure + } + return false +} + +func (x *Config) GetAllowInsecureCiphers() bool { + if x != nil { + return x.AllowInsecureCiphers + } + return false +} + +func (x *Config) GetCertificate() []*Certificate { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Config) GetServerName() string { + if x != nil { + return x.ServerName + } + return "" +} + +func (x *Config) GetNextProtocol() []string { + if x != nil { + return x.NextProtocol + } + return nil +} + +func (x *Config) GetDisableSessionResumption() bool { + if x != nil { + return x.DisableSessionResumption + } + return false +} + +func (x *Config) GetDisableSystemRoot() bool { + if x != nil { + return x.DisableSystemRoot + } + return false +} + +var File_transport_internet_tls_config_proto protoreflect.FileDescriptor + +var file_transport_internet_tls_config_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1b, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, + 0x6c, 0x73, 0x22, 0xcd, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x74, 0x6c, 0x73, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x22, 0x44, 0x0a, 0x05, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, + 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, + 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, + 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, + 0x10, 0x02, 0x22, 0xe5, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, + 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, + 0x63, 0x75, 0x72, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, + 0x75, 0x72, 0x65, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x2e, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3c, 0x0a, 0x1a, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x72, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x18, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x6f, 0x6f, + 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x6f, 0x6f, 0x74, 0x42, 0x76, 0x0a, 0x1f, 0x63, 0x6f, + 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, + 0x33, 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, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, + 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_tls_config_proto_rawDescOnce sync.Once + file_transport_internet_tls_config_proto_rawDescData = file_transport_internet_tls_config_proto_rawDesc +) + +func file_transport_internet_tls_config_proto_rawDescGZIP() []byte { + file_transport_internet_tls_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tls_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_tls_config_proto_rawDescData) + }) + return file_transport_internet_tls_config_proto_rawDescData +} + +var file_transport_internet_tls_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_transport_internet_tls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_tls_config_proto_goTypes = []interface{}{ + (Certificate_Usage)(0), // 0: xray.transport.internet.tls.Certificate.Usage + (*Certificate)(nil), // 1: xray.transport.internet.tls.Certificate + (*Config)(nil), // 2: xray.transport.internet.tls.Config +} +var file_transport_internet_tls_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.tls.Certificate.usage:type_name -> xray.transport.internet.tls.Certificate.Usage + 1, // 1: xray.transport.internet.tls.Config.certificate:type_name -> xray.transport.internet.tls.Certificate + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_transport_internet_tls_config_proto_init() } +func file_transport_internet_tls_config_proto_init() { + if File_transport_internet_tls_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_tls_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Certificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_tls_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_transport_internet_tls_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tls_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tls_config_proto_depIdxs, + EnumInfos: file_transport_internet_tls_config_proto_enumTypes, + MessageInfos: file_transport_internet_tls_config_proto_msgTypes, + }.Build() + File_transport_internet_tls_config_proto = out.File + file_transport_internet_tls_config_proto_rawDesc = nil + file_transport_internet_tls_config_proto_goTypes = nil + file_transport_internet_tls_config_proto_depIdxs = nil +} diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto new file mode 100644 index 00000000..e03c0d65 --- /dev/null +++ b/transport/internet/tls/config.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package xray.transport.internet.tls; +option csharp_namespace = "Xray.Transport.Internet.Tls"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/tls"; +option java_package = "com.xray.transport.internet.tls"; +option java_multiple_files = true; + +message Certificate { + // TLS certificate in x509 format. + bytes Certificate = 1; + + // TLS key in x509 format. + bytes Key = 2; + + enum Usage { + ENCIPHERMENT = 0; + AUTHORITY_VERIFY = 1; + AUTHORITY_ISSUE = 2; + } + + Usage usage = 3; +} + +message Config { + // Whether or not to allow self-signed certificates. + bool allow_insecure = 1; + + // Whether or not to allow insecure cipher suites. + bool allow_insecure_ciphers = 5; + + // List of certificates to be served on server. + repeated Certificate certificate = 2; + + // Override server name. + string server_name = 3; + + // Lists of string as ALPN values. + repeated string next_protocol = 4; + + // Whether or not to disable session (ticket) resumption. + bool disable_session_resumption = 6; + + // If true, root certificates on the system will not be loaded for + // verification. + bool disable_system_root = 7; +} diff --git a/transport/internet/tls/config_other.go b/transport/internet/tls/config_other.go new file mode 100644 index 00000000..abc32ed4 --- /dev/null +++ b/transport/internet/tls/config_other.go @@ -0,0 +1,53 @@ +// +build !windows +// +build !confonly + +package tls + +import ( + "crypto/x509" + "sync" +) + +type rootCertsCache struct { + sync.Mutex + pool *x509.CertPool +} + +func (c *rootCertsCache) load() (*x509.CertPool, error) { + c.Lock() + defer c.Unlock() + + if c.pool != nil { + return c.pool, nil + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + c.pool = pool + return pool, nil +} + +var rootCerts rootCertsCache + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + if len(c.Certificate) == 0 { + return rootCerts.load() + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, newError("system root").AtWarning().Base(err) + } + for _, cert := range c.Certificate { + if !pool.AppendCertsFromPEM(cert.Certificate) { + return nil, newError("append cert to root").AtWarning().Base(err) + } + } + return pool, err +} diff --git a/transport/internet/tls/config_test.go b/transport/internet/tls/config_test.go new file mode 100644 index 00000000..41a5c7ab --- /dev/null +++ b/transport/internet/tls/config_test.go @@ -0,0 +1,99 @@ +package tls_test + +import ( + gotls "crypto/tls" + "crypto/x509" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + . "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +func TestCertificateIssuing(t *testing.T) { + certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + tlsConfig := c.GetTLSConfig() + xrayCert, err := tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestExpiredCertificate(t *testing.T) { + caCert := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + expiredCert := cert.MustGenerate(caCert, cert.NotAfter(time.Now().Add(time.Minute*-2)), cert.CommonName("www.example.com"), cert.DNSNames("www.example.com")) + + certificate := ParseCertificate(caCert) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + certificate2 := ParseCertificate(expiredCert) + + c := &Config{ + Certificate: []*Certificate{ + certificate, + certificate2, + }, + } + + tlsConfig := c.GetTLSConfig() + xrayCert, err := tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestInsecureCertificates(t *testing.T) { + c := &Config{ + AllowInsecureCiphers: true, + } + + tlsConfig := c.GetTLSConfig() + if len(tlsConfig.CipherSuites) > 0 { + t.Fatal("Unexpected tls cipher suites list: ", tlsConfig.CipherSuites) + } +} + +func BenchmarkCertificateIssuing(b *testing.B) { + certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + tlsConfig := c.GetTLSConfig() + lenCerts := len(tlsConfig.Certificates) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + delete(tlsConfig.NameToCertificate, "www.example.com") + tlsConfig.Certificates = tlsConfig.Certificates[:lenCerts] + } +} diff --git a/transport/internet/tls/config_windows.go b/transport/internet/tls/config_windows.go new file mode 100644 index 00000000..f02f6d26 --- /dev/null +++ b/transport/internet/tls/config_windows.go @@ -0,0 +1,14 @@ +// +build windows +// +build !confonly + +package tls + +import "crypto/x509" + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + return nil, nil +} diff --git a/transport/internet/tls/errors.generated.go b/transport/internet/tls/errors.generated.go new file mode 100644 index 00000000..8890e007 --- /dev/null +++ b/transport/internet/tls/errors.generated.go @@ -0,0 +1,9 @@ +package tls + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go new file mode 100644 index 00000000..f7889be6 --- /dev/null +++ b/transport/internet/tls/tls.go @@ -0,0 +1,67 @@ +// +build !confonly + +package tls + +import ( + "crypto/tls" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +var ( + _ buf.Writer = (*Conn)(nil) +) + +type Conn struct { + *tls.Conn +} + +func (c *Conn) WriteMultiBuffer(mb buf.MultiBuffer) error { + mb = buf.Compact(mb) + mb, err := buf.WriteMultiBuffer(c, mb) + buf.ReleaseMulti(mb) + return err +} + +func (c *Conn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +// Client initiates a TLS client handshake on the given connection. +func Client(c net.Conn, config *tls.Config) net.Conn { + tlsConn := tls.Client(c, config) + return &Conn{Conn: tlsConn} +} + +/* +func copyConfig(c *tls.Config) *utls.Config { + return &utls.Config{ + NextProtos: c.NextProtos, + ServerName: c.ServerName, + InsecureSkipVerify: c.InsecureSkipVerify, + MinVersion: utls.VersionTLS12, + MaxVersion: utls.VersionTLS12, + } +} + +func UClient(c net.Conn, config *tls.Config) net.Conn { + uConfig := copyConfig(config) + return utls.Client(c, uConfig) +} +*/ + +// Server initiates a TLS server handshake on the given connection. +func Server(c net.Conn, config *tls.Config) net.Conn { + tlsConn := tls.Server(c, config) + return &Conn{Conn: tlsConn} +} diff --git a/transport/internet/udp/config.go b/transport/internet/udp/config.go new file mode 100644 index 00000000..15b45ce9 --- /dev/null +++ b/transport/internet/udp/config.go @@ -0,0 +1,12 @@ +package udp + +import ( + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/udp/config.pb.go b/transport/internet/udp/config.pb.go new file mode 100644 index 00000000..d5a7796a --- /dev/null +++ b/transport/internet/udp/config.pb.go @@ -0,0 +1,145 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/udp/config.proto + +package udp + +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_transport_internet_udp_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_transport_internet_udp_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_transport_internet_udp_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_udp_config_proto protoreflect.FileDescriptor + +var file_transport_internet_udp_config_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x75, 0x64, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1b, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x75, + 0x64, 0x70, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x76, 0x0a, 0x1f, + 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x75, 0x64, 0x70, 0x50, + 0x01, 0x5a, 0x33, 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, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x2f, 0x75, 0x64, 0x70, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x2e, 0x55, 0x64, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_udp_config_proto_rawDescOnce sync.Once + file_transport_internet_udp_config_proto_rawDescData = file_transport_internet_udp_config_proto_rawDesc +) + +func file_transport_internet_udp_config_proto_rawDescGZIP() []byte { + file_transport_internet_udp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_udp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_udp_config_proto_rawDescData) + }) + return file_transport_internet_udp_config_proto_rawDescData +} + +var file_transport_internet_udp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_udp_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: xray.transport.internet.udp.Config +} +var file_transport_internet_udp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_transport_internet_udp_config_proto_init() } +func file_transport_internet_udp_config_proto_init() { + if File_transport_internet_udp_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_udp_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_transport_internet_udp_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_udp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_udp_config_proto_depIdxs, + MessageInfos: file_transport_internet_udp_config_proto_msgTypes, + }.Build() + File_transport_internet_udp_config_proto = out.File + file_transport_internet_udp_config_proto_rawDesc = nil + file_transport_internet_udp_config_proto_goTypes = nil + file_transport_internet_udp_config_proto_depIdxs = nil +} diff --git a/transport/internet/udp/config.proto b/transport/internet/udp/config.proto new file mode 100644 index 00000000..ffd00f7b --- /dev/null +++ b/transport/internet/udp/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.udp; +option csharp_namespace = "Xray.Transport.Internet.Udp"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/udp"; +option java_package = "com.xray.transport.internet.udp"; +option java_multiple_files = true; + +message Config {} diff --git a/transport/internet/udp/dialer.go b/transport/internet/udp/dialer.go new file mode 100644 index 00000000..653b4e96 --- /dev/null +++ b/transport/internet/udp/dialer.go @@ -0,0 +1,25 @@ +package udp + +import ( + "context" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, + func(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + var sockopt *internet.SocketConfig + if streamSettings != nil { + sockopt = streamSettings.SocketSettings + } + conn, err := internet.DialSystem(ctx, dest, sockopt) + if err != nil { + return nil, err + } + // TODO: handle dialer options + return internet.Connection(conn), nil + })) +} diff --git a/transport/internet/udp/dispatcher.go b/transport/internet/udp/dispatcher.go new file mode 100644 index 00000000..73989ba8 --- /dev/null +++ b/transport/internet/udp/dispatcher.go @@ -0,0 +1,198 @@ +package udp + +import ( + "context" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common/signal/done" + + "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/protocol/udp" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport" +) + +type ResponseCallback func(ctx context.Context, packet *udp.Packet) + +type connEntry struct { + link *transport.Link + timer signal.ActivityUpdater + cancel context.CancelFunc +} + +type Dispatcher struct { + sync.RWMutex + conns map[net.Destination]*connEntry + dispatcher routing.Dispatcher + callback ResponseCallback +} + +func NewDispatcher(dispatcher routing.Dispatcher, callback ResponseCallback) *Dispatcher { + return &Dispatcher{ + conns: make(map[net.Destination]*connEntry), + dispatcher: dispatcher, + callback: callback, + } +} + +func (v *Dispatcher) RemoveRay(dest net.Destination) { + v.Lock() + defer v.Unlock() + if conn, found := v.conns[dest]; found { + common.Close(conn.link.Reader) + common.Close(conn.link.Writer) + delete(v.conns, dest) + } +} + +func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) *connEntry { + v.Lock() + defer v.Unlock() + + if entry, found := v.conns[dest]; found { + return entry + } + + newError("establishing new connection for ", dest).WriteToLog() + + ctx, cancel := context.WithCancel(ctx) + removeRay := func() { + cancel() + v.RemoveRay(dest) + } + timer := signal.CancelAfterInactivity(ctx, removeRay, time.Second*4) + link, _ := v.dispatcher.Dispatch(ctx, dest) + entry := &connEntry{ + link: link, + timer: timer, + cancel: removeRay, + } + v.conns[dest] = entry + go handleInput(ctx, entry, dest, v.callback) + return entry +} + +func (v *Dispatcher) Dispatch(ctx context.Context, destination net.Destination, payload *buf.Buffer) { + // TODO: Add user to destString + newError("dispatch request to: ", destination).AtDebug().WriteToLog(session.ExportIDToError(ctx)) + + conn := v.getInboundRay(ctx, destination) + outputStream := conn.link.Writer + if outputStream != nil { + if err := outputStream.WriteMultiBuffer(buf.MultiBuffer{payload}); err != nil { + newError("failed to write first UDP payload").Base(err).WriteToLog(session.ExportIDToError(ctx)) + conn.cancel() + return + } + } +} + +func handleInput(ctx context.Context, conn *connEntry, dest net.Destination, callback ResponseCallback) { + defer conn.cancel() + + input := conn.link.Reader + timer := conn.timer + + for { + select { + case <-ctx.Done(): + return + default: + } + + mb, err := input.ReadMultiBuffer() + if err != nil { + newError("failed to handle UDP input").Base(err).WriteToLog(session.ExportIDToError(ctx)) + return + } + timer.Update() + for _, b := range mb { + callback(ctx, &udp.Packet{ + Payload: b, + Source: dest, + }) + } + } +} + +type dispatcherConn struct { + dispatcher *Dispatcher + cache chan *udp.Packet + done *done.Instance +} + +func DialDispatcher(ctx context.Context, dispatcher routing.Dispatcher) (net.PacketConn, error) { + c := &dispatcherConn{ + cache: make(chan *udp.Packet, 16), + done: done.New(), + } + + d := NewDispatcher(dispatcher, c.callback) + c.dispatcher = d + return c, nil +} + +func (c *dispatcherConn) callback(ctx context.Context, packet *udp.Packet) { + select { + case <-c.done.Wait(): + packet.Payload.Release() + return + case c.cache <- packet: + default: + packet.Payload.Release() + return + } +} + +func (c *dispatcherConn) ReadFrom(p []byte) (int, net.Addr, error) { + select { + case <-c.done.Wait(): + return 0, nil, io.EOF + case packet := <-c.cache: + n := copy(p, packet.Payload.Bytes()) + return n, &net.UDPAddr{ + IP: packet.Source.Address.IP(), + Port: int(packet.Source.Port), + }, nil + } +} + +func (c *dispatcherConn) WriteTo(p []byte, addr net.Addr) (int, error) { + buffer := buf.New() + raw := buffer.Extend(buf.Size) + n := copy(raw, p) + buffer.Resize(0, int32(n)) + + ctx := context.Background() + c.dispatcher.Dispatch(ctx, net.DestinationFromAddr(addr), buffer) + return n, nil +} + +func (c *dispatcherConn) Close() error { + return c.done.Close() +} + +func (c *dispatcherConn) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } +} + +func (c *dispatcherConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *dispatcherConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *dispatcherConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/transport/internet/udp/dispatcher_test.go b/transport/internet/udp/dispatcher_test.go new file mode 100644 index 00000000..ce4de0fc --- /dev/null +++ b/transport/internet/udp/dispatcher_test.go @@ -0,0 +1,86 @@ +package udp_test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "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/protocol/udp" + "github.com/xtls/xray-core/v1/features/routing" + "github.com/xtls/xray-core/v1/transport" + . "github.com/xtls/xray-core/v1/transport/internet/udp" + "github.com/xtls/xray-core/v1/transport/pipe" +) + +type TestDispatcher struct { + OnDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error) +} + +func (d *TestDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + return d.OnDispatch(ctx, dest) +} + +func (d *TestDispatcher) Start() error { + return nil +} + +func (d *TestDispatcher) Close() error { + return nil +} + +func (*TestDispatcher) Type() interface{} { + return routing.DispatcherType() +} + +func TestSameDestinationDispatching(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + uplinkReader, uplinkWriter := pipe.New(pipe.WithSizeLimit(1024)) + downlinkReader, downlinkWriter := pipe.New(pipe.WithSizeLimit(1024)) + + go func() { + for { + data, err := uplinkReader.ReadMultiBuffer() + if err != nil { + break + } + err = downlinkWriter.WriteMultiBuffer(data) + common.Must(err) + } + }() + + var count uint32 + td := &TestDispatcher{ + OnDispatch: func(ctx context.Context, dest net.Destination) (*transport.Link, error) { + atomic.AddUint32(&count, 1) + return &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, nil + }, + } + dest := net.UDPDestination(net.LocalHostIP, 53) + + b := buf.New() + b.WriteString("abcd") + + var msgCount uint32 + dispatcher := NewDispatcher(td, func(ctx context.Context, packet *udp.Packet) { + atomic.AddUint32(&msgCount, 1) + }) + + dispatcher.Dispatch(ctx, dest, b) + for i := 0; i < 5; i++ { + dispatcher.Dispatch(ctx, dest, b) + } + + time.Sleep(time.Second) + cancel() + + if count != 1 { + t.Error("count: ", count) + } + if v := atomic.LoadUint32(&msgCount); v != 6 { + t.Error("msgCount: ", v) + } +} diff --git a/transport/internet/udp/errors.generated.go b/transport/internet/udp/errors.generated.go new file mode 100644 index 00000000..3d6b44f2 --- /dev/null +++ b/transport/internet/udp/errors.generated.go @@ -0,0 +1,9 @@ +package udp + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/udp/hub.go b/transport/internet/udp/hub.go new file mode 100644 index 00000000..ab20e580 --- /dev/null +++ b/transport/internet/udp/hub.go @@ -0,0 +1,132 @@ +package udp + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol/udp" + "github.com/xtls/xray-core/v1/transport/internet" +) + +type HubOption func(h *Hub) + +func HubCapacity(capacity int) HubOption { + return func(h *Hub) { + h.capacity = capacity + } +} + +func HubReceiveOriginalDestination(r bool) HubOption { + return func(h *Hub) { + h.recvOrigDest = r + } +} + +type Hub struct { + conn *net.UDPConn + cache chan *udp.Packet + capacity int + recvOrigDest bool +} + +func ListenUDP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, options ...HubOption) (*Hub, error) { + hub := &Hub{ + capacity: 256, + recvOrigDest: false, + } + for _, opt := range options { + opt(hub) + } + + var sockopt *internet.SocketConfig + if streamSettings != nil { + sockopt = streamSettings.SocketSettings + } + if sockopt != nil && sockopt.ReceiveOriginalDestAddress { + hub.recvOrigDest = true + } + + udpConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{ + IP: address.IP(), + Port: int(port), + }, sockopt) + if err != nil { + return nil, err + } + newError("listening UDP on ", address, ":", port).WriteToLog() + hub.conn = udpConn.(*net.UDPConn) + hub.cache = make(chan *udp.Packet, hub.capacity) + + go hub.start() + return hub, nil +} + +// Close implements net.Listener. +func (h *Hub) Close() error { + h.conn.Close() + return nil +} + +func (h *Hub) WriteTo(payload []byte, dest net.Destination) (int, error) { + return h.conn.WriteToUDP(payload, &net.UDPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + }) +} + +func (h *Hub) start() { + c := h.cache + defer close(c) + + oobBytes := make([]byte, 256) + + for { + buffer := buf.New() + var noob int + var addr *net.UDPAddr + rawBytes := buffer.Extend(buf.Size) + + n, noob, _, addr, err := ReadUDPMsg(h.conn, rawBytes, oobBytes) + if err != nil { + newError("failed to read UDP msg").Base(err).WriteToLog() + buffer.Release() + break + } + buffer.Resize(0, int32(n)) + + if buffer.IsEmpty() { + buffer.Release() + continue + } + + payload := &udp.Packet{ + Payload: buffer, + Source: net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), + } + if h.recvOrigDest && noob > 0 { + payload.Target = RetrieveOriginalDest(oobBytes[:noob]) + if payload.Target.IsValid() { + newError("UDP original destination: ", payload.Target).AtDebug().WriteToLog() + } else { + newError("failed to read UDP original destination").WriteToLog() + } + } + + select { + case c <- payload: + default: + buffer.Release() + payload.Payload = nil + } + } +} + +// Addr implements net.Listener. +func (h *Hub) Addr() net.Addr { + return h.conn.LocalAddr() +} + +func (h *Hub) Receive() <-chan *udp.Packet { + return h.cache +} diff --git a/transport/internet/udp/hub_freebsd.go b/transport/internet/udp/hub_freebsd.go new file mode 100644 index 00000000..bc7649fd --- /dev/null +++ b/transport/internet/udp/hub_freebsd.go @@ -0,0 +1,37 @@ +// +build freebsd + +package udp + +import ( + "bytes" + "encoding/gob" + "io" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/transport/internet" +) + +// RetrieveOriginalDest from stored laddr, caddr +func RetrieveOriginalDest(oob []byte) net.Destination { + dec := gob.NewDecoder(bytes.NewBuffer(oob)) + var la, ra net.UDPAddr + dec.Decode(&la) + dec.Decode(&ra) + ip, port, err := internet.OriginalDst(&la, &ra) + if err != nil { + return net.Destination{} + } + return net.UDPDestination(net.IPAddress(ip), net.Port(port)) +} + +// ReadUDPMsg stores laddr, caddr for later use +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + nBytes, addr, err := conn.ReadFromUDP(payload) + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + enc.Encode(conn.LocalAddr().(*net.UDPAddr)) + enc.Encode(addr) + var reader io.Reader = &buf + noob, _ := reader.Read(oob) + return nBytes, noob, 0, addr, err +} diff --git a/transport/internet/udp/hub_linux.go b/transport/internet/udp/hub_linux.go new file mode 100644 index 00000000..e0cfda7b --- /dev/null +++ b/transport/internet/udp/hub_linux.go @@ -0,0 +1,33 @@ +// +build linux + +package udp + +import ( + "syscall" + + "github.com/xtls/xray-core/v1/common/net" + "golang.org/x/sys/unix" +) + +func RetrieveOriginalDest(oob []byte) net.Destination { + msgs, err := syscall.ParseSocketControlMessage(oob) + if err != nil { + return net.Destination{} + } + for _, msg := range msgs { + if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { + ip := net.IPAddress(msg.Data[4:8]) + port := net.PortFromBytes(msg.Data[2:4]) + return net.UDPDestination(ip, port) + } else if msg.Header.Level == syscall.SOL_IPV6 && msg.Header.Type == unix.IPV6_RECVORIGDSTADDR { + ip := net.IPAddress(msg.Data[8:24]) + port := net.PortFromBytes(msg.Data[2:4]) + return net.UDPDestination(ip, port) + } + } + return net.Destination{} +} + +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + return conn.ReadMsgUDP(payload, oob) +} diff --git a/transport/internet/udp/hub_other.go b/transport/internet/udp/hub_other.go new file mode 100644 index 00000000..10d16d54 --- /dev/null +++ b/transport/internet/udp/hub_other.go @@ -0,0 +1,16 @@ +// +build !linux,!freebsd + +package udp + +import ( + "github.com/xtls/xray-core/v1/common/net" +) + +func RetrieveOriginalDest(oob []byte) net.Destination { + return net.Destination{} +} + +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + nBytes, addr, err := conn.ReadFromUDP(payload) + return nBytes, 0, 0, addr, err +} diff --git a/transport/internet/udp/udp.go b/transport/internet/udp/udp.go new file mode 100644 index 00000000..525d6a85 --- /dev/null +++ b/transport/internet/udp/udp.go @@ -0,0 +1,5 @@ +package udp + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +const protocolName = "udp" diff --git a/transport/internet/websocket/config.go b/transport/internet/websocket/config.go new file mode 100644 index 00000000..2dbbf993 --- /dev/null +++ b/transport/internet/websocket/config.go @@ -0,0 +1,37 @@ +// +build !confonly + +package websocket + +import ( + "net/http" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/transport/internet" +) + +const protocolName = "websocket" + +func (c *Config) GetNormalizedPath() string { + path := c.Path + if path == "" { + return "/" + } + if path[0] != '/' { + return "/" + path + } + return path +} + +func (c *Config) GetRequestHeader() http.Header { + header := http.Header{} + for _, h := range c.Header { + header.Add(h.Key, h.Value) + } + return header +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/websocket/config.pb.go b/transport/internet/websocket/config.pb.go new file mode 100644 index 00000000..9009bdba --- /dev/null +++ b/transport/internet/websocket/config.pb.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/websocket/config.proto + +package websocket + +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 Header struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Header) Reset() { + *x = Header{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_websocket_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_websocket_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 Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_transport_internet_websocket_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Header) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Header) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // URL path to the WebSocket service. Empty value means root(/). + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Header []*Header `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,4,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_websocket_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_transport_internet_websocket_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_transport_internet_websocket_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Config) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +var File_transport_internet_websocket_config_proto protoreflect.FileDescriptor + +var file_transport_internet_websocket_config_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x22, 0x30, + 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 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, + 0x22, 0x99, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x41, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, + 0x6b, 0x65, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x70, 0x72, 0x6f, + 0x78, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x13, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x42, 0x88, 0x01, 0x0a, + 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x77, 0x65, 0x62, + 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x39, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, + 0x6b, 0x65, 0x74, 0xaa, 0x02, 0x21, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x57, 0x65, + 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_websocket_config_proto_rawDescOnce sync.Once + file_transport_internet_websocket_config_proto_rawDescData = file_transport_internet_websocket_config_proto_rawDesc +) + +func file_transport_internet_websocket_config_proto_rawDescGZIP() []byte { + file_transport_internet_websocket_config_proto_rawDescOnce.Do(func() { + file_transport_internet_websocket_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_websocket_config_proto_rawDescData) + }) + return file_transport_internet_websocket_config_proto_rawDescData +} + +var file_transport_internet_websocket_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_websocket_config_proto_goTypes = []interface{}{ + (*Header)(nil), // 0: xray.transport.internet.websocket.Header + (*Config)(nil), // 1: xray.transport.internet.websocket.Config +} +var file_transport_internet_websocket_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.websocket.Config.header:type_name -> xray.transport.internet.websocket.Header + 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_transport_internet_websocket_config_proto_init() } +func file_transport_internet_websocket_config_proto_init() { + if File_transport_internet_websocket_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_websocket_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Header); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_websocket_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_transport_internet_websocket_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_websocket_config_proto_goTypes, + DependencyIndexes: file_transport_internet_websocket_config_proto_depIdxs, + MessageInfos: file_transport_internet_websocket_config_proto_msgTypes, + }.Build() + File_transport_internet_websocket_config_proto = out.File + file_transport_internet_websocket_config_proto_rawDesc = nil + file_transport_internet_websocket_config_proto_goTypes = nil + file_transport_internet_websocket_config_proto_depIdxs = nil +} diff --git a/transport/internet/websocket/config.proto b/transport/internet/websocket/config.proto new file mode 100644 index 00000000..e8ef9d1f --- /dev/null +++ b/transport/internet/websocket/config.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package xray.transport.internet.websocket; +option csharp_namespace = "Xray.Transport.Internet.Websocket"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/websocket"; +option java_package = "com.xray.transport.internet.websocket"; +option java_multiple_files = true; + +message Header { + string key = 1; + string value = 2; +} + +message Config { + reserved 1; + + // URL path to the WebSocket service. Empty value means root(/). + string path = 2; + + repeated Header header = 3; + + bool accept_proxy_protocol = 4; +} diff --git a/transport/internet/websocket/connection.go b/transport/internet/websocket/connection.go new file mode 100644 index 00000000..1bae37ff --- /dev/null +++ b/transport/internet/websocket/connection.go @@ -0,0 +1,114 @@ +// +build !confonly + +package websocket + +import ( + "io" + "net" + "time" + + "github.com/gorilla/websocket" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/errors" + "github.com/xtls/xray-core/v1/common/serial" +) + +var ( + _ buf.Writer = (*connection)(nil) +) + +// connection is a wrapper for net.Conn over WebSocket connection. +type connection struct { + conn *websocket.Conn + reader io.Reader + remoteAddr net.Addr +} + +func newConnection(conn *websocket.Conn, remoteAddr net.Addr) *connection { + return &connection{ + conn: conn, + remoteAddr: remoteAddr, + } +} + +// Read implements net.Conn.Read() +func (c *connection) Read(b []byte) (int, error) { + for { + reader, err := c.getReader() + if err != nil { + return 0, err + } + + nBytes, err := reader.Read(b) + if errors.Cause(err) == io.EOF { + c.reader = nil + continue + } + return nBytes, err + } +} + +func (c *connection) getReader() (io.Reader, error) { + if c.reader != nil { + return c.reader, nil + } + + _, reader, err := c.conn.NextReader() + if err != nil { + return nil, err + } + c.reader = reader + return reader, nil +} + +// Write implements io.Writer. +func (c *connection) Write(b []byte) (int, error) { + if err := c.conn.WriteMessage(websocket.BinaryMessage, b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + mb = buf.Compact(mb) + mb, err := buf.WriteMultiBuffer(c, mb) + buf.ReleaseMulti(mb) + return err +} + +func (c *connection) Close() error { + var errors []interface{} + if err := c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil { + errors = append(errors, err) + } + if err := c.conn.Close(); err != nil { + errors = append(errors, err) + } + if len(errors) > 0 { + return newError("failed to close connection").Base(newError(serial.Concat(errors...))) + } + return nil +} + +func (c *connection) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *connection) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *connection) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +func (c *connection) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *connection) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/transport/internet/websocket/dialer.go b/transport/internet/websocket/dialer.go new file mode 100644 index 00000000..ab3afa9d --- /dev/null +++ b/transport/internet/websocket/dialer.go @@ -0,0 +1,67 @@ +// +build !confonly + +package websocket + +import ( + "context" + "time" + + "github.com/gorilla/websocket" + "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/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +// Dial dials a WebSocket connection to the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + newError("creating connection to ", dest).WriteToLog(session.ExportIDToError(ctx)) + + conn, err := dialWebsocket(ctx, dest, streamSettings) + if err != nil { + return nil, newError("failed to dial WebSocket").Base(err) + } + return internet.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} + +func dialWebsocket(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) { + wsSettings := streamSettings.ProtocolSettings.(*Config) + + dialer := &websocket.Dialer{ + NetDial: func(network, addr string) (net.Conn, error) { + return internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + }, + ReadBufferSize: 4 * 1024, + WriteBufferSize: 4 * 1024, + HandshakeTimeout: time.Second * 8, + } + + protocol := "ws" + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + protocol = "wss" + dialer.TLSClientConfig = config.GetTLSConfig(tls.WithDestination(dest), tls.WithNextProto("http/1.1")) + } + + host := dest.NetAddr() + if (protocol == "ws" && dest.Port == 80) || (protocol == "wss" && dest.Port == 443) { + host = dest.Address.String() + } + uri := protocol + "://" + host + wsSettings.GetNormalizedPath() + + conn, resp, err := dialer.Dial(uri, wsSettings.GetRequestHeader()) + if err != nil { + var reason string + if resp != nil { + reason = resp.Status + } + return nil, newError("failed to dial to (", uri, "): ", reason).Base(err) + } + + return newConnection(conn, conn.RemoteAddr()), nil +} diff --git a/transport/internet/websocket/errors.generated.go b/transport/internet/websocket/errors.generated.go new file mode 100644 index 00000000..3f628958 --- /dev/null +++ b/transport/internet/websocket/errors.generated.go @@ -0,0 +1,9 @@ +package websocket + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/websocket/hub.go b/transport/internet/websocket/hub.go new file mode 100644 index 00000000..6b8e4733 --- /dev/null +++ b/transport/internet/websocket/hub.go @@ -0,0 +1,151 @@ +// +build !confonly + +package websocket + +import ( + "context" + "crypto/tls" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + http_proto "github.com/xtls/xray-core/v1/common/protocol/http" + "github.com/xtls/xray-core/v1/common/session" + "github.com/xtls/xray-core/v1/transport/internet" + v2tls "github.com/xtls/xray-core/v1/transport/internet/tls" +) + +type requestHandler struct { + path string + ln *Listener +} + +var upgrader = &websocket.Upgrader{ + ReadBufferSize: 4 * 1024, + WriteBufferSize: 4 * 1024, + HandshakeTimeout: time.Second * 4, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != h.path { + writer.WriteHeader(http.StatusNotFound) + return + } + conn, err := upgrader.Upgrade(writer, request, nil) + if err != nil { + newError("failed to convert to WebSocket connection").Base(err).WriteToLog() + return + } + + forwardedAddrs := http_proto.ParseXForwardedFor(request.Header) + remoteAddr := conn.RemoteAddr() + if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddrs[0].IP(), + Port: int(0), + } + } + + h.ln.addConn(newConnection(conn, remoteAddr)) +} + +type Listener struct { + sync.Mutex + server http.Server + listener net.Listener + config *Config + addConn internet.ConnHandler + locker *internet.FileLocker // for unix domain socket +} + +func ListenWS(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + l := &Listener{ + addConn: addConn, + } + wsSettings := streamSettings.ProtocolSettings.(*Config) + l.config = wsSettings + if l.config != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol + } + var listener net.Listener + var err error + if port == net.Port(0) { //unix + listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, newError("failed to listen unix domain socket(for WS) on ", address).Base(err) + } + newError("listening unix domain socket(for WS) on ", address).WriteToLog(session.ExportIDToError(ctx)) + locker := ctx.Value(address.Domain()) + if locker != nil { + l.locker = locker.(*internet.FileLocker) + } + } else { //tcp + listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, newError("failed to listen TCP(for WS) on ", address, ":", port).Base(err) + } + newError("listening TCP(for WS) on ", address, ":", port).WriteToLog(session.ExportIDToError(ctx)) + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + + if config := v2tls.ConfigFromStreamSettings(streamSettings); config != nil { + if tlsConfig := config.GetTLSConfig(); tlsConfig != nil { + listener = tls.NewListener(listener, tlsConfig) + } + } + + l.listener = listener + + l.server = http.Server{ + Handler: &requestHandler{ + path: wsSettings.GetNormalizedPath(), + ln: l, + }, + ReadHeaderTimeout: time.Second * 4, + MaxHeaderBytes: 2048, + } + + go func() { + if err := l.server.Serve(l.listener); err != nil { + newError("failed to serve http for WebSocket").Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + }() + + return l, err +} + +// Addr implements net.Listener.Addr(). +func (ln *Listener) Addr() net.Addr { + return ln.listener.Addr() +} + +// Close implements net.Listener.Close(). +func (ln *Listener) Close() error { + if ln.locker != nil { + ln.locker.Release() + } + return ln.listener.Close() +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenWS)) +} diff --git a/transport/internet/websocket/ws.go b/transport/internet/websocket/ws.go new file mode 100644 index 00000000..c28df03a --- /dev/null +++ b/transport/internet/websocket/ws.go @@ -0,0 +1,7 @@ +/*Package websocket implements Websocket transport + +Websocket transport implements an HTTP(S) compliable, surveillance proof transport method with plausible deniability. +*/ +package websocket + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen diff --git a/transport/internet/websocket/ws_test.go b/transport/internet/websocket/ws_test.go new file mode 100644 index 00000000..c1b8e1cb --- /dev/null +++ b/transport/internet/websocket/ws_test.go @@ -0,0 +1,148 @@ +package websocket_test + +import ( + "context" + "runtime" + "testing" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/transport/internet" + "github.com/xtls/xray-core/v1/transport/internet/tls" + . "github.com/xtls/xray-core/v1/transport/internet/websocket" +) + +func Test_listenWSAndDial(t *testing.T) { + listen, err := ListenWS(context.Background(), net.LocalHostIP, 13146, &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "ws", + }, + }, func(conn internet.Connection) { + go func(c internet.Connection) { + defer c.Close() + + var b [1024]byte + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{Path: "ws"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + <-time.After(time.Second * 5) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146), streamSettings) + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, err = conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func TestDialWithRemoteAddr(t *testing.T) { + listen, err := ListenWS(context.Background(), net.LocalHostIP, 13148, &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "ws", + }, + }, func(conn internet.Connection) { + go func(c internet.Connection) { + defer c.Close() + + var b [1024]byte + _, err := c.Read(b[:]) + // common.Must(err) + if err != nil { + return + } + + _, err = c.Write([]byte("Response")) + common.Must(err) + }(conn) + }) + common.Must(err) + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), 13148), &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{Path: "ws", Header: []*Header{{Key: "X-Forwarded-For", Value: "1.1.1.1"}}}, + }) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(listen.Close()) +} + +func Test_listenWSAndDial_TLS(t *testing.T) { + if runtime.GOARCH == "arm64" { + return + } + + start := time.Now() + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "wss", + }, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + AllowInsecure: true, + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("localhost")))}, + }, + } + listen, err := ListenWS(context.Background(), net.LocalHostIP, 13143, streamSettings, func(conn internet.Connection) { + go func() { + _ = conn.Close() + }() + }) + common.Must(err) + defer listen.Close() + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), 13143), streamSettings) + common.Must(err) + _ = conn.Close() + + end := time.Now() + if !end.Before(start.Add(time.Second * 5)) { + t.Error("end: ", end, " start: ", start) + } +} diff --git a/transport/internet/xtls/config.go b/transport/internet/xtls/config.go new file mode 100644 index 00000000..3ebc503a --- /dev/null +++ b/transport/internet/xtls/config.go @@ -0,0 +1,241 @@ +// +build !confonly + +package xtls + +import ( + "crypto/x509" + "sync" + "time" + + xtls "github.com/xtls/go" + + "github.com/xtls/xray-core/v1/common/net" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + "github.com/xtls/xray-core/v1/transport/internet" +) + +var ( + globalSessionCache = xtls.NewLRUClientSessionCache(128) +) + +// ParseCertificate converts a cert.Certificate to Certificate. +func ParseCertificate(c *cert.Certificate) *Certificate { + if c != nil { + certPEM, keyPEM := c.ToPEM() + return &Certificate{ + Certificate: certPEM, + Key: keyPEM, + } + } + return nil +} + +func (c *Config) loadSelfCertPool() (*x509.CertPool, error) { + root := x509.NewCertPool() + for _, cert := range c.Certificate { + if !root.AppendCertsFromPEM(cert.Certificate) { + return nil, newError("failed to append cert").AtWarning() + } + } + return root, nil +} + +// BuildCertificates builds a list of TLS certificates from proto definition. +func (c *Config) BuildCertificates() []xtls.Certificate { + certs := make([]xtls.Certificate, 0, len(c.Certificate)) + for _, entry := range c.Certificate { + if entry.Usage != Certificate_ENCIPHERMENT { + continue + } + keyPair, err := xtls.X509KeyPair(entry.Certificate, entry.Key) + if err != nil { + newError("ignoring invalid X509 key pair").Base(err).AtWarning().WriteToLog() + continue + } + certs = append(certs, keyPair) + } + return certs +} + +func isCertificateExpired(c *xtls.Certificate) bool { + if c.Leaf == nil && len(c.Certificate) > 0 { + if pc, err := x509.ParseCertificate(c.Certificate[0]); err == nil { + c.Leaf = pc + } + } + + // If leaf is not there, the certificate is probably not used yet. We trust user to provide a valid certificate. + return c.Leaf != nil && c.Leaf.NotAfter.Before(time.Now().Add(-time.Minute)) +} + +func issueCertificate(rawCA *Certificate, domain string) (*xtls.Certificate, error) { + parent, err := cert.ParseCertificate(rawCA.Certificate, rawCA.Key) + if err != nil { + return nil, newError("failed to parse raw certificate").Base(err) + } + newCert, err := cert.Generate(parent, cert.CommonName(domain), cert.DNSNames(domain)) + if err != nil { + return nil, newError("failed to generate new certificate for ", domain).Base(err) + } + newCertPEM, newKeyPEM := newCert.ToPEM() + cert, err := xtls.X509KeyPair(newCertPEM, newKeyPEM) + return &cert, err +} + +func (c *Config) getCustomCA() []*Certificate { + certs := make([]*Certificate, 0, len(c.Certificate)) + for _, certificate := range c.Certificate { + if certificate.Usage == Certificate_AUTHORITY_ISSUE { + certs = append(certs, certificate) + } + } + return certs +} + +func getGetCertificateFunc(c *xtls.Config, ca []*Certificate) func(hello *xtls.ClientHelloInfo) (*xtls.Certificate, error) { + var access sync.RWMutex + + return func(hello *xtls.ClientHelloInfo) (*xtls.Certificate, error) { + domain := hello.ServerName + certExpired := false + + access.RLock() + certificate, found := c.NameToCertificate[domain] + access.RUnlock() + + if found { + if !isCertificateExpired(certificate) { + return certificate, nil + } + certExpired = true + } + + if certExpired { + newCerts := make([]xtls.Certificate, 0, len(c.Certificates)) + + access.Lock() + for _, certificate := range c.Certificates { + if !isCertificateExpired(&certificate) { + newCerts = append(newCerts, certificate) + } + } + + c.Certificates = newCerts + access.Unlock() + } + + var issuedCertificate *xtls.Certificate + + // Create a new certificate from existing CA if possible + for _, rawCert := range ca { + if rawCert.Usage == Certificate_AUTHORITY_ISSUE { + newCert, err := issueCertificate(rawCert, domain) + if err != nil { + newError("failed to issue new certificate for ", domain).Base(err).WriteToLog() + continue + } + + access.Lock() + c.Certificates = append(c.Certificates, *newCert) + issuedCertificate = &c.Certificates[len(c.Certificates)-1] + access.Unlock() + break + } + } + + if issuedCertificate == nil { + return nil, newError("failed to create a new certificate for ", domain) + } + + access.Lock() + c.BuildNameToCertificate() + access.Unlock() + + return issuedCertificate, nil + } +} + +func (c *Config) parseServerName() string { + return c.ServerName +} + +// GetXTLSConfig converts this Config into xtls.Config. +func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config { + root, err := c.getCertPool() + if err != nil { + newError("failed to load system root certificate").AtError().Base(err).WriteToLog() + } + + if c == nil { + return &xtls.Config{ + ClientSessionCache: globalSessionCache, + RootCAs: root, + InsecureSkipVerify: false, + NextProtos: nil, + SessionTicketsDisabled: false, + } + } + + config := &xtls.Config{ + ClientSessionCache: globalSessionCache, + RootCAs: root, + InsecureSkipVerify: c.AllowInsecure, + NextProtos: c.NextProtocol, + SessionTicketsDisabled: c.DisableSessionResumption, + } + + for _, opt := range opts { + opt(config) + } + + config.Certificates = c.BuildCertificates() + config.BuildNameToCertificate() + + caCerts := c.getCustomCA() + if len(caCerts) > 0 { + config.GetCertificate = getGetCertificateFunc(config, caCerts) + } + + if sn := c.parseServerName(); len(sn) > 0 { + config.ServerName = sn + } + + if len(config.NextProtos) == 0 { + config.NextProtos = []string{"h2", "http/1.1"} + } + + return config +} + +// Option for building XTLS config. +type Option func(*xtls.Config) + +// WithDestination sets the server name in XTLS config. +func WithDestination(dest net.Destination) Option { + return func(config *xtls.Config) { + if dest.Address.Family().IsDomain() && config.ServerName == "" { + config.ServerName = dest.Address.Domain() + } + } +} + +// WithNextProto sets the ALPN values in XTLS config. +func WithNextProto(protocol ...string) Option { + return func(config *xtls.Config) { + if len(config.NextProtos) == 0 { + config.NextProtos = protocol + } + } +} + +// ConfigFromStreamSettings fetches Config from stream settings. Nil if not found. +func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config { + if settings == nil { + return nil + } + config, ok := settings.SecuritySettings.(*Config) + if !ok { + return nil + } + return config +} diff --git a/transport/internet/xtls/config.pb.go b/transport/internet/xtls/config.pb.go new file mode 100644 index 00000000..701581bc --- /dev/null +++ b/transport/internet/xtls/config.pb.go @@ -0,0 +1,378 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.14.0 +// source: transport/internet/xtls/config.proto + +package xtls + +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 Certificate_Usage int32 + +const ( + Certificate_ENCIPHERMENT Certificate_Usage = 0 + Certificate_AUTHORITY_VERIFY Certificate_Usage = 1 + Certificate_AUTHORITY_ISSUE Certificate_Usage = 2 +) + +// Enum value maps for Certificate_Usage. +var ( + Certificate_Usage_name = map[int32]string{ + 0: "ENCIPHERMENT", + 1: "AUTHORITY_VERIFY", + 2: "AUTHORITY_ISSUE", + } + Certificate_Usage_value = map[string]int32{ + "ENCIPHERMENT": 0, + "AUTHORITY_VERIFY": 1, + "AUTHORITY_ISSUE": 2, + } +) + +func (x Certificate_Usage) Enum() *Certificate_Usage { + p := new(Certificate_Usage) + *p = x + return p +} + +func (x Certificate_Usage) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Certificate_Usage) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_xtls_config_proto_enumTypes[0].Descriptor() +} + +func (Certificate_Usage) Type() protoreflect.EnumType { + return &file_transport_internet_xtls_config_proto_enumTypes[0] +} + +func (x Certificate_Usage) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Certificate_Usage.Descriptor instead. +func (Certificate_Usage) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_xtls_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Certificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // XTLS certificate in x509 format. + Certificate []byte `protobuf:"bytes,1,opt,name=Certificate,proto3" json:"Certificate,omitempty"` + // XTLS key in x509 format. + Key []byte `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"` + Usage Certificate_Usage `protobuf:"varint,3,opt,name=usage,proto3,enum=xray.transport.internet.xtls.Certificate_Usage" json:"usage,omitempty"` +} + +func (x *Certificate) Reset() { + *x = Certificate{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_xtls_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Certificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Certificate) ProtoMessage() {} + +func (x *Certificate) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_xtls_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 Certificate.ProtoReflect.Descriptor instead. +func (*Certificate) Descriptor() ([]byte, []int) { + return file_transport_internet_xtls_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Certificate) GetCertificate() []byte { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Certificate) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *Certificate) GetUsage() Certificate_Usage { + if x != nil { + return x.Usage + } + return Certificate_ENCIPHERMENT +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Whether or not to allow self-signed certificates. + AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` + // Whether or not to allow insecure cipher suites. + AllowInsecureCiphers bool `protobuf:"varint,5,opt,name=allow_insecure_ciphers,json=allowInsecureCiphers,proto3" json:"allow_insecure_ciphers,omitempty"` + // List of certificates to be served on server. + Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` + // Override server name. + ServerName string `protobuf:"bytes,3,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` + // Lists of string as ALPN values. + NextProtocol []string `protobuf:"bytes,4,rep,name=next_protocol,json=nextProtocol,proto3" json:"next_protocol,omitempty"` + // Whether or not to disable session (ticket) resumption. + DisableSessionResumption bool `protobuf:"varint,6,opt,name=disable_session_resumption,json=disableSessionResumption,proto3" json:"disable_session_resumption,omitempty"` + // If true, root certificates on the system will not be loaded for + // verification. + DisableSystemRoot bool `protobuf:"varint,7,opt,name=disable_system_root,json=disableSystemRoot,proto3" json:"disable_system_root,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_xtls_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_transport_internet_xtls_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_transport_internet_xtls_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetAllowInsecure() bool { + if x != nil { + return x.AllowInsecure + } + return false +} + +func (x *Config) GetAllowInsecureCiphers() bool { + if x != nil { + return x.AllowInsecureCiphers + } + return false +} + +func (x *Config) GetCertificate() []*Certificate { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Config) GetServerName() string { + if x != nil { + return x.ServerName + } + return "" +} + +func (x *Config) GetNextProtocol() []string { + if x != nil { + return x.NextProtocol + } + return nil +} + +func (x *Config) GetDisableSessionResumption() bool { + if x != nil { + return x.DisableSessionResumption + } + return false +} + +func (x *Config) GetDisableSystemRoot() bool { + if x != nil { + return x.DisableSystemRoot + } + return false +} + +var File_transport_internet_xtls_config_proto protoreflect.FileDescriptor + +var file_transport_internet_xtls_config_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, + 0x78, 0x74, 0x6c, 0x73, 0x22, 0xce, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, + 0x74, 0x2e, 0x78, 0x74, 0x6c, 0x73, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x2e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x44, 0x0a, 0x05, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x4e, 0x43, 0x49, + 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, + 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, + 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x49, 0x53, + 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe6, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, + 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, + 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x73, 0x12, 0x4b, 0x0a, + 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x78, 0x74, 0x6c, + 0x73, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6e, + 0x65, 0x78, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x3c, 0x0a, 0x1a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, + 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x6f, 0x6f, 0x74, 0x42, 0x79, + 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x78, 0x74, + 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x34, 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, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1c, 0x58, 0x72, 0x61, + 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x58, 0x74, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_transport_internet_xtls_config_proto_rawDescOnce sync.Once + file_transport_internet_xtls_config_proto_rawDescData = file_transport_internet_xtls_config_proto_rawDesc +) + +func file_transport_internet_xtls_config_proto_rawDescGZIP() []byte { + file_transport_internet_xtls_config_proto_rawDescOnce.Do(func() { + file_transport_internet_xtls_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_xtls_config_proto_rawDescData) + }) + return file_transport_internet_xtls_config_proto_rawDescData +} + +var file_transport_internet_xtls_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_transport_internet_xtls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_xtls_config_proto_goTypes = []interface{}{ + (Certificate_Usage)(0), // 0: xray.transport.internet.xtls.Certificate.Usage + (*Certificate)(nil), // 1: xray.transport.internet.xtls.Certificate + (*Config)(nil), // 2: xray.transport.internet.xtls.Config +} +var file_transport_internet_xtls_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.xtls.Certificate.usage:type_name -> xray.transport.internet.xtls.Certificate.Usage + 1, // 1: xray.transport.internet.xtls.Config.certificate:type_name -> xray.transport.internet.xtls.Certificate + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_transport_internet_xtls_config_proto_init() } +func file_transport_internet_xtls_config_proto_init() { + if File_transport_internet_xtls_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_xtls_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Certificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_xtls_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_transport_internet_xtls_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_xtls_config_proto_goTypes, + DependencyIndexes: file_transport_internet_xtls_config_proto_depIdxs, + EnumInfos: file_transport_internet_xtls_config_proto_enumTypes, + MessageInfos: file_transport_internet_xtls_config_proto_msgTypes, + }.Build() + File_transport_internet_xtls_config_proto = out.File + file_transport_internet_xtls_config_proto_rawDesc = nil + file_transport_internet_xtls_config_proto_goTypes = nil + file_transport_internet_xtls_config_proto_depIdxs = nil +} diff --git a/transport/internet/xtls/config.proto b/transport/internet/xtls/config.proto new file mode 100644 index 00000000..9cd0bb7c --- /dev/null +++ b/transport/internet/xtls/config.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package xray.transport.internet.xtls; +option csharp_namespace = "Xray.Transport.Internet.Xtls"; +option go_package = "github.com/xtls/xray-core/v1/transport/internet/xtls"; +option java_package = "com.xray.transport.internet.xtls"; +option java_multiple_files = true; + +message Certificate { + // XTLS certificate in x509 format. + bytes Certificate = 1; + + // XTLS key in x509 format. + bytes Key = 2; + + enum Usage { + ENCIPHERMENT = 0; + AUTHORITY_VERIFY = 1; + AUTHORITY_ISSUE = 2; + } + + Usage usage = 3; +} + +message Config { + // Whether or not to allow self-signed certificates. + bool allow_insecure = 1; + + // Whether or not to allow insecure cipher suites. + bool allow_insecure_ciphers = 5; + + // List of certificates to be served on server. + repeated Certificate certificate = 2; + + // Override server name. + string server_name = 3; + + // Lists of string as ALPN values. + repeated string next_protocol = 4; + + // Whether or not to disable session (ticket) resumption. + bool disable_session_resumption = 6; + + // If true, root certificates on the system will not be loaded for + // verification. + bool disable_system_root = 7; +} diff --git a/transport/internet/xtls/config_other.go b/transport/internet/xtls/config_other.go new file mode 100644 index 00000000..a1dda046 --- /dev/null +++ b/transport/internet/xtls/config_other.go @@ -0,0 +1,53 @@ +// +build !windows +// +build !confonly + +package xtls + +import ( + "crypto/x509" + "sync" +) + +type rootCertsCache struct { + sync.Mutex + pool *x509.CertPool +} + +func (c *rootCertsCache) load() (*x509.CertPool, error) { + c.Lock() + defer c.Unlock() + + if c.pool != nil { + return c.pool, nil + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + c.pool = pool + return pool, nil +} + +var rootCerts rootCertsCache + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + if len(c.Certificate) == 0 { + return rootCerts.load() + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, newError("system root").AtWarning().Base(err) + } + for _, cert := range c.Certificate { + if !pool.AppendCertsFromPEM(cert.Certificate) { + return nil, newError("append cert to root").AtWarning().Base(err) + } + } + return pool, err +} diff --git a/transport/internet/xtls/config_test.go b/transport/internet/xtls/config_test.go new file mode 100644 index 00000000..b63cea84 --- /dev/null +++ b/transport/internet/xtls/config_test.go @@ -0,0 +1,100 @@ +package xtls_test + +import ( + "crypto/x509" + "testing" + "time" + + xtls "github.com/xtls/go" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/protocol/tls/cert" + . "github.com/xtls/xray-core/v1/transport/internet/xtls" +) + +func TestCertificateIssuing(t *testing.T) { + certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + xtlsConfig := c.GetXTLSConfig() + xrayCert, err := xtlsConfig.GetCertificate(&xtls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestExpiredCertificate(t *testing.T) { + caCert := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + expiredCert := cert.MustGenerate(caCert, cert.NotAfter(time.Now().Add(time.Minute*-2)), cert.CommonName("www.example.com"), cert.DNSNames("www.example.com")) + + certificate := ParseCertificate(caCert) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + certificate2 := ParseCertificate(expiredCert) + + c := &Config{ + Certificate: []*Certificate{ + certificate, + certificate2, + }, + } + + xtlsConfig := c.GetXTLSConfig() + xrayCert, err := xtlsConfig.GetCertificate(&xtls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestInsecureCertificates(t *testing.T) { + c := &Config{ + AllowInsecureCiphers: true, + } + + xtlsConfig := c.GetXTLSConfig() + if len(xtlsConfig.CipherSuites) > 0 { + t.Fatal("Unexpected tls cipher suites list: ", xtlsConfig.CipherSuites) + } +} + +func BenchmarkCertificateIssuing(b *testing.B) { + certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + xtlsConfig := c.GetXTLSConfig() + lenCerts := len(xtlsConfig.Certificates) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = xtlsConfig.GetCertificate(&xtls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + delete(xtlsConfig.NameToCertificate, "www.example.com") + xtlsConfig.Certificates = xtlsConfig.Certificates[:lenCerts] + } +} diff --git a/transport/internet/xtls/config_windows.go b/transport/internet/xtls/config_windows.go new file mode 100644 index 00000000..8c5bf01d --- /dev/null +++ b/transport/internet/xtls/config_windows.go @@ -0,0 +1,14 @@ +// +build windows +// +build !confonly + +package xtls + +import "crypto/x509" + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + return nil, nil +} diff --git a/transport/internet/xtls/errors.generated.go b/transport/internet/xtls/errors.generated.go new file mode 100644 index 00000000..044b7579 --- /dev/null +++ b/transport/internet/xtls/errors.generated.go @@ -0,0 +1,9 @@ +package xtls + +import "github.com/xtls/xray-core/v1/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/xtls/xtls.go b/transport/internet/xtls/xtls.go new file mode 100644 index 00000000..e0787a7b --- /dev/null +++ b/transport/internet/xtls/xtls.go @@ -0,0 +1,38 @@ +// +build !confonly + +package xtls + +import ( + xtls "github.com/xtls/go" + + "github.com/xtls/xray-core/v1/common/net" +) + +//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen + +type Conn struct { + *xtls.Conn +} + +func (c *Conn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +// Client initiates a XTLS client handshake on the given connection. +func Client(c net.Conn, config *xtls.Config) net.Conn { + xtlsConn := xtls.Client(c, config) + return &Conn{Conn: xtlsConn} +} + +// Server initiates a XTLS server handshake on the given connection. +func Server(c net.Conn, config *xtls.Config) net.Conn { + xtlsConn := xtls.Server(c, config) + return &Conn{Conn: xtlsConn} +} diff --git a/transport/link.go b/transport/link.go new file mode 100644 index 00000000..e9ffd1c0 --- /dev/null +++ b/transport/link.go @@ -0,0 +1,9 @@ +package transport + +import "github.com/xtls/xray-core/v1/common/buf" + +// Link is a utility for connecting between an inbound and an outbound proxy handler. +type Link struct { + Reader buf.Reader + Writer buf.Writer +} diff --git a/transport/pipe/impl.go b/transport/pipe/impl.go new file mode 100644 index 00000000..b0011f2b --- /dev/null +++ b/transport/pipe/impl.go @@ -0,0 +1,202 @@ +package pipe + +import ( + "errors" + "io" + "runtime" + "sync" + "time" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/signal/done" +) + +type state byte + +const ( + open state = iota + closed + errord +) + +type pipeOption struct { + limit int32 // maximum buffer size in bytes + discardOverflow bool +} + +func (o *pipeOption) isFull(curSize int32) bool { + return o.limit >= 0 && curSize > o.limit +} + +type pipe struct { + sync.Mutex + data buf.MultiBuffer + readSignal *signal.Notifier + writeSignal *signal.Notifier + done *done.Instance + option pipeOption + state state +} + +var errBufferFull = errors.New("buffer full") +var errSlowDown = errors.New("slow down") + +func (p *pipe) getState(forRead bool) error { + switch p.state { + case open: + if !forRead && p.option.isFull(p.data.Len()) { + return errBufferFull + } + return nil + case closed: + if !forRead { + return io.ErrClosedPipe + } + if !p.data.IsEmpty() { + return nil + } + return io.EOF + case errord: + return io.ErrClosedPipe + default: + panic("impossible case") + } +} + +func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) { + p.Lock() + defer p.Unlock() + + if err := p.getState(true); err != nil { + return nil, err + } + + data := p.data + p.data = nil + return data, nil +} + +func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + data, err := p.readMultiBufferInternal() + if data != nil || err != nil { + p.writeSignal.Signal() + return data, err + } + + select { + case <-p.readSignal.Wait(): + case <-p.done.Wait(): + } + } +} + +func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) { + timer := time.NewTimer(d) + defer timer.Stop() + + for { + data, err := p.readMultiBufferInternal() + if data != nil || err != nil { + p.writeSignal.Signal() + return data, err + } + + select { + case <-p.readSignal.Wait(): + case <-p.done.Wait(): + case <-timer.C: + return nil, buf.ErrReadTimeout + } + } +} + +func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error { + p.Lock() + defer p.Unlock() + + if err := p.getState(false); err != nil { + return err + } + + if p.data == nil { + p.data = mb + return nil + } + + p.data, _ = buf.MergeMulti(p.data, mb) + return errSlowDown +} + +func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error { + if mb.IsEmpty() { + return nil + } + + for { + err := p.writeMultiBufferInternal(mb) + if err == nil { + p.readSignal.Signal() + return nil + } + + if err == errSlowDown { + p.readSignal.Signal() + + // Yield current goroutine. Hopefully the reading counterpart can pick up the payload. + runtime.Gosched() + return nil + } + + if err == errBufferFull && p.option.discardOverflow { + buf.ReleaseMulti(mb) + return nil + } + + if err != errBufferFull { + buf.ReleaseMulti(mb) + p.readSignal.Signal() + return err + } + + select { + case <-p.writeSignal.Wait(): + case <-p.done.Wait(): + return io.ErrClosedPipe + } + } +} + +func (p *pipe) Close() error { + p.Lock() + defer p.Unlock() + + if p.state == closed || p.state == errord { + return nil + } + + p.state = closed + common.Must(p.done.Close()) + return nil +} + +// Interrupt implements common.Interruptible. +func (p *pipe) Interrupt() { + p.Lock() + defer p.Unlock() + + if p.state == closed || p.state == errord { + return + } + + p.state = errord + + if !p.data.IsEmpty() { + buf.ReleaseMulti(p.data) + p.data = nil + } + + common.Must(p.done.Close()) +} diff --git a/transport/pipe/pipe.go b/transport/pipe/pipe.go new file mode 100644 index 00000000..45b00402 --- /dev/null +++ b/transport/pipe/pipe.go @@ -0,0 +1,69 @@ +package pipe + +import ( + "context" + + "github.com/xtls/xray-core/v1/common/signal" + "github.com/xtls/xray-core/v1/common/signal/done" + "github.com/xtls/xray-core/v1/features/policy" +) + +// Option for creating new Pipes. +type Option func(*pipeOption) + +// WithoutSizeLimit returns an Option for Pipe to have no size limit. +func WithoutSizeLimit() Option { + return func(opt *pipeOption) { + opt.limit = -1 + } +} + +// WithSizeLimit returns an Option for Pipe to have the given size limit. +func WithSizeLimit(limit int32) Option { + return func(opt *pipeOption) { + opt.limit = limit + } +} + +// DiscardOverflow returns an Option for Pipe to discard writes if full. +func DiscardOverflow() Option { + return func(opt *pipeOption) { + opt.discardOverflow = true + } +} + +// OptionsFromContext returns a list of Options from context. +func OptionsFromContext(ctx context.Context) []Option { + var opt []Option + + bp := policy.BufferPolicyFromContext(ctx) + if bp.PerConnection >= 0 { + opt = append(opt, WithSizeLimit(bp.PerConnection)) + } else { + opt = append(opt, WithoutSizeLimit()) + } + + return opt +} + +// New creates a new Reader and Writer that connects to each other. +func New(opts ...Option) (*Reader, *Writer) { + p := &pipe{ + readSignal: signal.NewNotifier(), + writeSignal: signal.NewNotifier(), + done: done.New(), + option: pipeOption{ + limit: -1, + }, + } + + for _, opt := range opts { + opt(&(p.option)) + } + + return &Reader{ + pipe: p, + }, &Writer{ + pipe: p, + } +} diff --git a/transport/pipe/pipe_test.go b/transport/pipe/pipe_test.go new file mode 100644 index 00000000..446fb824 --- /dev/null +++ b/transport/pipe/pipe_test.go @@ -0,0 +1,153 @@ +package pipe_test + +import ( + "errors" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/v1/common" + "github.com/xtls/xray-core/v1/common/buf" + . "github.com/xtls/xray-core/v1/transport/pipe" +) + +func TestPipeReadWrite(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + + b := buf.New() + b.WriteString("abcd") + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + + b2 := buf.New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b2})) + + rb, err := pReader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(rb.String(), "abcdefg"); r != "" { + t.Error(r) + } +} + +func TestPipeInterrupt(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + payload := []byte{'a', 'b', 'c', 'd'} + b := buf.New() + b.Write(payload) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + pWriter.Interrupt() + + rb, err := pReader.ReadMultiBuffer() + if err != io.ErrClosedPipe { + t.Fatal("expect io.ErrClosePipe, but got ", err) + } + if !rb.IsEmpty() { + t.Fatal("expect empty buffer, but got ", rb.Len()) + } +} + +func TestPipeClose(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + payload := []byte{'a', 'b', 'c', 'd'} + b := buf.New() + common.Must2(b.Write(payload)) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + common.Must(pWriter.Close()) + + rb, err := pReader.ReadMultiBuffer() + common.Must(err) + if rb.String() != string(payload) { + t.Fatal("expect content ", string(payload), " but actually ", rb.String()) + } + + rb, err = pReader.ReadMultiBuffer() + if err != io.EOF { + t.Fatal("expected EOF, but got ", err) + } + if !rb.IsEmpty() { + t.Fatal("expect empty buffer, but got ", rb.String()) + } +} + +func TestPipeLimitZero(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(0)) + bb := buf.New() + common.Must2(bb.Write([]byte{'a', 'b'})) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{bb})) + + var errg errgroup.Group + errg.Go(func() error { + b := buf.New() + b.Write([]byte{'c', 'd'}) + return pWriter.WriteMultiBuffer(buf.MultiBuffer{b}) + }) + errg.Go(func() error { + time.Sleep(time.Second) + + var container buf.MultiBufferContainer + if err := buf.Copy(pReader, &container); err != nil { + return err + } + + if r := cmp.Diff(container.String(), "abcd"); r != "" { + return errors.New(r) + } + return nil + }) + errg.Go(func() error { + time.Sleep(time.Second * 2) + return pWriter.Close() + }) + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestPipeWriteMultiThread(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(0)) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(func() error { + b := buf.New() + b.WriteString("abcd") + return pWriter.WriteMultiBuffer(buf.MultiBuffer{b}) + }) + } + time.Sleep(time.Millisecond * 100) + pWriter.Close() + errg.Wait() + + b, err := pReader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(b[0].Bytes(), []byte{'a', 'b', 'c', 'd'}); r != "" { + t.Error(r) + } +} + +func TestInterfaces(t *testing.T) { + _ = (buf.Reader)(new(Reader)) + _ = (buf.TimeoutReader)(new(Reader)) + + _ = (common.Interruptible)(new(Reader)) + _ = (common.Interruptible)(new(Writer)) + _ = (common.Closable)(new(Writer)) +} + +func BenchmarkPipeReadWrite(b *testing.B) { + reader, writer := New(WithoutSizeLimit()) + a := buf.New() + a.Extend(buf.Size) + c := buf.MultiBuffer{a} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(writer.WriteMultiBuffer(c)) + d, err := reader.ReadMultiBuffer() + common.Must(err) + c = d + } +} diff --git a/transport/pipe/reader.go b/transport/pipe/reader.go new file mode 100644 index 00000000..ba722154 --- /dev/null +++ b/transport/pipe/reader.go @@ -0,0 +1,27 @@ +package pipe + +import ( + "time" + + "github.com/xtls/xray-core/v1/common/buf" +) + +// Reader is a buf.Reader that reads content from a pipe. +type Reader struct { + pipe *pipe +} + +// ReadMultiBuffer implements buf.Reader. +func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) { + return r.pipe.ReadMultiBuffer() +} + +// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise. +func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) { + return r.pipe.ReadMultiBufferTimeout(d) +} + +// Interrupt implements common.Interruptible. +func (r *Reader) Interrupt() { + r.pipe.Interrupt() +} diff --git a/transport/pipe/writer.go b/transport/pipe/writer.go new file mode 100644 index 00000000..a954fc9c --- /dev/null +++ b/transport/pipe/writer.go @@ -0,0 +1,25 @@ +package pipe + +import ( + "github.com/xtls/xray-core/v1/common/buf" +) + +// Writer is a buf.Writer that writes data into a pipe. +type Writer struct { + pipe *pipe +} + +// WriteMultiBuffer implements buf.Writer. +func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { + return w.pipe.WriteMultiBuffer(mb) +} + +// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF. +func (w *Writer) Close() error { + return w.pipe.Close() +} + +// Interrupt implements common.Interruptible. +func (w *Writer) Interrupt() { + w.pipe.Interrupt() +}