RPRX 4 سال پیش
والد
کامیت
c7f7c08ead
100فایلهای تغییر یافته به همراه18560 افزوده شده و 2 حذف شده
  1. 373 0
      LICENSE
  2. 52 2
      README.md
  3. 2 0
      app/app.go
  4. 110 0
      app/commander/commander.go
  5. 227 0
      app/commander/config.pb.go
  6. 21 0
      app/commander/config.proto
  7. 9 0
      app/commander/errors.generated.go
  8. 110 0
      app/commander/outbound.go
  9. 29 0
      app/commander/service.go
  10. 209 0
      app/dispatcher/config.pb.go
  11. 15 0
      app/dispatcher/config.proto
  12. 301 0
      app/dispatcher/default.go
  13. 5 0
      app/dispatcher/dispatcher.go
  14. 9 0
      app/dispatcher/errors.generated.go
  15. 55 0
      app/dispatcher/sniffer.go
  16. 27 0
      app/dispatcher/stats.go
  17. 44 0
      app/dispatcher/stats_test.go
  18. 654 0
      app/dns/config.pb.go
  19. 71 0
      app/dns/config.proto
  20. 4 0
      app/dns/dns.go
  21. 230 0
      app/dns/dnscommon.go
  22. 160 0
      app/dns/dnscommon_test.go
  23. 383 0
      app/dns/dohdns.go
  24. 9 0
      app/dns/errors.generated.go
  25. 124 0
      app/dns/hosts.go
  26. 79 0
      app/dns/hosts_test.go
  27. 56 0
      app/dns/nameserver.go
  28. 24 0
      app/dns/nameserver_test.go
  29. 449 0
      app/dns/server.go
  30. 972 0
      app/dns/server_test.go
  31. 289 0
      app/dns/udpns.go
  32. 53 0
      app/log/command/command.go
  33. 34 0
      app/log/command/command_test.go
  34. 258 0
      app/log/command/config.pb.go
  35. 17 0
      app/log/command/config.proto
  36. 97 0
      app/log/command/config_grpc.pb.go
  37. 9 0
      app/log/command/errors.generated.go
  38. 263 0
      app/log/config.pb.go
  39. 25 0
      app/log/config.proto
  40. 9 0
      app/log/errors.generated.go
  41. 143 0
      app/log/log.go
  42. 53 0
      app/log/log_creator.go
  43. 52 0
      app/log/log_test.go
  44. 93 0
      app/policy/config.go
  45. 729 0
      app/policy/config.pb.go
  46. 51 0
      app/policy/config.proto
  47. 9 0
      app/policy/errors.generated.go
  48. 68 0
      app/policy/manager.go
  49. 45 0
      app/policy/manager_test.go
  50. 4 0
      app/policy/policy.go
  51. 150 0
      app/proxyman/command/command.go
  52. 1065 0
      app/proxyman/command/command.pb.go
  53. 73 0
      app/proxyman/command/command.proto
  54. 277 0
      app/proxyman/command/command_grpc.pb.go
  55. 3 0
      app/proxyman/command/doc.go
  56. 9 0
      app/proxyman/command/errors.generated.go
  57. 39 0
      app/proxyman/config.go
  58. 1049 0
      app/proxyman/config.pb.go
  59. 97 0
      app/proxyman/config.proto
  60. 185 0
      app/proxyman/inbound/always.go
  61. 201 0
      app/proxyman/inbound/dynamic.go
  62. 9 0
      app/proxyman/inbound/errors.generated.go
  63. 178 0
      app/proxyman/inbound/inbound.go
  64. 483 0
      app/proxyman/inbound/worker.go
  65. 9 0
      app/proxyman/outbound/errors.generated.go
  66. 228 0
      app/proxyman/outbound/handler.go
  67. 80 0
      app/proxyman/outbound/handler_test.go
  68. 170 0
      app/proxyman/outbound/outbound.go
  69. 194 0
      app/reverse/bridge.go
  70. 16 0
      app/reverse/config.go
  71. 439 0
      app/reverse/config.pb.go
  72. 32 0
      app/reverse/config.proto
  73. 9 0
      app/reverse/errors.generated.go
  74. 266 0
      app/reverse/portal.go
  75. 20 0
      app/reverse/portal_test.go
  76. 98 0
      app/reverse/reverse.go
  77. 46 0
      app/router/balancing.go
  78. 95 0
      app/router/command/command.go
  79. 532 0
      app/router/command/command.pb.go
  80. 69 0
      app/router/command/command.proto
  81. 161 0
      app/router/command/command_grpc.pb.go
  82. 361 0
      app/router/command/command_test.go
  83. 94 0
      app/router/command/config.go
  84. 9 0
      app/router/command/errors.generated.go
  85. 319 0
      app/router/condition.go
  86. 193 0
      app/router/condition_geoip.go
  87. 195 0
      app/router/condition_geoip_test.go
  88. 446 0
      app/router/condition_test.go
  89. 156 0
      app/router/config.go
  90. 1242 0
      app/router/config.pb.go
  91. 146 0
      app/router/config.proto
  92. 9 0
      app/router/errors.generated.go
  93. 146 0
      app/router/router.go
  94. 198 0
      app/router/router_test.go
  95. 174 0
      app/stats/channel.go
  96. 405 0
      app/stats/channel_test.go
  97. 127 0
      app/stats/command/command.go
  98. 720 0
      app/stats/command/command.pb.go
  99. 55 0
      app/stats/command/command.proto
  100. 169 0
      app/stats/command/command_grpc.pb.go

+ 373 - 0
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.

+ 52 - 2
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)

+ 2 - 0
app/app.go

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 71 - 0
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<string, xray.common.net.IPOrDomain> Hosts = 2 [deprecated = true];
+
+  // Client IP for EDNS client subnet. Must be 4 bytes (IPv4) or 16 bytes
+  // (IPv6).
+  bytes client_ip = 3;
+
+  message HostMapping {
+    DomainMatchingType type = 1;
+    string domain = 2;
+
+    repeated bytes ip = 3;
+
+    // ProxiedDomain indicates the mapped domain has the same IP address on this
+    // domain. Xray will use this domain for IP queries. This field is only
+    // effective if ip is empty.
+    string proxied_domain = 4;
+  }
+
+  repeated HostMapping static_hosts = 4;
+
+  // Tag is the inbound tag of DNS client.
+  string tag = 6;
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 97 - 0
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",
+}

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

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

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

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

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

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

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

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

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

+ 51 - 0
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<uint32, Policy> level = 1;
+  SystemPolicy system = 2;
+}

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

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

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

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

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

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

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

+ 277 - 0
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",
+}

+ 3 - 0
app/proxyman/command/doc.go

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 69 - 0
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<string, string> Attributes = 10;
+  repeated string OutboundGroupTags = 11;
+  string OutboundTag = 12;
+}
+
+// SubscribeRoutingStatsRequest subscribes to routing statistics channel if
+// opened by xray-core.
+// * FieldSelectors selects a subset of fields in routing statistics to return.
+// Valid selectors:
+//  - inbound: Selects connection's inbound tag.
+//  - network: Selects connection's network.
+//  - ip: Equivalent as "ip_source" and "ip_target", selects both source and
+//  target IP.
+//  - port: Equivalent as "port_source" and "port_target", selects both source
+//  and target port.
+//  - domain: Selects target domain.
+//  - protocol: Select connection's protocol.
+//  - user: Select connection's inbound user email.
+//  - attributes: Select connection's additional attributes.
+//  - outbound: Equivalent as "outbound" and "outbound_group", select both
+//  outbound tag and outbound group tags.
+// * If FieldSelectors is left empty, all fields will be returned.
+message SubscribeRoutingStatsRequest {
+  repeated string FieldSelectors = 1;
+}
+
+// TestRouteRequest manually tests a routing result according to the routing
+// context message.
+// * RoutingContext is the routing message without outbound information.
+// * FieldSelectors selects the fields to return in the routing result. All
+// fields are returned if left empty.
+// * PublishResult broadcasts the routing result to routing statistics channel
+// if set true.
+message TestRouteRequest {
+  RoutingContext RoutingContext = 1;
+  repeated string FieldSelectors = 2;
+  bool PublishResult = 3;
+}
+
+service RoutingService {
+  rpc SubscribeRoutingStats(SubscribeRoutingStatsRequest)
+      returns (stream RoutingContext) {}
+  rpc TestRoute(TestRouteRequest) returns (RoutingContext) {}
+}
+
+message Config {}

+ 161 - 0
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",
+}

+ 361 - 0
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: "[email protected]", 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{"[email protected]"},
+				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: "[email protected]", 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)
+		}
+	}
+}

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

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

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

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

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

+ 446 - 0
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{
+					"[email protected]",
+				},
+			},
+			test: []ruleTest{
+				{
+					input:  withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "[email protected]"}}),
+					output: true,
+				},
+				{
+					input:  withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "[email protected]"}}),
+					output: false,
+				},
+				{
+					input:  withBackground(),
+					output: false,
+				},
+			},
+		},
+		{
+			rule: &RoutingRule{
+				Protocol: []string{"http"},
+			},
+			test: []ruleTest{
+				{
+					input:  withContent(&session.Content{Protocol: (&http.SniffHeader{}).Protocol()}),
+					output: true,
+				},
+			},
+		},
+		{
+			rule: &RoutingRule{
+				InboundTag: []string{"test", "test1"},
+			},
+			test: []ruleTest{
+				{
+					input:  withInbound(&session.Inbound{Tag: "test"}),
+					output: true,
+				},
+				{
+					input:  withInbound(&session.Inbound{Tag: "test2"}),
+					output: false,
+				},
+			},
+		},
+		{
+			rule: &RoutingRule{
+				PortList: &net.PortList{
+					Range: []*net.PortRange{
+						{From: 443, To: 443},
+						{From: 1000, To: 1100},
+					},
+				},
+			},
+			test: []ruleTest{
+				{
+					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 443)}),
+					output: true,
+				},
+				{
+					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1100)}),
+					output: true,
+				},
+				{
+					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1005)}),
+					output: true,
+				},
+				{
+					input:  withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 53)}),
+					output: false,
+				},
+			},
+		},
+		{
+			rule: &RoutingRule{
+				SourcePortList: &net.PortList{
+					Range: []*net.PortRange{
+						{From: 123, To: 123},
+						{From: 9993, To: 9999},
+					},
+				},
+			},
+			test: []ruleTest{
+				{
+					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 123)}),
+					output: true,
+				},
+				{
+					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9999)}),
+					output: true,
+				},
+				{
+					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9994)}),
+					output: true,
+				},
+				{
+					input:  withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 53)}),
+					output: false,
+				},
+			},
+		},
+		{
+			rule: &RoutingRule{
+				Protocol:   []string{"http"},
+				Attributes: "attrs[':path'].startswith('/test')",
+			},
+			test: []ruleTest{
+				{
+					input:  withContent(&session.Content{Protocol: "http/1.1", Attributes: map[string]string{":path": "/test/1"}}),
+					output: true,
+				},
+			},
+		},
+	}
+
+	for _, test := range cases {
+		cond, err := test.rule.BuildCondition()
+		common.Must(err)
+
+		for _, subtest := range test.test {
+			actual := cond.Apply(subtest.input)
+			if actual != subtest.output {
+				t.Error("test case failed: ", subtest.input, " expected ", subtest.output, " but got ", actual)
+			}
+		}
+	}
+}
+
+func loadGeoSite(country string) ([]*Domain, error) {
+	geositeBytes, err := filesystem.ReadAsset("geosite.dat")
+	if err != nil {
+		return nil, err
+	}
+	var geositeList GeoSiteList
+	if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
+		return nil, err
+	}
+
+	for _, site := range geositeList.Entry {
+		if site.CountryCode == country {
+			return site.Domain, nil
+		}
+	}
+
+	return nil, errors.New("country not found: " + country)
+}
+
+func TestChinaSites(t *testing.T) {
+	domains, err := loadGeoSite("CN")
+	common.Must(err)
+
+	matcher, err := NewDomainMatcher(domains)
+	common.Must(err)
+
+	type TestCase struct {
+		Domain string
+		Output bool
+	}
+	testCases := []TestCase{
+		{
+			Domain: "163.com",
+			Output: true,
+		},
+		{
+			Domain: "163.com",
+			Output: true,
+		},
+		{
+			Domain: "164.com",
+			Output: false,
+		},
+		{
+			Domain: "164.com",
+			Output: false,
+		},
+	}
+
+	for i := 0; i < 1024; i++ {
+		testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false})
+	}
+
+	for _, testCase := range testCases {
+		r := matcher.ApplyDomain(testCase.Domain)
+		if r != testCase.Output {
+			t.Error("expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r)
+		}
+	}
+}
+
+func BenchmarkMultiGeoIPMatcher(b *testing.B) {
+	var geoips []*GeoIP
+
+	{
+		ips, err := loadGeoIP("CN")
+		common.Must(err)
+		geoips = append(geoips, &GeoIP{
+			CountryCode: "CN",
+			Cidr:        ips,
+		})
+	}
+
+	{
+		ips, err := loadGeoIP("JP")
+		common.Must(err)
+		geoips = append(geoips, &GeoIP{
+			CountryCode: "JP",
+			Cidr:        ips,
+		})
+	}
+
+	{
+		ips, err := loadGeoIP("CA")
+		common.Must(err)
+		geoips = append(geoips, &GeoIP{
+			CountryCode: "CA",
+			Cidr:        ips,
+		})
+	}
+
+	{
+		ips, err := loadGeoIP("US")
+		common.Must(err)
+		geoips = append(geoips, &GeoIP{
+			CountryCode: "US",
+			Cidr:        ips,
+		})
+	}
+
+	matcher, err := NewMultiGeoIPMatcher(geoips, false)
+	common.Must(err)
+
+	ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)})
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		_ = matcher.Apply(ctx)
+	}
+}

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

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

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

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

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

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

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

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

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

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

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

+ 169 - 0
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 نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است