Browse Source

v5: Health Check & LeastLoad Strategy (rebased from 2c5a71490368500a982018a74a6d519c7e121816)

Some changes will be necessary to integrate it into V2Ray
Jebbs 4 years ago
parent
commit
fa0cf6db26

+ 31 - 2
app/dispatcher/default.go

@@ -207,10 +207,16 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
 		content = new(session.Content)
 		ctx = session.ContextWithContent(ctx, content)
 	}
+
+	handler := session.HandlerFromContext(ctx)
 	sniffingRequest := content.SniffingRequest
 	switch {
 	case !sniffingRequest.Enabled:
-		go d.routedDispatch(ctx, outbound, destination)
+		if handler != nil {
+			go d.targetedDispatch(ctx, outbound, handler.Tag)
+		} else {
+			go d.routedDispatch(ctx, outbound, destination)
+		}
 	case destination.Network != net.Network_TCP:
 		// Only metadata sniff will be used for non tcp connection
 		result, err := sniffer(ctx, nil, true)
@@ -240,7 +246,11 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
 				destination.Address = net.ParseAddress(domain)
 				ob.Target = destination
 			}
-			d.routedDispatch(ctx, outbound, destination)
+			if handler != nil {
+				d.targetedDispatch(ctx, outbound, handler.Tag)
+			} else {
+				d.routedDispatch(ctx, outbound, destination)
+			}
 		}()
 	}
 	return inbound, nil
@@ -292,6 +302,25 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni
 	return contentResult, contentErr
 }
 
+//TODO Pending removal for tagged connection
+func (d *DefaultDispatcher) targetedDispatch(ctx context.Context, link *transport.Link, tag string) {
+	handler := d.ohm.GetHandler(tag)
+	if handler == nil {
+		newError("outbound handler [", tag, "] not exist").AtError().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)
+}
+
 func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {
 	var handler outbound.Handler
 

+ 35 - 22
app/router/balancing.go

@@ -5,8 +5,8 @@ package router
 
 import (
 	"context"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
 
-	"github.com/v2fly/v2ray-core/v4/common/dice"
 	"github.com/v2fly/v2ray-core/v4/features/extension"
 	"github.com/v2fly/v2ray-core/v4/features/outbound"
 )
@@ -15,34 +15,37 @@ 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
+	selectors   []string
+	strategy    routing.BalancingStrategy
+	ohm         outbound.Manager
+	fallbackTag string
+
+	override overridden
 }
 
+// PickOutbound picks the tag of a outbound
 func (b *Balancer) PickOutbound() (string, error) {
-	hs, ok := b.ohm.(outbound.HandlerSelector)
-	if !ok {
-		return "", newError("outbound.Manager is not a HandlerSelector")
+	candidates, err := b.SelectOutbounds()
+	if err != nil {
+		if b.fallbackTag != "" {
+			newError("fallback to [", b.fallbackTag, "], due to error: ", err).AtInfo().WriteToLog()
+			return b.fallbackTag, nil
+		}
+		return "", err
 	}
-	tags := hs.Select(b.selectors)
-	if len(tags) == 0 {
-		return "", newError("no available outbounds selected")
+	var tag string
+	if o := b.override.Get(); o != nil {
+		tag = b.strategy.Pick(o.selects)
+	} else {
+		tag = b.strategy.SelectAndPick(candidates)
 	}
-	tag := b.strategy.PickOutbound(tags)
 	if tag == "" {
+		if b.fallbackTag != "" {
+			newError("fallback to [", b.fallbackTag, "], due to empty tag returned").AtInfo().WriteToLog()
+			return b.fallbackTag, nil
+		}
+		// will use default handler
 		return "", newError("balancing strategy returns empty tag")
 	}
 	return tag, nil
@@ -53,3 +56,13 @@ func (b *Balancer) InjectContext(ctx context.Context) {
 		contextReceiver.InjectContext(ctx)
 	}
 }
+
+// SelectOutbounds select outbounds with selectors of the Balancer
+func (b *Balancer) SelectOutbounds() ([]string, error) {
+	hs, ok := b.ohm.(outbound.HandlerSelector)
+	if !ok {
+		return nil, newError("outbound.Manager is not a HandlerSelector")
+	}
+	tags := hs.Select(b.selectors)
+	return tags, nil
+}

+ 83 - 0
app/router/balancing_override.go

@@ -0,0 +1,83 @@
+package router
+
+import (
+	sync "sync"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/features/outbound"
+)
+
+func (b *Balancer) overrideSelecting(selects []string, validity time.Duration) error {
+	if validity <= 0 {
+		b.override.Clear()
+		return nil
+	}
+	hs, ok := b.ohm.(outbound.HandlerSelector)
+	if !ok {
+		return newError("outbound.Manager is not a HandlerSelector")
+	}
+	tags := hs.Select(selects)
+	if len(tags) == 0 {
+		return newError("no outbound selected")
+	}
+	b.override.Put(tags, time.Now().Add(validity))
+	return nil
+}
+
+// OverrideSelecting implements routing.BalancingOverrider
+func (r *Router) OverrideSelecting(balancer string, selects []string, validity time.Duration) error {
+	var b *Balancer
+	for tag, bl := range r.balancers {
+		if tag == balancer {
+			b = bl
+			break
+		}
+	}
+	if b == nil {
+		return newError("balancer '", balancer, "' not found")
+	}
+	err := b.overrideSelecting(selects, validity)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type overriddenSettings struct {
+	selects []string
+	until   time.Time
+}
+
+type overridden struct {
+	access   sync.RWMutex
+	settings overriddenSettings
+}
+
+// Get gets the overridden settings
+func (o *overridden) Get() *overriddenSettings {
+	o.access.RLock()
+	defer o.access.RUnlock()
+	if len(o.settings.selects) == 0 || time.Now().After(o.settings.until) {
+		return nil
+	}
+	return &overriddenSettings{
+		selects: o.settings.selects,
+		until:   o.settings.until,
+	}
+}
+
+// Put updates the overridden settings
+func (o *overridden) Put(selects []string, until time.Time) {
+	o.access.Lock()
+	defer o.access.Unlock()
+	o.settings.selects = selects
+	o.settings.until = until
+}
+
+// Clear clears the overridden settings
+func (o *overridden) Clear() {
+	o.access.Lock()
+	defer o.access.Unlock()
+	o.settings.selects = nil
+	o.settings.until = time.Time{}
+}

+ 77 - 0
app/router/command/command.go

@@ -8,6 +8,9 @@ import (
 
 	"google.golang.org/grpc"
 
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
 	core "github.com/v2fly/v2ray-core/v4"
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/features/routing"
@@ -73,6 +76,80 @@ func (s *routingServer) SubscribeRoutingStats(request *SubscribeRoutingStatsRequ
 	}
 }
 
+func (s *routingServer) GetBalancers(ctx context.Context, request *GetBalancersRequest) (*GetBalancersResponse, error) {
+	h, ok := s.router.(routing.RouterChecker)
+	if !ok {
+		return nil, status.Errorf(codes.Unavailable, "current router is not a health checker")
+	}
+	results, err := h.GetBalancersInfo(request.BalancerTags)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, err.Error())
+	}
+	rsp := &GetBalancersResponse{
+		Balancers: make([]*BalancerMsg, 0),
+	}
+	for _, result := range results {
+		var override *OverrideSelectingMsg
+		if result.Override != nil {
+			override = &OverrideSelectingMsg{
+				Until:   result.Override.Until.Local().String(),
+				Selects: result.Override.Selects,
+			}
+		}
+		stat := &BalancerMsg{
+			Tag:              result.Tag,
+			StrategySettings: result.Strategy.Settings,
+			Titles:           result.Strategy.ValueTitles,
+			Override:         override,
+			Selects:          make([]*OutboundMsg, 0),
+			Others:           make([]*OutboundMsg, 0),
+		}
+		for _, item := range result.Strategy.Selects {
+			stat.Selects = append(stat.Selects, &OutboundMsg{
+				Tag:    item.Tag,
+				Values: item.Values,
+			})
+		}
+		for _, item := range result.Strategy.Others {
+			stat.Others = append(stat.Others, &OutboundMsg{
+				Tag:    item.Tag,
+				Values: item.Values,
+			})
+		}
+		rsp.Balancers = append(rsp.Balancers, stat)
+	}
+	return rsp, nil
+}
+func (s *routingServer) CheckBalancers(ctx context.Context, request *CheckBalancersRequest) (*CheckBalancersResponse, error) {
+	h, ok := s.router.(routing.RouterChecker)
+	if !ok {
+		return nil, status.Errorf(codes.Unavailable, "current router is not a health checker")
+	}
+	go func() {
+		err := h.CheckBalancers(request.BalancerTags)
+		if err != nil {
+			newError("CheckBalancers error:", err).AtInfo().WriteToLog()
+		}
+	}()
+	return &CheckBalancersResponse{}, nil
+}
+
+func (s *routingServer) OverrideSelecting(ctx context.Context, request *OverrideSelectingRequest) (*OverrideSelectingResponse, error) {
+	bo, ok := s.router.(routing.BalancingOverrider)
+	if !ok {
+		return nil, status.Errorf(codes.Unavailable, "current router doesn't support balancing override")
+	}
+	err := bo.OverrideSelecting(
+		request.BalancerTag,
+		request.Selectors,
+		time.Duration(request.Validity),
+	)
+	if err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, err.Error())
+	}
+	return &OverrideSelectingResponse{}, nil
+}
+
 func (s *routingServer) mustEmbedUnimplementedRoutingServiceServer() {}
 
 type service struct {

+ 725 - 43
app/router/command/command.pb.go

@@ -293,6 +293,483 @@ func (x *TestRouteRequest) GetPublishResult() bool {
 	return false
 }
 
+type GetBalancersRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	BalancerTags []string `protobuf:"bytes,1,rep,name=balancerTags,proto3" json:"balancerTags,omitempty"`
+}
+
+func (x *GetBalancersRequest) Reset() {
+	*x = GetBalancersRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBalancersRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBalancersRequest) ProtoMessage() {}
+
+func (x *GetBalancersRequest) 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 GetBalancersRequest.ProtoReflect.Descriptor instead.
+func (*GetBalancersRequest) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GetBalancersRequest) GetBalancerTags() []string {
+	if x != nil {
+		return x.BalancerTags
+	}
+	return nil
+}
+
+type OutboundMsg struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Tag    string   `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
+	Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
+}
+
+func (x *OutboundMsg) Reset() {
+	*x = OutboundMsg{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *OutboundMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OutboundMsg) ProtoMessage() {}
+
+func (x *OutboundMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 OutboundMsg.ProtoReflect.Descriptor instead.
+func (*OutboundMsg) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *OutboundMsg) GetTag() string {
+	if x != nil {
+		return x.Tag
+	}
+	return ""
+}
+
+func (x *OutboundMsg) GetValues() []string {
+	if x != nil {
+		return x.Values
+	}
+	return nil
+}
+
+type OverrideSelectingMsg struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Until   string   `protobuf:"bytes,1,opt,name=until,proto3" json:"until,omitempty"`
+	Selects []string `protobuf:"bytes,2,rep,name=selects,proto3" json:"selects,omitempty"`
+}
+
+func (x *OverrideSelectingMsg) Reset() {
+	*x = OverrideSelectingMsg{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *OverrideSelectingMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OverrideSelectingMsg) ProtoMessage() {}
+
+func (x *OverrideSelectingMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 OverrideSelectingMsg.ProtoReflect.Descriptor instead.
+func (*OverrideSelectingMsg) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *OverrideSelectingMsg) GetUntil() string {
+	if x != nil {
+		return x.Until
+	}
+	return ""
+}
+
+func (x *OverrideSelectingMsg) GetSelects() []string {
+	if x != nil {
+		return x.Selects
+	}
+	return nil
+}
+
+type BalancerMsg struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Tag              string                `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
+	StrategySettings []string              `protobuf:"bytes,2,rep,name=strategySettings,proto3" json:"strategySettings,omitempty"`
+	Titles           []string              `protobuf:"bytes,4,rep,name=titles,proto3" json:"titles,omitempty"`
+	Override         *OverrideSelectingMsg `protobuf:"bytes,5,opt,name=override,proto3" json:"override,omitempty"`
+	Selects          []*OutboundMsg        `protobuf:"bytes,6,rep,name=selects,proto3" json:"selects,omitempty"`
+	Others           []*OutboundMsg        `protobuf:"bytes,7,rep,name=others,proto3" json:"others,omitempty"`
+}
+
+func (x *BalancerMsg) Reset() {
+	*x = BalancerMsg{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BalancerMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BalancerMsg) ProtoMessage() {}
+
+func (x *BalancerMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 BalancerMsg.ProtoReflect.Descriptor instead.
+func (*BalancerMsg) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *BalancerMsg) GetTag() string {
+	if x != nil {
+		return x.Tag
+	}
+	return ""
+}
+
+func (x *BalancerMsg) GetStrategySettings() []string {
+	if x != nil {
+		return x.StrategySettings
+	}
+	return nil
+}
+
+func (x *BalancerMsg) GetTitles() []string {
+	if x != nil {
+		return x.Titles
+	}
+	return nil
+}
+
+func (x *BalancerMsg) GetOverride() *OverrideSelectingMsg {
+	if x != nil {
+		return x.Override
+	}
+	return nil
+}
+
+func (x *BalancerMsg) GetSelects() []*OutboundMsg {
+	if x != nil {
+		return x.Selects
+	}
+	return nil
+}
+
+func (x *BalancerMsg) GetOthers() []*OutboundMsg {
+	if x != nil {
+		return x.Others
+	}
+	return nil
+}
+
+type GetBalancersResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Balancers []*BalancerMsg `protobuf:"bytes,1,rep,name=balancers,proto3" json:"balancers,omitempty"`
+}
+
+func (x *GetBalancersResponse) Reset() {
+	*x = GetBalancersResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[7]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBalancersResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBalancersResponse) ProtoMessage() {}
+
+func (x *GetBalancersResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 GetBalancersResponse.ProtoReflect.Descriptor instead.
+func (*GetBalancersResponse) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetBalancersResponse) GetBalancers() []*BalancerMsg {
+	if x != nil {
+		return x.Balancers
+	}
+	return nil
+}
+
+type CheckBalancersRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	BalancerTags []string `protobuf:"bytes,1,rep,name=balancerTags,proto3" json:"balancerTags,omitempty"`
+}
+
+func (x *CheckBalancersRequest) Reset() {
+	*x = CheckBalancersRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CheckBalancersRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CheckBalancersRequest) ProtoMessage() {}
+
+func (x *CheckBalancersRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 CheckBalancersRequest.ProtoReflect.Descriptor instead.
+func (*CheckBalancersRequest) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *CheckBalancersRequest) GetBalancerTags() []string {
+	if x != nil {
+		return x.BalancerTags
+	}
+	return nil
+}
+
+type CheckBalancersResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *CheckBalancersResponse) Reset() {
+	*x = CheckBalancersResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CheckBalancersResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CheckBalancersResponse) ProtoMessage() {}
+
+func (x *CheckBalancersResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 CheckBalancersResponse.ProtoReflect.Descriptor instead.
+func (*CheckBalancersResponse) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{9}
+}
+
+type OverrideSelectingRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	BalancerTag string   `protobuf:"bytes,1,opt,name=balancerTag,proto3" json:"balancerTag,omitempty"`
+	Selectors   []string `protobuf:"bytes,2,rep,name=selectors,proto3" json:"selectors,omitempty"`
+	Validity    int64    `protobuf:"varint,3,opt,name=validity,proto3" json:"validity,omitempty"`
+}
+
+func (x *OverrideSelectingRequest) Reset() {
+	*x = OverrideSelectingRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[10]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *OverrideSelectingRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OverrideSelectingRequest) ProtoMessage() {}
+
+func (x *OverrideSelectingRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 OverrideSelectingRequest.ProtoReflect.Descriptor instead.
+func (*OverrideSelectingRequest) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *OverrideSelectingRequest) GetBalancerTag() string {
+	if x != nil {
+		return x.BalancerTag
+	}
+	return ""
+}
+
+func (x *OverrideSelectingRequest) GetSelectors() []string {
+	if x != nil {
+		return x.Selectors
+	}
+	return nil
+}
+
+func (x *OverrideSelectingRequest) GetValidity() int64 {
+	if x != nil {
+		return x.Validity
+	}
+	return 0
+}
+
+type OverrideSelectingResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *OverrideSelectingResponse) Reset() {
+	*x = OverrideSelectingResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_command_command_proto_msgTypes[11]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *OverrideSelectingResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OverrideSelectingResponse) ProtoMessage() {}
+
+func (x *OverrideSelectingResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_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 OverrideSelectingResponse.ProtoReflect.Descriptor instead.
+func (*OverrideSelectingResponse) Descriptor() ([]byte, []int) {
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{11}
+}
+
 type Config struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -302,7 +779,7 @@ type Config struct {
 func (x *Config) Reset() {
 	*x = Config{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_app_router_command_command_proto_msgTypes[3]
+		mi := &file_app_router_command_command_proto_msgTypes[12]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -315,7 +792,7 @@ func (x *Config) String() string {
 func (*Config) ProtoMessage() {}
 
 func (x *Config) ProtoReflect() protoreflect.Message {
-	mi := &file_app_router_command_command_proto_msgTypes[3]
+	mi := &file_app_router_command_command_proto_msgTypes[12]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -328,7 +805,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Config.ProtoReflect.Descriptor instead.
 func (*Config) Descriptor() ([]byte, []int) {
-	return file_app_router_command_command_proto_rawDescGZIP(), []int{3}
+	return file_app_router_command_command_proto_rawDescGZIP(), []int{12}
 }
 
 var File_app_router_command_command_proto protoreflect.FileDescriptor
@@ -390,32 +867,110 @@ var file_app_router_command_command_proto_rawDesc = []byte{
 	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, 0x89, 0x02, 0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x87, 0x01, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
-	0x69, 0x62, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12,
-	0x3b, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x2d, 0x2e, 0x76,
+	0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42,
+	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x22, 0x0a, 0x0c, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54,
+	0x61, 0x67, 0x73, 0x22, 0x37, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4d,
+	0x73, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02,
+	0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x46, 0x0a, 0x14,
+	0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6e,
+	0x67, 0x4d, 0x73, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65,
+	0x6c, 0x65, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x6c,
+	0x65, 0x63, 0x74, 0x73, 0x22, 0xbe, 0x02, 0x0a, 0x0b, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65,
+	0x72, 0x4d, 0x73, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65,
+	0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
+	0x52, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e,
+	0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03,
+	0x28, 0x09, 0x52, 0x06, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x08, 0x6f, 0x76,
+	0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x76,
 	0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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,
-	0x6d, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x76,
+	0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65,
+	0x72, 0x72, 0x69, 0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x73,
+	0x67, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x44, 0x0a, 0x07, 0x73,
+	0x65, 0x6c, 0x65, 0x63, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x76,
 	0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x2d, 0x2e,
+	0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x75, 0x74,
+	0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x52, 0x07, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
+	0x73, 0x12, 0x42, 0x0a, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x2a, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61,
+	0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
+	0x64, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4d, 0x73, 0x67, 0x52, 0x06, 0x6f,
+	0x74, 0x68, 0x65, 0x72, 0x73, 0x22, 0x60, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61,
+	0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a,
+	0x09, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x2a, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70,
+	0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
+	0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x52, 0x09, 0x62, 0x61,
+	0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x22, 0x3b, 0x0a, 0x15, 0x43, 0x68, 0x65, 0x63, 0x6b,
+	0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x22, 0x0a, 0x0c, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73,
+	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72,
+	0x54, 0x61, 0x67, 0x73, 0x22, 0x18, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x61, 0x6c,
+	0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76,
+	0x0a, 0x18, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74,
+	0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x62, 0x61,
+	0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0b, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x67, 0x12, 0x1c, 0x0a, 0x09,
+	0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
+	0x09, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61,
+	0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x76, 0x61,
+	0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x22, 0x1b, 0x0a, 0x19, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69,
+	0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0x90, 0x05,
+	0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x12, 0x87, 0x01, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x6f,
+	0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x3b, 0x2e, 0x76, 0x32, 0x72,
+	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
+	0x63, 0x6f, 0x72, 0x65, 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, 0x6d, 0x0a, 0x09, 0x54, 0x65,
+	0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
+	0x63, 0x6f, 0x72, 0x65, 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, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79,
+	0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x12, 0x79, 0x0a, 0x0c, 0x47, 0x65, 0x74,
+	0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x12, 0x32, 0x2e, 0x76, 0x32, 0x72, 0x61,
+	0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c,
+	0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e,
 	0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x78,
-	0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
-	0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
-	0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
-	0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f,
-	0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72,
-	0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1d, 0x56, 0x32, 0x52, 0x61, 0x79,
-	0x2e, 0x43, 0x6f, 0x72, 0x65, 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,
+	0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65,
+	0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x22, 0x00, 0x12, 0x7f, 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x61, 0x6c,
+	0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x12, 0x34, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63,
+	0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63,
+	0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x42, 0x61, 0x6c, 0x61,
+	0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x76,
+	0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f,
+	0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x43, 0x68, 0x65,
+	0x63, 0x6b, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x88, 0x01, 0x0a, 0x11, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69,
+	0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x37, 0x2e, 0x76, 0x32,
+	0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65, 0x72,
+	0x72, 0x69, 0x64, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
+	0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d,
+	0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x53, 0x65, 0x6c,
+	0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
+	0x42, 0x78, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
+	0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f,
+	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d,
+	0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74,
+	0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1d, 0x56, 0x32, 0x52,
+	0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 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 (
@@ -430,28 +985,47 @@ func file_app_router_command_command_proto_rawDescGZIP() []byte {
 	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_msgTypes = make([]protoimpl.MessageInfo, 14)
 var file_app_router_command_command_proto_goTypes = []interface{}{
 	(*RoutingContext)(nil),               // 0: v2ray.core.app.router.command.RoutingContext
 	(*SubscribeRoutingStatsRequest)(nil), // 1: v2ray.core.app.router.command.SubscribeRoutingStatsRequest
 	(*TestRouteRequest)(nil),             // 2: v2ray.core.app.router.command.TestRouteRequest
-	(*Config)(nil),                       // 3: v2ray.core.app.router.command.Config
-	nil,                                  // 4: v2ray.core.app.router.command.RoutingContext.AttributesEntry
-	(net.Network)(0),                     // 5: v2ray.core.common.net.Network
+	(*GetBalancersRequest)(nil),          // 3: v2ray.core.app.router.command.GetBalancersRequest
+	(*OutboundMsg)(nil),                  // 4: v2ray.core.app.router.command.OutboundMsg
+	(*OverrideSelectingMsg)(nil),         // 5: v2ray.core.app.router.command.OverrideSelectingMsg
+	(*BalancerMsg)(nil),                  // 6: v2ray.core.app.router.command.BalancerMsg
+	(*GetBalancersResponse)(nil),         // 7: v2ray.core.app.router.command.GetBalancersResponse
+	(*CheckBalancersRequest)(nil),        // 8: v2ray.core.app.router.command.CheckBalancersRequest
+	(*CheckBalancersResponse)(nil),       // 9: v2ray.core.app.router.command.CheckBalancersResponse
+	(*OverrideSelectingRequest)(nil),     // 10: v2ray.core.app.router.command.OverrideSelectingRequest
+	(*OverrideSelectingResponse)(nil),    // 11: v2ray.core.app.router.command.OverrideSelectingResponse
+	(*Config)(nil),                       // 12: v2ray.core.app.router.command.Config
+	nil,                                  // 13: v2ray.core.app.router.command.RoutingContext.AttributesEntry
+	(net.Network)(0),                     // 14: v2ray.core.common.net.Network
 }
 var file_app_router_command_command_proto_depIdxs = []int32{
-	5, // 0: v2ray.core.app.router.command.RoutingContext.Network:type_name -> v2ray.core.common.net.Network
-	4, // 1: v2ray.core.app.router.command.RoutingContext.Attributes:type_name -> v2ray.core.app.router.command.RoutingContext.AttributesEntry
-	0, // 2: v2ray.core.app.router.command.TestRouteRequest.RoutingContext:type_name -> v2ray.core.app.router.command.RoutingContext
-	1, // 3: v2ray.core.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> v2ray.core.app.router.command.SubscribeRoutingStatsRequest
-	2, // 4: v2ray.core.app.router.command.RoutingService.TestRoute:input_type -> v2ray.core.app.router.command.TestRouteRequest
-	0, // 5: v2ray.core.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> v2ray.core.app.router.command.RoutingContext
-	0, // 6: v2ray.core.app.router.command.RoutingService.TestRoute:output_type -> v2ray.core.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
+	14, // 0: v2ray.core.app.router.command.RoutingContext.Network:type_name -> v2ray.core.common.net.Network
+	13, // 1: v2ray.core.app.router.command.RoutingContext.Attributes:type_name -> v2ray.core.app.router.command.RoutingContext.AttributesEntry
+	0,  // 2: v2ray.core.app.router.command.TestRouteRequest.RoutingContext:type_name -> v2ray.core.app.router.command.RoutingContext
+	5,  // 3: v2ray.core.app.router.command.BalancerMsg.override:type_name -> v2ray.core.app.router.command.OverrideSelectingMsg
+	4,  // 4: v2ray.core.app.router.command.BalancerMsg.selects:type_name -> v2ray.core.app.router.command.OutboundMsg
+	4,  // 5: v2ray.core.app.router.command.BalancerMsg.others:type_name -> v2ray.core.app.router.command.OutboundMsg
+	6,  // 6: v2ray.core.app.router.command.GetBalancersResponse.balancers:type_name -> v2ray.core.app.router.command.BalancerMsg
+	1,  // 7: v2ray.core.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> v2ray.core.app.router.command.SubscribeRoutingStatsRequest
+	2,  // 8: v2ray.core.app.router.command.RoutingService.TestRoute:input_type -> v2ray.core.app.router.command.TestRouteRequest
+	3,  // 9: v2ray.core.app.router.command.RoutingService.GetBalancers:input_type -> v2ray.core.app.router.command.GetBalancersRequest
+	8,  // 10: v2ray.core.app.router.command.RoutingService.CheckBalancers:input_type -> v2ray.core.app.router.command.CheckBalancersRequest
+	10, // 11: v2ray.core.app.router.command.RoutingService.OverrideSelecting:input_type -> v2ray.core.app.router.command.OverrideSelectingRequest
+	0,  // 12: v2ray.core.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> v2ray.core.app.router.command.RoutingContext
+	0,  // 13: v2ray.core.app.router.command.RoutingService.TestRoute:output_type -> v2ray.core.app.router.command.RoutingContext
+	7,  // 14: v2ray.core.app.router.command.RoutingService.GetBalancers:output_type -> v2ray.core.app.router.command.GetBalancersResponse
+	9,  // 15: v2ray.core.app.router.command.RoutingService.CheckBalancers:output_type -> v2ray.core.app.router.command.CheckBalancersResponse
+	11, // 16: v2ray.core.app.router.command.RoutingService.OverrideSelecting:output_type -> v2ray.core.app.router.command.OverrideSelectingResponse
+	12, // [12:17] is the sub-list for method output_type
+	7,  // [7:12] is the sub-list for method input_type
+	7,  // [7:7] is the sub-list for extension type_name
+	7,  // [7:7] is the sub-list for extension extendee
+	0,  // [0:7] is the sub-list for field type_name
 }
 
 func init() { file_app_router_command_command_proto_init() }
@@ -497,6 +1071,114 @@ func file_app_router_command_command_proto_init() {
 			}
 		}
 		file_app_router_command_command_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBalancersRequest); 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[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*OutboundMsg); 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[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*OverrideSelectingMsg); 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[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*BalancerMsg); 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[7].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBalancersResponse); 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[8].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CheckBalancersRequest); 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[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CheckBalancersResponse); 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[10].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*OverrideSelectingRequest); 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[11].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*OverrideSelectingResponse); 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[12].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Config); i {
 			case 0:
 				return &v.state
@@ -515,7 +1197,7 @@ func file_app_router_command_command_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_app_router_command_command_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   5,
+			NumMessages:   14,
 			NumExtensions: 0,
 			NumServices:   1,
 		},

+ 45 - 0
app/router/command/command.proto

@@ -60,10 +60,55 @@ message TestRouteRequest {
   bool PublishResult = 3;
 }
 
+message GetBalancersRequest {
+  repeated string balancerTags = 1;
+}
+
+message OutboundMsg {
+  string tag = 1;
+  repeated string values = 2;
+}
+
+message OverrideSelectingMsg {
+  string until = 1;
+  repeated string selects = 2;
+}
+
+message BalancerMsg {
+  string tag = 1;
+  repeated string strategySettings = 2;
+  repeated string titles = 4;
+  OverrideSelectingMsg override = 5;
+  repeated OutboundMsg selects = 6;
+  repeated OutboundMsg others = 7;
+}
+
+message GetBalancersResponse {
+  repeated BalancerMsg balancers = 1;
+}
+
+message CheckBalancersRequest {
+  repeated string balancerTags = 1;
+}
+
+message CheckBalancersResponse {}
+
+
+message OverrideSelectingRequest {
+  string balancerTag = 1;
+  repeated string selectors = 2;
+  int64 validity = 3;
+}
+
+message OverrideSelectingResponse {}
+
 service RoutingService {
   rpc SubscribeRoutingStats(SubscribeRoutingStatsRequest)
       returns (stream RoutingContext) {}
   rpc TestRoute(TestRouteRequest) returns (RoutingContext) {}
+  rpc GetBalancers(GetBalancersRequest) returns (GetBalancersResponse) {}
+  rpc CheckBalancers(CheckBalancersRequest) returns (CheckBalancersResponse) {}
+  rpc OverrideSelecting(OverrideSelectingRequest) returns (OverrideSelectingResponse) {}
 }
 
 message Config {}

+ 108 - 0
app/router/command/command_grpc.pb.go

@@ -20,6 +20,9 @@ const _ = grpc.SupportPackageIsVersion7
 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)
+	GetBalancers(ctx context.Context, in *GetBalancersRequest, opts ...grpc.CallOption) (*GetBalancersResponse, error)
+	CheckBalancers(ctx context.Context, in *CheckBalancersRequest, opts ...grpc.CallOption) (*CheckBalancersResponse, error)
+	OverrideSelecting(ctx context.Context, in *OverrideSelectingRequest, opts ...grpc.CallOption) (*OverrideSelectingResponse, error)
 }
 
 type routingServiceClient struct {
@@ -71,12 +74,42 @@ func (c *routingServiceClient) TestRoute(ctx context.Context, in *TestRouteReque
 	return out, nil
 }
 
+func (c *routingServiceClient) GetBalancers(ctx context.Context, in *GetBalancersRequest, opts ...grpc.CallOption) (*GetBalancersResponse, error) {
+	out := new(GetBalancersResponse)
+	err := c.cc.Invoke(ctx, "/v2ray.core.app.router.command.RoutingService/GetBalancers", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *routingServiceClient) CheckBalancers(ctx context.Context, in *CheckBalancersRequest, opts ...grpc.CallOption) (*CheckBalancersResponse, error) {
+	out := new(CheckBalancersResponse)
+	err := c.cc.Invoke(ctx, "/v2ray.core.app.router.command.RoutingService/CheckBalancers", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *routingServiceClient) OverrideSelecting(ctx context.Context, in *OverrideSelectingRequest, opts ...grpc.CallOption) (*OverrideSelectingResponse, error) {
+	out := new(OverrideSelectingResponse)
+	err := c.cc.Invoke(ctx, "/v2ray.core.app.router.command.RoutingService/OverrideSelecting", 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)
+	GetBalancers(context.Context, *GetBalancersRequest) (*GetBalancersResponse, error)
+	CheckBalancers(context.Context, *CheckBalancersRequest) (*CheckBalancersResponse, error)
+	OverrideSelecting(context.Context, *OverrideSelectingRequest) (*OverrideSelectingResponse, error)
 	mustEmbedUnimplementedRoutingServiceServer()
 }
 
@@ -90,6 +123,15 @@ func (UnimplementedRoutingServiceServer) SubscribeRoutingStats(*SubscribeRouting
 func (UnimplementedRoutingServiceServer) TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method TestRoute not implemented")
 }
+func (UnimplementedRoutingServiceServer) GetBalancers(context.Context, *GetBalancersRequest) (*GetBalancersResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBalancers not implemented")
+}
+func (UnimplementedRoutingServiceServer) CheckBalancers(context.Context, *CheckBalancersRequest) (*CheckBalancersResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CheckBalancers not implemented")
+}
+func (UnimplementedRoutingServiceServer) OverrideSelecting(context.Context, *OverrideSelectingRequest) (*OverrideSelectingResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method OverrideSelecting not implemented")
+}
 func (UnimplementedRoutingServiceServer) mustEmbedUnimplementedRoutingServiceServer() {}
 
 // UnsafeRoutingServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -142,6 +184,60 @@ func _RoutingService_TestRoute_Handler(srv interface{}, ctx context.Context, dec
 	return interceptor(ctx, in, info, handler)
 }
 
+func _RoutingService_GetBalancers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetBalancersRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RoutingServiceServer).GetBalancers(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/v2ray.core.app.router.command.RoutingService/GetBalancers",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RoutingServiceServer).GetBalancers(ctx, req.(*GetBalancersRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _RoutingService_CheckBalancers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(CheckBalancersRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RoutingServiceServer).CheckBalancers(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/v2ray.core.app.router.command.RoutingService/CheckBalancers",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RoutingServiceServer).CheckBalancers(ctx, req.(*CheckBalancersRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _RoutingService_OverrideSelecting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(OverrideSelectingRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(RoutingServiceServer).OverrideSelecting(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/v2ray.core.app.router.command.RoutingService/OverrideSelecting",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(RoutingServiceServer).OverrideSelecting(ctx, req.(*OverrideSelectingRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 // RoutingService_ServiceDesc is the grpc.ServiceDesc for RoutingService service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -153,6 +249,18 @@ var RoutingService_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "TestRoute",
 			Handler:    _RoutingService_TestRoute_Handler,
 		},
+		{
+			MethodName: "GetBalancers",
+			Handler:    _RoutingService_GetBalancers_Handler,
+		},
+		{
+			MethodName: "CheckBalancers",
+			Handler:    _RoutingService_CheckBalancers_Handler,
+		},
+		{
+			MethodName: "OverrideSelecting",
+			Handler:    _RoutingService_OverrideSelecting_Handler,
+		},
 	},
 	Streams: []grpc.StreamDesc{
 		{

+ 1 - 1
app/router/command/command_test.go

@@ -248,7 +248,7 @@ func TestSerivceTestRoute(t *testing.T) {
 				TargetTag: &router.RoutingRule_Tag{Tag: "out"},
 			},
 		},
-	}, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl)))
+	}, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl), nil))
 
 	lis := bufconn.Listen(1024 * 1024)
 	bufDialer := func(context.Context, string) (net.Conn, error) {

+ 19 - 3
app/router/config.go

@@ -157,7 +157,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
 	return conds, nil
 }
 
-func (br *BalancingRule) Build(ohm outbound.Manager) (*Balancer, error) {
+// Build builds the balancing rule
+func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatcher) (*Balancer, error) {
 	switch br.Strategy {
 	case "leastPing":
 		return &Balancer{
@@ -165,13 +166,28 @@ func (br *BalancingRule) Build(ohm outbound.Manager) (*Balancer, error) {
 			strategy:  &LeastPingStrategy{},
 			ohm:       ohm,
 		}, nil
+	case "leastLoad":
+		i, err := br.StrategySettings.GetInstance()
+		if err != nil {
+			return nil, err
+		}
+		s, ok := i.(*StrategyLeastLoadConfig)
+		if !ok {
+			return nil, newError("not a StrategyLeastLoadConfig").AtError()
+		}
+		leastLoadStrategy := NewLeastLoadStrategy(s, dispatcher)
+		return &Balancer{
+			selectors: br.OutboundSelector,
+			ohm:       ohm, fallbackTag: br.FallbackTag,
+			strategy: leastLoadStrategy,
+		}, nil
 	case "random":
 		fallthrough
 	default:
 		return &Balancer{
 			selectors: br.OutboundSelector,
-			strategy:  &RandomStrategy{},
-			ohm:       ohm,
+			ohm:       ohm, fallbackTag: br.FallbackTag,
+			strategy: &RandomStrategy{},
 		}, nil
 	}
 }

+ 521 - 177
app/router/config.pb.go

@@ -8,6 +8,7 @@ package router
 
 import (
 	net "github.com/v2fly/v2ray-core/v4/common/net"
+	serial "github.com/v2fly/v2ray-core/v4/common/serial"
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 	reflect "reflect"
@@ -131,7 +132,7 @@ func (x Config_DomainStrategy) Number() protoreflect.EnumNumber {
 
 // Deprecated: Use Config_DomainStrategy.Descriptor instead.
 func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) {
-	return file_app_router_config_proto_rawDescGZIP(), []int{8, 0}
+	return file_app_router_config_proto_rawDescGZIP(), []int{11, 0}
 }
 
 // Domain for routing decision.
@@ -706,9 +707,11 @@ type BalancingRule struct {
 	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"`
-	Strategy         string   `protobuf:"bytes,3,opt,name=strategy,proto3" json:"strategy,omitempty"`
+	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"`
+	Strategy         string               `protobuf:"bytes,3,opt,name=strategy,proto3" json:"strategy,omitempty"`
+	StrategySettings *serial.TypedMessage `protobuf:"bytes,4,opt,name=strategy_settings,json=strategySettings,proto3" json:"strategy_settings,omitempty"`
+	FallbackTag      string               `protobuf:"bytes,5,opt,name=fallback_tag,json=fallbackTag,proto3" json:"fallback_tag,omitempty"`
 }
 
 func (x *BalancingRule) Reset() {
@@ -764,6 +767,260 @@ func (x *BalancingRule) GetStrategy() string {
 	return ""
 }
 
+func (x *BalancingRule) GetStrategySettings() *serial.TypedMessage {
+	if x != nil {
+		return x.StrategySettings
+	}
+	return nil
+}
+
+func (x *BalancingRule) GetFallbackTag() string {
+	if x != nil {
+		return x.FallbackTag
+	}
+	return ""
+}
+
+type StrategyWeight struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Regexp bool    `protobuf:"varint,1,opt,name=regexp,proto3" json:"regexp,omitempty"`
+	Match  string  `protobuf:"bytes,2,opt,name=match,proto3" json:"match,omitempty"`
+	Value  float32 `protobuf:"fixed32,3,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *StrategyWeight) Reset() {
+	*x = StrategyWeight{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_config_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *StrategyWeight) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StrategyWeight) ProtoMessage() {}
+
+func (x *StrategyWeight) 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 StrategyWeight.ProtoReflect.Descriptor instead.
+func (*StrategyWeight) Descriptor() ([]byte, []int) {
+	return file_app_router_config_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *StrategyWeight) GetRegexp() bool {
+	if x != nil {
+		return x.Regexp
+	}
+	return false
+}
+
+func (x *StrategyWeight) GetMatch() string {
+	if x != nil {
+		return x.Match
+	}
+	return ""
+}
+
+func (x *StrategyWeight) GetValue() float32 {
+	if x != nil {
+		return x.Value
+	}
+	return 0
+}
+
+type StrategyLeastLoadConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	HealthCheck *HealthPingConfig `protobuf:"bytes,1,opt,name=healthCheck,proto3" json:"healthCheck,omitempty"`
+	// weight settings
+	Costs []*StrategyWeight `protobuf:"bytes,2,rep,name=costs,proto3" json:"costs,omitempty"`
+	// RTT baselines for selecting, int64 values of time.Duration
+	Baselines []int64 `protobuf:"varint,3,rep,packed,name=baselines,proto3" json:"baselines,omitempty"`
+	// expected nodes count to select
+	Expected int32 `protobuf:"varint,4,opt,name=expected,proto3" json:"expected,omitempty"`
+	// max acceptable rtt, filter away high delay nodes. defalut 0
+	MaxRTT int64 `protobuf:"varint,5,opt,name=maxRTT,proto3" json:"maxRTT,omitempty"`
+	// acceptable failure rate
+	Tolerance float32 `protobuf:"fixed32,6,opt,name=tolerance,proto3" json:"tolerance,omitempty"`
+}
+
+func (x *StrategyLeastLoadConfig) Reset() {
+	*x = StrategyLeastLoadConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_config_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *StrategyLeastLoadConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StrategyLeastLoadConfig) ProtoMessage() {}
+
+func (x *StrategyLeastLoadConfig) 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 StrategyLeastLoadConfig.ProtoReflect.Descriptor instead.
+func (*StrategyLeastLoadConfig) Descriptor() ([]byte, []int) {
+	return file_app_router_config_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *StrategyLeastLoadConfig) GetHealthCheck() *HealthPingConfig {
+	if x != nil {
+		return x.HealthCheck
+	}
+	return nil
+}
+
+func (x *StrategyLeastLoadConfig) GetCosts() []*StrategyWeight {
+	if x != nil {
+		return x.Costs
+	}
+	return nil
+}
+
+func (x *StrategyLeastLoadConfig) GetBaselines() []int64 {
+	if x != nil {
+		return x.Baselines
+	}
+	return nil
+}
+
+func (x *StrategyLeastLoadConfig) GetExpected() int32 {
+	if x != nil {
+		return x.Expected
+	}
+	return 0
+}
+
+func (x *StrategyLeastLoadConfig) GetMaxRTT() int64 {
+	if x != nil {
+		return x.MaxRTT
+	}
+	return 0
+}
+
+func (x *StrategyLeastLoadConfig) GetTolerance() float32 {
+	if x != nil {
+		return x.Tolerance
+	}
+	return 0
+}
+
+type HealthPingConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// destination url, need 204 for success return
+	// default http://www.google.com/gen_204
+	Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"`
+	// connectivity check url
+	Connectivity string `protobuf:"bytes,2,opt,name=connectivity,proto3" json:"connectivity,omitempty"`
+	// health check interval, int64 values of time.Duration
+	Interval int64 `protobuf:"varint,3,opt,name=interval,proto3" json:"interval,omitempty"`
+	// samplingcount is the amount of recent ping results which are kept for calculation
+	SamplingCount int32 `protobuf:"varint,4,opt,name=samplingCount,proto3" json:"samplingCount,omitempty"`
+	// ping timeout, int64 values of time.Duration
+	Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
+}
+
+func (x *HealthPingConfig) Reset() {
+	*x = HealthPingConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_app_router_config_proto_msgTypes[10]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HealthPingConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthPingConfig) ProtoMessage() {}
+
+func (x *HealthPingConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_app_router_config_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 HealthPingConfig.ProtoReflect.Descriptor instead.
+func (*HealthPingConfig) Descriptor() ([]byte, []int) {
+	return file_app_router_config_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *HealthPingConfig) GetDestination() string {
+	if x != nil {
+		return x.Destination
+	}
+	return ""
+}
+
+func (x *HealthPingConfig) GetConnectivity() string {
+	if x != nil {
+		return x.Connectivity
+	}
+	return ""
+}
+
+func (x *HealthPingConfig) GetInterval() int64 {
+	if x != nil {
+		return x.Interval
+	}
+	return 0
+}
+
+func (x *HealthPingConfig) GetSamplingCount() int32 {
+	if x != nil {
+		return x.SamplingCount
+	}
+	return 0
+}
+
+func (x *HealthPingConfig) GetTimeout() int64 {
+	if x != nil {
+		return x.Timeout
+	}
+	return 0
+}
+
 type Config struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -777,7 +1034,7 @@ type Config struct {
 func (x *Config) Reset() {
 	*x = Config{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_app_router_config_proto_msgTypes[8]
+		mi := &file_app_router_config_proto_msgTypes[11]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -790,7 +1047,7 @@ func (x *Config) String() string {
 func (*Config) ProtoMessage() {}
 
 func (x *Config) ProtoReflect() protoreflect.Message {
-	mi := &file_app_router_config_proto_msgTypes[8]
+	mi := &file_app_router_config_proto_msgTypes[11]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -803,7 +1060,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Config.ProtoReflect.Descriptor instead.
 func (*Config) Descriptor() ([]byte, []int) {
-	return file_app_router_config_proto_rawDescGZIP(), []int{8}
+	return file_app_router_config_proto_rawDescGZIP(), []int{11}
 }
 
 func (x *Config) GetDomainStrategy() Config_DomainStrategy {
@@ -842,7 +1099,7 @@ type Domain_Attribute struct {
 func (x *Domain_Attribute) Reset() {
 	*x = Domain_Attribute{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_app_router_config_proto_msgTypes[9]
+		mi := &file_app_router_config_proto_msgTypes[12]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -855,7 +1112,7 @@ func (x *Domain_Attribute) String() string {
 func (*Domain_Attribute) ProtoMessage() {}
 
 func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
-	mi := &file_app_router_config_proto_msgTypes[9]
+	mi := &file_app_router_config_proto_msgTypes[12]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -921,143 +1178,187 @@ 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, 0x15, 0x76, 0x32, 0x72, 0x61, 0x79,
 	0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0xbf, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x36, 0x0a, 0x04,
-	0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72,
-	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x45, 0x0a, 0x09, 0x61, 0x74,
-	0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e,
+	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, 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, 0xbf, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
+	0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e,
 	0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x80, 0x01, 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, 0x2f, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b,
-	0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e,
-	0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64,
-	0x72, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74,
-	0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73,
-	0x65, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x22, 0x3f, 0x0a, 0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c,
-	0x69, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x63, 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, 0x35, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
-	0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
-	0x72, 0x65, 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, 0x43, 0x0a, 0x0b,
-	0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x65,
-	0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x76, 0x32, 0x72,
-	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0xf1, 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, 0x35, 0x0a, 0x06,
-	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76,
-	0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x33, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28,
+	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, 0x45, 0x0a,
+	0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x27, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x80, 0x01, 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, 0x2f, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28,
 	0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x32, 0x0a, 0x05, 0x67, 0x65, 0x6f, 0x69,
-	0x70, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
-	0x63, 0x6f, 0x72, 0x65, 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, 0x43, 0x0a, 0x0a,
-	0x70, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x20, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x3c, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x0e,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
-	0x65, 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,
-	0x49, 0x0a, 0x0c, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18,
-	0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
-	0x72, 0x65, 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, 0x3a, 0x0a, 0x08, 0x6e, 0x65,
-	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x76,
-	0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x40, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
-	0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x32,
-	0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x3f, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72,
-	0x63, 0x65, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c,
-	0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x49, 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, 0x1f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
+	0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04,
+	0x63, 0x69, 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f,
+	0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x76,
+	0x65, 0x72, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x22, 0x3f, 0x0a, 0x09, 0x47, 0x65, 0x6f,
+	0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
+	0x72, 0x65, 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, 0x63, 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, 0x35, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61,
+	0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79,
+	0x2e, 0x63, 0x6f, 0x72, 0x65, 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,
+	0x43, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x34,
+	0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e,
+	0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0xf1, 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,
+	0x35, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x1d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x33, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x03,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
+	0x65, 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, 0x32, 0x0a, 0x05, 0x67,
+	0x65, 0x6f, 0x69, 0x70, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x76, 0x32, 0x72,
+	0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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,
+	0x43, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
 	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,
-	0x12, 0x25, 0x0a, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,
-	0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
-	0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65,
-	0x74, 0x5f, 0x74, 0x61, 0x67, 0x22, 0x6a, 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, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
-	0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
-	0x79, 0x22, 0xad, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0f,
-	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f,
-	0x72, 0x65, 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, 0x36, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28,
-	0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x4b, 0x0a, 0x0e, 0x62,
-	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
-	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, 0x60, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63,
-	0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01,
-	0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66,
-	0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34,
-	0x2f, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x15, 0x56, 0x32,
-	0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75,
-	0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x52,
+	0x61, 0x6e, 0x67, 0x65, 0x12, 0x3c, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6c, 0x69, 0x73,
+	0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e,
+	0x63, 0x6f, 0x72, 0x65, 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, 0x49, 0x0a, 0x0c, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x69,
+	0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79,
+	0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x3a, 0x0a,
+	0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0e, 0x32,
+	0x1e, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x40, 0x0a, 0x0b, 0x73, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b,
+	0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x3f, 0x0a, 0x0c, 0x73,
+	0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0b, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x1c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x49, 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, 0x1f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63,
+	0x6f, 0x72, 0x65, 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, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6d, 0x61,
+	0x74, 0x63, 0x68, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61,
+	0x72, 0x67, 0x65, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x22, 0xe2, 0x01, 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, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72,
+	0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72,
+	0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x53, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
+	0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x26, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x63, 0x6f,
+	0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65,
+	0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65,
+	0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61,
+	0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0b, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x22, 0x54, 0x0a,
+	0x0e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12,
+	0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a,
+	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x76, 0x61,
+	0x6c, 0x75, 0x65, 0x22, 0x91, 0x02, 0x0a, 0x17, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79,
+	0x4c, 0x65, 0x61, 0x73, 0x74, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+	0x49, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72,
+	0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61,
+	0x6c, 0x74, 0x68, 0x50, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x68,
+	0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x3b, 0x0a, 0x05, 0x63, 0x6f,
+	0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x76, 0x32, 0x72, 0x61,
+	0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x2e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74,
+	0x52, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c,
+	0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, 0x62, 0x61, 0x73, 0x65,
+	0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65,
+	0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65,
+	0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28,
+	0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c,
+	0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f,
+	0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x10, 0x48, 0x65, 0x61, 0x6c,
+	0x74, 0x68, 0x50, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, 0x0b,
+	0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22,
+	0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69,
+	0x74, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x24,
+	0x0a, 0x0d, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18,
+	0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67, 0x43,
+	0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x22, 0xad,
+	0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0f, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x36, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22,
+	0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x4b, 0x0a, 0x0e, 0x62, 0x61, 0x6c, 0x61,
+	0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x24, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 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, 0x60,
+	0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65,
+	0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x29, 0x67,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f,
+	0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70,
+	0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x15, 0x56, 0x32, 0x52, 0x61, 0x79,
+	0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1073,28 +1374,32 @@ func file_app_router_config_proto_rawDescGZIP() []byte {
 }
 
 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_msgTypes = make([]protoimpl.MessageInfo, 13)
 var file_app_router_config_proto_goTypes = []interface{}{
-	(Domain_Type)(0),           // 0: v2ray.core.app.router.Domain.Type
-	(Config_DomainStrategy)(0), // 1: v2ray.core.app.router.Config.DomainStrategy
-	(*Domain)(nil),             // 2: v2ray.core.app.router.Domain
-	(*CIDR)(nil),               // 3: v2ray.core.app.router.CIDR
-	(*GeoIP)(nil),              // 4: v2ray.core.app.router.GeoIP
-	(*GeoIPList)(nil),          // 5: v2ray.core.app.router.GeoIPList
-	(*GeoSite)(nil),            // 6: v2ray.core.app.router.GeoSite
-	(*GeoSiteList)(nil),        // 7: v2ray.core.app.router.GeoSiteList
-	(*RoutingRule)(nil),        // 8: v2ray.core.app.router.RoutingRule
-	(*BalancingRule)(nil),      // 9: v2ray.core.app.router.BalancingRule
-	(*Config)(nil),             // 10: v2ray.core.app.router.Config
-	(*Domain_Attribute)(nil),   // 11: v2ray.core.app.router.Domain.Attribute
-	(*net.PortRange)(nil),      // 12: v2ray.core.common.net.PortRange
-	(*net.PortList)(nil),       // 13: v2ray.core.common.net.PortList
-	(*net.NetworkList)(nil),    // 14: v2ray.core.common.net.NetworkList
-	(net.Network)(0),           // 15: v2ray.core.common.net.Network
+	(Domain_Type)(0),                // 0: v2ray.core.app.router.Domain.Type
+	(Config_DomainStrategy)(0),      // 1: v2ray.core.app.router.Config.DomainStrategy
+	(*Domain)(nil),                  // 2: v2ray.core.app.router.Domain
+	(*CIDR)(nil),                    // 3: v2ray.core.app.router.CIDR
+	(*GeoIP)(nil),                   // 4: v2ray.core.app.router.GeoIP
+	(*GeoIPList)(nil),               // 5: v2ray.core.app.router.GeoIPList
+	(*GeoSite)(nil),                 // 6: v2ray.core.app.router.GeoSite
+	(*GeoSiteList)(nil),             // 7: v2ray.core.app.router.GeoSiteList
+	(*RoutingRule)(nil),             // 8: v2ray.core.app.router.RoutingRule
+	(*BalancingRule)(nil),           // 9: v2ray.core.app.router.BalancingRule
+	(*StrategyWeight)(nil),          // 10: v2ray.core.app.router.StrategyWeight
+	(*StrategyLeastLoadConfig)(nil), // 11: v2ray.core.app.router.StrategyLeastLoadConfig
+	(*HealthPingConfig)(nil),        // 12: v2ray.core.app.router.HealthPingConfig
+	(*Config)(nil),                  // 13: v2ray.core.app.router.Config
+	(*Domain_Attribute)(nil),        // 14: v2ray.core.app.router.Domain.Attribute
+	(*net.PortRange)(nil),           // 15: v2ray.core.common.net.PortRange
+	(*net.PortList)(nil),            // 16: v2ray.core.common.net.PortList
+	(*net.NetworkList)(nil),         // 17: v2ray.core.common.net.NetworkList
+	(net.Network)(0),                // 18: v2ray.core.common.net.Network
+	(*serial.TypedMessage)(nil),     // 19: v2ray.core.common.serial.TypedMessage
 }
 var file_app_router_config_proto_depIdxs = []int32{
 	0,  // 0: v2ray.core.app.router.Domain.type:type_name -> v2ray.core.app.router.Domain.Type
-	11, // 1: v2ray.core.app.router.Domain.attribute:type_name -> v2ray.core.app.router.Domain.Attribute
+	14, // 1: v2ray.core.app.router.Domain.attribute:type_name -> v2ray.core.app.router.Domain.Attribute
 	3,  // 2: v2ray.core.app.router.GeoIP.cidr:type_name -> v2ray.core.app.router.CIDR
 	4,  // 3: v2ray.core.app.router.GeoIPList.entry:type_name -> v2ray.core.app.router.GeoIP
 	2,  // 4: v2ray.core.app.router.GeoSite.domain:type_name -> v2ray.core.app.router.Domain
@@ -1102,21 +1407,24 @@ var file_app_router_config_proto_depIdxs = []int32{
 	2,  // 6: v2ray.core.app.router.RoutingRule.domain:type_name -> v2ray.core.app.router.Domain
 	3,  // 7: v2ray.core.app.router.RoutingRule.cidr:type_name -> v2ray.core.app.router.CIDR
 	4,  // 8: v2ray.core.app.router.RoutingRule.geoip:type_name -> v2ray.core.app.router.GeoIP
-	12, // 9: v2ray.core.app.router.RoutingRule.port_range:type_name -> v2ray.core.common.net.PortRange
-	13, // 10: v2ray.core.app.router.RoutingRule.port_list:type_name -> v2ray.core.common.net.PortList
-	14, // 11: v2ray.core.app.router.RoutingRule.network_list:type_name -> v2ray.core.common.net.NetworkList
-	15, // 12: v2ray.core.app.router.RoutingRule.networks:type_name -> v2ray.core.common.net.Network
+	15, // 9: v2ray.core.app.router.RoutingRule.port_range:type_name -> v2ray.core.common.net.PortRange
+	16, // 10: v2ray.core.app.router.RoutingRule.port_list:type_name -> v2ray.core.common.net.PortList
+	17, // 11: v2ray.core.app.router.RoutingRule.network_list:type_name -> v2ray.core.common.net.NetworkList
+	18, // 12: v2ray.core.app.router.RoutingRule.networks:type_name -> v2ray.core.common.net.Network
 	3,  // 13: v2ray.core.app.router.RoutingRule.source_cidr:type_name -> v2ray.core.app.router.CIDR
 	4,  // 14: v2ray.core.app.router.RoutingRule.source_geoip:type_name -> v2ray.core.app.router.GeoIP
-	13, // 15: v2ray.core.app.router.RoutingRule.source_port_list:type_name -> v2ray.core.common.net.PortList
-	1,  // 16: v2ray.core.app.router.Config.domain_strategy:type_name -> v2ray.core.app.router.Config.DomainStrategy
-	8,  // 17: v2ray.core.app.router.Config.rule:type_name -> v2ray.core.app.router.RoutingRule
-	9,  // 18: v2ray.core.app.router.Config.balancing_rule:type_name -> v2ray.core.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
+	16, // 15: v2ray.core.app.router.RoutingRule.source_port_list:type_name -> v2ray.core.common.net.PortList
+	19, // 16: v2ray.core.app.router.BalancingRule.strategy_settings:type_name -> v2ray.core.common.serial.TypedMessage
+	12, // 17: v2ray.core.app.router.StrategyLeastLoadConfig.healthCheck:type_name -> v2ray.core.app.router.HealthPingConfig
+	10, // 18: v2ray.core.app.router.StrategyLeastLoadConfig.costs:type_name -> v2ray.core.app.router.StrategyWeight
+	1,  // 19: v2ray.core.app.router.Config.domain_strategy:type_name -> v2ray.core.app.router.Config.DomainStrategy
+	8,  // 20: v2ray.core.app.router.Config.rule:type_name -> v2ray.core.app.router.RoutingRule
+	9,  // 21: v2ray.core.app.router.Config.balancing_rule:type_name -> v2ray.core.app.router.BalancingRule
+	22, // [22:22] is the sub-list for method output_type
+	22, // [22:22] is the sub-list for method input_type
+	22, // [22:22] is the sub-list for extension type_name
+	22, // [22:22] is the sub-list for extension extendee
+	0,  // [0:22] is the sub-list for field type_name
 }
 
 func init() { file_app_router_config_proto_init() }
@@ -1222,7 +1530,7 @@ func file_app_router_config_proto_init() {
 			}
 		}
 		file_app_router_config_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*Config); i {
+			switch v := v.(*StrategyWeight); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1234,6 +1542,42 @@ func file_app_router_config_proto_init() {
 			}
 		}
 		file_app_router_config_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*StrategyLeastLoadConfig); 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[10].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HealthPingConfig); 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[11].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[12].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Domain_Attribute); i {
 			case 0:
 				return &v.state
@@ -1250,7 +1594,7 @@ func file_app_router_config_proto_init() {
 		(*RoutingRule_Tag)(nil),
 		(*RoutingRule_BalancingTag)(nil),
 	}
-	file_app_router_config_proto_msgTypes[9].OneofWrappers = []interface{}{
+	file_app_router_config_proto_msgTypes[12].OneofWrappers = []interface{}{
 		(*Domain_Attribute_BoolValue)(nil),
 		(*Domain_Attribute_IntValue)(nil),
 	}
@@ -1260,7 +1604,7 @@ func file_app_router_config_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_app_router_config_proto_rawDesc,
 			NumEnums:      2,
-			NumMessages:   10,
+			NumMessages:   13,
 			NumExtensions: 0,
 			NumServices:   0,
 		},

+ 37 - 0
app/router/config.proto

@@ -6,6 +6,7 @@ option go_package = "github.com/v2fly/v2ray-core/v4/app/router";
 option java_package = "com.v2ray.core.app.router";
 option java_multiple_files = true;
 
+import "common/serial/typed_message.proto";
 import "common/net/port.proto";
 import "common/net/network.proto";
 
@@ -128,6 +129,42 @@ message BalancingRule {
   string tag = 1;
   repeated string outbound_selector = 2;
   string strategy = 3;
+  v2ray.core.common.serial.TypedMessage strategy_settings = 4;
+  string fallback_tag = 5;
+}
+
+message StrategyWeight {
+  bool regexp = 1;
+  string match = 2;
+  float value =3;
+}
+
+message StrategyLeastLoadConfig {
+  HealthPingConfig healthCheck = 1;
+  // weight settings
+  repeated StrategyWeight costs = 2;
+  // RTT baselines for selecting, int64 values of time.Duration
+  repeated int64 baselines = 3;
+  // expected nodes count to select
+  int32 expected = 4;
+  // max acceptable rtt, filter away high delay nodes. defalut 0
+  int64 maxRTT = 5;
+  // acceptable failure rate
+  float tolerance = 6;
+}
+
+message HealthPingConfig {
+  // destination url, need 204 for success return
+  // default http://www.google.com/gen_204
+  string destination = 1;
+  // connectivity check url
+  string connectivity = 2;
+  // health check interval, int64 values of time.Duration
+  int64 interval = 3;
+  // samplingcount is the amount of recent ping results which are kept for calculation
+  int32 samplingCount = 4;
+  // ping timeout, int64 values of time.Duration
+  int64 timeout = 5;
 }
 
 message Config {

+ 231 - 0
app/router/healthping.go

@@ -0,0 +1,231 @@
+package router
+
+import (
+	"fmt"
+	"strings"
+	sync "sync"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/common/dice"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
+)
+
+// HealthPingSettings holds settings for health Checker
+type HealthPingSettings struct {
+	Destination   string        `json:"destination"`
+	Connectivity  string        `json:"connectivity"`
+	Interval      time.Duration `json:"interval"`
+	SamplingCount int           `json:"sampling"`
+	Timeout       time.Duration `json:"timeout"`
+}
+
+// HealthPing is the health checker for balancers
+type HealthPing struct {
+	access     sync.Mutex
+	ticker     *time.Ticker
+	dispatcher routing.Dispatcher
+
+	Settings *HealthPingSettings
+	Results  map[string]*HealthPingRTTS
+}
+
+// NewHealthPing creates a new HealthPing with settings
+func NewHealthPing(config *HealthPingConfig, dispatcher routing.Dispatcher) *HealthPing {
+	settings := &HealthPingSettings{}
+	if config != nil {
+		settings = &HealthPingSettings{
+			Connectivity:  strings.TrimSpace(config.Connectivity),
+			Destination:   strings.TrimSpace(config.Destination),
+			Interval:      time.Duration(config.Interval),
+			SamplingCount: int(config.SamplingCount),
+			Timeout:       time.Duration(config.Timeout),
+		}
+	}
+	if settings.Destination == "" {
+		settings.Destination = "http://www.google.com/gen_204"
+	}
+	if settings.Interval == 0 {
+		settings.Interval = time.Duration(1) * time.Minute
+	} else if settings.Interval < 10 {
+		newError("health check interval is too small, 10s is applied").AtWarning().WriteToLog()
+		settings.Interval = time.Duration(10) * time.Second
+	}
+	if settings.SamplingCount <= 0 {
+		settings.SamplingCount = 10
+	}
+	if settings.Timeout <= 0 {
+		// results are saved after all health pings finish,
+		// a larger timeout could possibly makes checks run longer
+		settings.Timeout = time.Duration(5) * time.Second
+	}
+	return &HealthPing{
+		dispatcher: dispatcher,
+		Settings:   settings,
+		Results:    nil,
+	}
+}
+
+// StartScheduler implements the HealthChecker
+func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
+	if h.ticker != nil {
+		return
+	}
+	interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
+	ticker := time.NewTicker(interval)
+	h.ticker = ticker
+	go func() {
+		for {
+			go func() {
+				tags, err := selector()
+				if err != nil {
+					newError("error select outbounds for scheduled health check: ", err).AtWarning().WriteToLog()
+					return
+				}
+				h.doCheck(tags, interval, h.Settings.SamplingCount)
+				h.Cleanup(tags)
+			}()
+			_, ok := <-ticker.C
+			if !ok {
+				break
+			}
+		}
+	}()
+}
+
+// StopScheduler implements the HealthChecker
+func (h *HealthPing) StopScheduler() {
+	h.ticker.Stop()
+	h.ticker = nil
+}
+
+// Check implements the HealthChecker
+func (h *HealthPing) Check(tags []string) error {
+	if len(tags) == 0 {
+		return nil
+	}
+	newError("perform one-time health check for tags ", tags).AtInfo().WriteToLog()
+	h.doCheck(tags, 0, 1)
+	return nil
+}
+
+type rtt struct {
+	handler string
+	value   time.Duration
+}
+
+// doCheck performs the 'rounds' amount checks in given 'duration'. You should make
+// sure all tags are valid for current balancer
+func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) {
+	count := len(tags) * rounds
+	if count == 0 {
+		return
+	}
+	ch := make(chan *rtt, count)
+	// rtts := make(map[string][]time.Duration)
+	for _, tag := range tags {
+		handler := tag
+		client := newPingClient(
+			h.Settings.Destination,
+			h.Settings.Timeout,
+			handler,
+			h.dispatcher,
+		)
+		for i := 0; i < rounds; i++ {
+			delay := time.Duration(0)
+			if duration > 0 {
+				delay = time.Duration(dice.Roll(int(duration)))
+			}
+			time.AfterFunc(delay, func() {
+				newError("checking ", handler).AtDebug().WriteToLog()
+				delay, err := client.MeasureDelay()
+				if err == nil {
+					ch <- &rtt{
+						handler: handler,
+						value:   delay,
+					}
+					return
+				}
+				if !h.checkConnectivity() {
+					newError("network is down").AtWarning().WriteToLog()
+					ch <- &rtt{
+						handler: handler,
+						value:   0,
+					}
+					return
+				}
+				newError(fmt.Sprintf(
+					"error ping %s with %s: %s",
+					h.Settings.Destination,
+					handler,
+					err,
+				)).AtWarning().WriteToLog()
+				ch <- &rtt{
+					handler: handler,
+					value:   rttFailed,
+				}
+			})
+		}
+	}
+	for i := 0; i < count; i++ {
+		rtt := <-ch
+		if rtt.value > 0 {
+			// should not put results when network is down
+			h.PutResult(rtt.handler, rtt.value)
+		}
+	}
+}
+
+// PutResult put a ping rtt to results
+func (h *HealthPing) PutResult(tag string, rtt time.Duration) {
+	h.access.Lock()
+	defer h.access.Unlock()
+	if h.Results == nil {
+		h.Results = make(map[string]*HealthPingRTTS)
+	}
+	r, ok := h.Results[tag]
+	if !ok {
+		// validity is 2 times to sampling period, since the check are
+		// distributed in the time line randomly, in extreme cases,
+		// previous checks are distributed on the left, and latters
+		// on the right
+		validity := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) * 2
+		r = NewHealthPingResult(h.Settings.SamplingCount, validity)
+		h.Results[tag] = r
+	}
+	r.Put(rtt)
+}
+
+// Cleanup removes results of removed handlers,
+// tags should be all valid tags of the Balancer now
+func (h *HealthPing) Cleanup(tags []string) {
+	h.access.Lock()
+	defer h.access.Unlock()
+	for tag := range h.Results {
+		found := false
+		for _, v := range tags {
+			if tag == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			delete(h.Results, tag)
+		}
+	}
+}
+
+// checkConnectivity checks the network connectivity, it returns
+// true if network is good or "connectivity check url" not set
+func (h *HealthPing) checkConnectivity() bool {
+	if h.Settings.Connectivity == "" {
+		return true
+	}
+	tester := newDirectPingClient(
+		h.Settings.Connectivity,
+		h.Settings.Timeout,
+	)
+	if _, err := tester.MeasureDelay(); err != nil {
+		return false
+	}
+	return true
+}

+ 143 - 0
app/router/healthping_result.go

@@ -0,0 +1,143 @@
+package router
+
+import (
+	"math"
+	"time"
+)
+
+// HealthPingStats is the statistics of HealthPingRTTS
+type HealthPingStats struct {
+	All       int
+	Fail      int
+	Deviation time.Duration
+	Average   time.Duration
+	Max       time.Duration
+	Min       time.Duration
+}
+
+// HealthPingRTTS holds ping rtts for health Checker
+type HealthPingRTTS struct {
+	idx      int
+	cap      int
+	validity time.Duration
+	rtts     []*pingRTT
+
+	lastUpdateAt time.Time
+	stats        *HealthPingStats
+}
+
+type pingRTT struct {
+	time  time.Time
+	value time.Duration
+}
+
+// NewHealthPingResult returns a *HealthPingResult with specified capacity
+func NewHealthPingResult(cap int, validity time.Duration) *HealthPingRTTS {
+	return &HealthPingRTTS{cap: cap, validity: validity}
+}
+
+// Get gets statistics of the HealthPingRTTS
+func (h *HealthPingRTTS) Get() *HealthPingStats {
+	return h.getStatistics()
+}
+
+// GetWithCache get statistics and write cache for next call
+// Make sure use Mutex.Lock() before calling it, RWMutex.RLock()
+// is not an option since it writes cache
+func (h *HealthPingRTTS) GetWithCache() *HealthPingStats {
+	lastPutAt := h.rtts[h.idx].time
+	now := time.Now()
+	if h.stats == nil || h.lastUpdateAt.Before(lastPutAt) || h.findOutdated(now) >= 0 {
+		h.stats = h.getStatistics()
+		h.lastUpdateAt = now
+	}
+	return h.stats
+}
+
+// Put puts a new rtt to the HealthPingResult
+func (h *HealthPingRTTS) Put(d time.Duration) {
+	if h.rtts == nil {
+		h.rtts = make([]*pingRTT, h.cap)
+		for i := 0; i < h.cap; i++ {
+			h.rtts[i] = &pingRTT{}
+		}
+		h.idx = -1
+	}
+	h.idx = h.calcIndex(1)
+	now := time.Now()
+	h.rtts[h.idx].time = now
+	h.rtts[h.idx].value = d
+}
+
+func (h *HealthPingRTTS) calcIndex(step int) int {
+	idx := h.idx
+	idx += step
+	if idx >= h.cap {
+		idx %= h.cap
+	}
+	return idx
+}
+
+func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
+	stats := &HealthPingStats{}
+	stats.Fail = 0
+	stats.Max = 0
+	stats.Min = rttFailed
+	sum := time.Duration(0)
+	cnt := 0
+	validRTTs := make([]time.Duration, 0)
+	for _, rtt := range h.rtts {
+		switch {
+		case rtt.value == 0 || time.Since(rtt.time) > h.validity:
+			continue
+		case rtt.value == rttFailed:
+			stats.Fail++
+			continue
+		}
+		cnt++
+		sum += rtt.value
+		validRTTs = append(validRTTs, rtt.value)
+		if stats.Max < rtt.value {
+			stats.Max = rtt.value
+		}
+		if stats.Min > rtt.value {
+			stats.Min = rtt.value
+		}
+	}
+	stats.All = cnt + stats.Fail
+	if cnt == 0 {
+		stats.Min = 0
+		return stats
+	}
+	stats.Average = time.Duration(int(sum) / cnt)
+	var std float64
+	if cnt < 2 {
+		// no enough data for standard deviation, we assume it's half of the average rtt
+		// if we don't do this, standard deviation of 1 round tested nodes is 0, will always
+		// selected before 2 or more rounds tested nodes
+		std = float64(stats.Average / 2)
+	} else {
+		variance := float64(0)
+		for _, rtt := range validRTTs {
+			variance += math.Pow(float64(rtt-stats.Average), 2)
+		}
+		std = math.Sqrt(variance / float64(cnt))
+	}
+	stats.Deviation = time.Duration(std)
+	return stats
+}
+
+func (h *HealthPingRTTS) findOutdated(now time.Time) int {
+	for i := h.cap - 1; i < 2*h.cap; i++ {
+		// from oldest to latest
+		idx := h.calcIndex(i)
+		validity := h.rtts[idx].time.Add(h.validity)
+		if h.lastUpdateAt.After(validity) {
+			return idx
+		}
+		if validity.Before(now) {
+			return idx
+		}
+	}
+	return -1
+}

+ 106 - 0
app/router/healthping_result_test.go

@@ -0,0 +1,106 @@
+package router_test
+
+import (
+	"math"
+	reflect "reflect"
+	"testing"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+)
+
+func TestHealthPingResults(t *testing.T) {
+	rtts := []int64{60, 140, 60, 140, 60, 60, 140, 60, 140}
+	hr := router.NewHealthPingResult(4, time.Hour)
+	for _, rtt := range rtts {
+		hr.Put(time.Duration(rtt))
+	}
+	rttFailed := time.Duration(math.MaxInt64)
+	expected := &router.HealthPingStats{
+		All:       4,
+		Fail:      0,
+		Deviation: 40,
+		Average:   100,
+		Max:       140,
+		Min:       60,
+	}
+	actual := hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("expected: %v, actual: %v", expected, actual)
+	}
+	hr.Put(rttFailed)
+	hr.Put(rttFailed)
+	expected.Fail = 2
+	actual = hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("failed half-failures test, expected: %v, actual: %v", expected, actual)
+	}
+	hr.Put(rttFailed)
+	hr.Put(rttFailed)
+	expected = &router.HealthPingStats{
+		All:       4,
+		Fail:      4,
+		Deviation: 0,
+		Average:   0,
+		Max:       0,
+		Min:       0,
+	}
+	actual = hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("failed all-failures test, expected: %v, actual: %v", expected, actual)
+	}
+}
+
+func TestHealthPingResultsIgnoreOutdated(t *testing.T) {
+	rtts := []int64{60, 140, 60, 140}
+	hr := router.NewHealthPingResult(4, time.Duration(10)*time.Millisecond)
+	for i, rtt := range rtts {
+		if i == 2 {
+			// wait for previous 2 outdated
+			time.Sleep(time.Duration(10) * time.Millisecond)
+		}
+		hr.Put(time.Duration(rtt))
+	}
+	hr.Get()
+	expected := &router.HealthPingStats{
+		All:       2,
+		Fail:      0,
+		Deviation: 40,
+		Average:   100,
+		Max:       140,
+		Min:       60,
+	}
+	actual := hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("failed 'half-outdated' test, expected: %v, actual: %v", expected, actual)
+	}
+	// wait for all outdated
+	time.Sleep(time.Duration(10) * time.Millisecond)
+	expected = &router.HealthPingStats{
+		All:       0,
+		Fail:      0,
+		Deviation: 0,
+		Average:   0,
+		Max:       0,
+		Min:       0,
+	}
+	actual = hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("failed 'outdated / not-tested' test, expected: %v, actual: %v", expected, actual)
+	}
+
+	hr.Put(time.Duration(60))
+	expected = &router.HealthPingStats{
+		All:  1,
+		Fail: 0,
+		// 1 sample, std=0.5rtt
+		Deviation: 30,
+		Average:   60,
+		Max:       60,
+		Min:       60,
+	}
+	actual = hr.Get()
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("expected: %v, actual: %v", expected, actual)
+	}
+}

+ 81 - 0
app/router/ping.go

@@ -0,0 +1,81 @@
+package router
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/common/net"
+	"github.com/v2fly/v2ray-core/v4/common/session"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
+)
+
+type pingClient struct {
+	destination string
+	httpClient  *http.Client
+}
+
+func newPingClient(destination string, timeout time.Duration, handler string, dispatcher routing.Dispatcher) *pingClient {
+	return &pingClient{
+		destination: destination,
+		httpClient:  newHTTPClient(handler, dispatcher, timeout),
+	}
+}
+
+func newDirectPingClient(destination string, timeout time.Duration) *pingClient {
+	return &pingClient{
+		destination: destination,
+		httpClient:  &http.Client{Timeout: timeout},
+	}
+}
+
+func newHTTPClient(handler string, dispatcher routing.Dispatcher, timeout time.Duration) *http.Client {
+	tr := &http.Transport{
+		DisableKeepAlives: true,
+		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+			dest, err := net.ParseDestination(network + ":" + addr)
+			if err != nil {
+				return nil, err
+			}
+			h := &session.Handler{
+				Tag: handler,
+			}
+			ctx = session.ContextWithHandler(ctx, h)
+			link, err := dispatcher.Dispatch(ctx, dest)
+			if err != nil {
+				return nil, err
+			}
+			return net.NewConnection(
+				net.ConnectionInputMulti(link.Writer),
+				net.ConnectionOutputMulti(link.Reader),
+			), nil
+		},
+	}
+	return &http.Client{
+		Transport: tr,
+		Timeout:   timeout,
+		// don't follow redirect
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+}
+
+// MeasureDelay returns the delay time of the request to dest
+func (s *pingClient) MeasureDelay() (time.Duration, error) {
+	if s.httpClient == nil {
+		panic("pingClient no initialized")
+	}
+	req, err := http.NewRequest(http.MethodHead, s.destination, nil)
+	if err != nil {
+		return rttFailed, err
+	}
+	start := time.Now()
+	resp, err := s.httpClient.Do(req)
+	if err != nil {
+		return rttFailed, err
+	}
+	// don't wait for body
+	resp.Body.Close()
+	return time.Since(start), nil
+}

+ 20 - 6
app/router/router.go

@@ -29,13 +29,13 @@ type Route struct {
 }
 
 // Init initializes the Router.
-func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager) error {
+func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) 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)
+		balancer, err := rule.Build(ohm, dispatcher)
 		if err != nil {
 			return err
 		}
@@ -113,12 +113,26 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
 }
 
 // Start implements common.Runnable.
-func (*Router) Start() error {
+func (r *Router) Start() error {
+	for _, b := range r.balancers {
+		checker, ok := b.strategy.(routing.HealthChecker)
+		if !ok {
+			continue
+		}
+		checker.StartScheduler(b.SelectOutbounds)
+	}
 	return nil
 }
 
 // Close implements common.Closable.
-func (*Router) Close() error {
+func (r *Router) Close() error {
+	for _, b := range r.balancers {
+		checker, ok := b.strategy.(routing.HealthChecker)
+		if !ok {
+			continue
+		}
+		checker.StopScheduler()
+	}
 	return nil
 }
 
@@ -140,8 +154,8 @@ func (r *Route) GetOutboundTag() string {
 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(ctx, config.(*Config), d, ohm)
+		if err := core.RequireFeatures(ctx, func(d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error {
+			return r.Init(ctx, config.(*Config), d, ohm, dispatcher)
 		}); err != nil {
 			return nil, err
 		}

+ 118 - 0
app/router/router_health.go

@@ -0,0 +1,118 @@
+package router
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/v2fly/v2ray-core/v4/features/routing"
+)
+
+// CheckHanlders implements routing.RouterChecker.
+func (r *Router) CheckHanlders(tags []string) error {
+	errs := make([]error, 0)
+	for _, b := range r.balancers {
+		checker, ok := b.strategy.(routing.HealthChecker)
+		if !ok {
+			continue
+		}
+		all, err := b.SelectOutbounds()
+		if err != nil {
+			return err
+		}
+		ts := getCheckTags(tags, all)
+		err = checker.Check(ts)
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+	if len(errs) == 0 {
+		return nil
+	}
+	return getCollectError(errs)
+}
+
+func getCheckTags(tags, all []string) []string {
+	ts := make([]string, 0)
+	for _, t := range tags {
+		if findSliceIndex(all, t) >= 0 && findSliceIndex(ts, t) < 0 {
+			ts = append(ts, t)
+		}
+	}
+	return ts
+}
+
+// CheckBalancers implements routing.RouterChecker.
+func (r *Router) CheckBalancers(tags []string) error {
+	errs := make([]error, 0)
+	for t, b := range r.balancers {
+		if len(tags) > 0 && findSliceIndex(tags, t) < 0 {
+			continue
+		}
+		checker, ok := b.strategy.(routing.HealthChecker)
+		if !ok {
+			continue
+		}
+		tags, err := b.SelectOutbounds()
+		if err != nil {
+			errs = append(errs, err)
+		}
+		err = checker.Check(tags)
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+	if len(errs) == 0 {
+		return nil
+	}
+	return getCollectError(errs)
+}
+
+func getCollectError(errs []error) error {
+	sb := new(strings.Builder)
+	sb.WriteString("collect errors:\n")
+	for _, err := range errs {
+		sb.WriteString("    * ")
+		sb.WriteString(err.Error())
+		sb.WriteString("\n")
+	}
+	return errors.New(sb.String())
+}
+
+// GetBalancersInfo implements routing.RouterChecker.
+func (r *Router) GetBalancersInfo(tags []string) (resp []*routing.BalancerInfo, err error) {
+	resp = make([]*routing.BalancerInfo, 0)
+	for t, b := range r.balancers {
+		if len(tags) > 0 && findSliceIndex(tags, t) < 0 {
+			continue
+		}
+		all, err := b.SelectOutbounds()
+		if err != nil {
+			return nil, err
+		}
+		var override *routing.BalancingOverrideInfo
+		if o := b.override.Get(); o != nil {
+			override = &routing.BalancingOverrideInfo{
+				Until:   o.until,
+				Selects: o.selects,
+			}
+		}
+		stat := &routing.BalancerInfo{
+			Tag:      t,
+			Override: override,
+			Strategy: b.strategy.GetInformation(all),
+		}
+		resp = append(resp, stat)
+	}
+	return resp, nil
+}
+
+func findSliceIndex(slice []string, find string) int {
+	index := -1
+	for i, v := range slice {
+		if find == v {
+			index = i
+			break
+		}
+	}
+	return index
+}

+ 52 - 5
app/router/router_test.go

@@ -9,6 +9,7 @@ import (
 	. "github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/common/net"
+	serial "github.com/v2fly/v2ray-core/v4/common/serial"
 	"github.com/v2fly/v2ray-core/v4/common/session"
 	"github.com/v2fly/v2ray-core/v4/features/outbound"
 	routing_session "github.com/v2fly/v2ray-core/v4/features/routing/session"
@@ -43,7 +44,7 @@ func TestSimpleRouter(t *testing.T) {
 	common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{
 		Manager:         mockOhm,
 		HandlerSelector: mockHs,
-	}))
+	}, nil))
 
 	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)})
 	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
@@ -84,7 +85,7 @@ func TestSimpleBalancer(t *testing.T) {
 	common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{
 		Manager:         mockOhm,
 		HandlerSelector: mockHs,
-	}))
+	}, nil))
 
 	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)})
 	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
@@ -94,6 +95,52 @@ func TestSimpleBalancer(t *testing.T) {
 	}
 }
 
+func TestLeastLoadBalancer(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-"},
+				Strategy:         "leastLoad",
+				StrategySettings: serial.ToTypedMessage(&StrategyLeastLoadConfig{
+					HealthCheck: nil,
+					Baselines:   nil,
+					Expected:    1,
+				}),
+			},
+		},
+	}
+
+	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{"test1"})
+
+	r := new(Router)
+	common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{
+		Manager:         mockOhm,
+		HandlerSelector: mockHs,
+	}, nil))
+	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)})
+	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
+	common.Must(err)
+	if tag := route.GetOutboundTag(); tag != "test1" {
+		t.Error("expect tag 'test1', bug actually ", tag)
+	}
+}
+
 func TestIPOnDemand(t *testing.T) {
 	config := &Config{
 		DomainStrategy: Config_IpOnDemand,
@@ -119,7 +166,7 @@ func TestIPOnDemand(t *testing.T) {
 	mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
-	common.Must(r.Init(context.TODO(), config, mockDNS, nil))
+	common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))
 
 	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)})
 	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
@@ -154,7 +201,7 @@ func TestIPIfNonMatchDomain(t *testing.T) {
 	mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
 
 	r := new(Router)
-	common.Must(r.Init(context.TODO(), config, mockDNS, nil))
+	common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))
 
 	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2fly.org"), 80)})
 	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))
@@ -188,7 +235,7 @@ func TestIPIfNonMatchIP(t *testing.T) {
 	mockDNS := mocks.NewDNSClient(mockCtl)
 
 	r := new(Router)
-	common.Must(r.Init(context.TODO(), config, mockDNS, nil))
+	common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))
 
 	ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 80)})
 	route, err := r.PickRoute(routing_session.AsRoutingContext(ctx))

+ 322 - 0
app/router/strategy_leastload.go

@@ -0,0 +1,322 @@
+package router
+
+import (
+	"fmt"
+	"math"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/common/dice"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
+)
+
+const (
+	rttFailed = time.Duration(math.MaxInt64 - iota)
+	rttUntested
+	rttUnqualified
+)
+
+// LeastLoadStrategy represents a random balancing strategy
+type LeastLoadStrategy struct {
+	*HealthPing
+
+	settings *StrategyLeastLoadConfig
+	costs    *WeightManager
+}
+
+// NewLeastLoadStrategy creates a new LeastLoadStrategy with settings
+func NewLeastLoadStrategy(settings *StrategyLeastLoadConfig, dispatcher routing.Dispatcher) *LeastLoadStrategy {
+	return &LeastLoadStrategy{
+		HealthPing: NewHealthPing(settings.HealthCheck, dispatcher),
+		settings:   settings,
+		costs: NewWeightManager(
+			settings.Costs, 1,
+			func(value, cost float64) float64 {
+				return value * math.Pow(cost, 0.5)
+			},
+		),
+	}
+}
+
+// node is a minimal copy of HealthCheckResult
+// we don't use HealthCheckResult directly because
+// it may change by health checker during routing
+type node struct {
+	Tag              string
+	CountAll         int
+	CountFail        int
+	RTTAverage       time.Duration
+	RTTDeviation     time.Duration
+	RTTDeviationCost time.Duration
+
+	applied time.Duration
+}
+
+// GetInformation implements the routing.BalancingStrategy.
+func (s *LeastLoadStrategy) GetInformation(tags []string) *routing.StrategyInfo {
+	qualified, others := s.getNodes(tags, time.Duration(s.settings.MaxRTT))
+	selects := s.selectLeastLoad(qualified)
+	// append qualified but not selected outbounds to others
+	others = append(others, qualified[len(selects):]...)
+	leastloadSort(others)
+	titles, sl := s.getNodesInfo(selects)
+	_, ot := s.getNodesInfo(others)
+	return &routing.StrategyInfo{
+		Settings:    s.getSettings(),
+		ValueTitles: titles,
+		Selects:     sl,
+		Others:      ot,
+	}
+}
+
+// SelectAndPick implements the routing.BalancingStrategy.
+func (s *LeastLoadStrategy) SelectAndPick(candidates []string) string {
+	qualified, _ := s.getNodes(candidates, time.Duration(s.settings.MaxRTT))
+	selects := s.selectLeastLoad(qualified)
+	count := len(selects)
+	if count == 0 {
+		// goes to fallbackTag
+		return ""
+	}
+	return selects[dice.Roll(count)].Tag
+}
+
+// Pick implements the routing.BalancingStrategy.
+func (s *LeastLoadStrategy) Pick(candidates []string) string {
+	count := len(candidates)
+	if count == 0 {
+		// goes to fallbackTag
+		return ""
+	}
+	return candidates[dice.Roll(count)]
+}
+
+// selectLeastLoad selects nodes according to Baselines and Expected Count.
+//
+// The strategy always improves network response speed, not matter which mode below is configurated.
+// But they can still have different priorities.
+//
+// 1. Bandwidth priority: no Baseline + Expected Count > 0.: selects `Expected Count` of nodes.
+// (one if Expected Count <= 0)
+//
+// 2. Bandwidth priority advanced: Baselines + Expected Count > 0.
+// Select `Expected Count` amount of nodes, and also those near them according to baselines.
+// In other words, it selects according to different Baselines, until one of them matches
+// the Expected Count, if no Baseline matches, Expected Count applied.
+//
+// 3. Speed priority: Baselines + `Expected Count <= 0`.
+// go through all baselines until find selects, if not, select none. Used in combination
+// with 'balancer.fallbackTag', it means: selects qualified nodes or use the fallback.
+func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node {
+	if len(nodes) == 0 {
+		newError("least load: no qualified outbound").AtInfo().WriteToLog()
+		return nil
+	}
+	expected := int(s.settings.Expected)
+	availableCount := len(nodes)
+	if expected > availableCount {
+		return nodes
+	}
+
+	if expected <= 0 {
+		expected = 1
+	}
+	if len(s.settings.Baselines) == 0 {
+		return nodes[:expected]
+	}
+
+	count := 0
+	// go through all base line until find expected selects
+	for _, b := range s.settings.Baselines {
+		baseline := time.Duration(b)
+		for i := 0; i < availableCount; i++ {
+			if nodes[i].applied > baseline {
+				break
+			}
+			count = i + 1
+		}
+		// don't continue if find expected selects
+		if count >= expected {
+			newError("applied baseline: ", baseline).AtDebug().WriteToLog()
+			break
+		}
+	}
+	if s.settings.Expected > 0 && count < expected {
+		count = expected
+	}
+	return nodes[:count]
+}
+
+func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) ([]*node, []*node) {
+	s.access.Lock()
+	defer s.access.Unlock()
+	results := s.Results
+	qualified := make([]*node, 0)
+	unqualified := make([]*node, 0)
+	failed := make([]*node, 0)
+	untested := make([]*node, 0)
+	others := make([]*node, 0)
+	for _, tag := range candidates {
+		r, ok := results[tag]
+		if !ok {
+			untested = append(untested, &node{
+				Tag:              tag,
+				RTTDeviationCost: 0,
+				RTTDeviation:     0,
+				RTTAverage:       0,
+				applied:          rttUntested,
+			})
+			continue
+		}
+		stats := r.Get()
+		node := &node{
+			Tag:              tag,
+			RTTDeviationCost: time.Duration(s.costs.Apply(tag, float64(stats.Deviation))),
+			RTTDeviation:     stats.Deviation,
+			RTTAverage:       stats.Average,
+			CountAll:         stats.All,
+			CountFail:        stats.Fail,
+		}
+		switch {
+		case stats.All == 0:
+			node.applied = rttUntested
+			untested = append(untested, node)
+		case maxRTT > 0 && stats.Average > maxRTT:
+			node.applied = rttUnqualified
+			unqualified = append(unqualified, node)
+		case float64(stats.Fail)/float64(stats.All) > float64(s.settings.Tolerance):
+			node.applied = rttFailed
+			if stats.All-stats.Fail == 0 {
+				// no good, put them after has-good nodes
+				node.RTTDeviationCost = rttFailed
+				node.RTTDeviation = rttFailed
+				node.RTTAverage = rttFailed
+			}
+			failed = append(failed, node)
+		default:
+			node.applied = node.RTTDeviationCost
+			qualified = append(qualified, node)
+		}
+	}
+	if len(qualified) > 0 {
+		leastloadSort(qualified)
+		others = append(others, unqualified...)
+		others = append(others, untested...)
+		others = append(others, failed...)
+	} else {
+		qualified = untested
+		others = append(others, unqualified...)
+		others = append(others, failed...)
+	}
+	return qualified, others
+}
+
+func (s *LeastLoadStrategy) getSettings() []string {
+	settings := make([]string, 0)
+	sb := new(strings.Builder)
+	for i, b := range s.settings.Baselines {
+		if i > 0 {
+			sb.WriteByte(' ')
+		}
+		sb.WriteString(time.Duration(b).String())
+	}
+	baselines := sb.String()
+	if baselines == "" {
+		baselines = "none"
+	}
+	maxRTT := time.Duration(s.settings.MaxRTT).String()
+	if s.settings.MaxRTT == 0 {
+		maxRTT = "none"
+	}
+	settings = append(settings, fmt.Sprintf(
+		"leastload, expected: %d, baselines: %s, max rtt: %s, tolerance: %.2f",
+		s.settings.Expected,
+		baselines,
+		maxRTT,
+		s.settings.Tolerance,
+	))
+	settings = append(settings, fmt.Sprintf(
+		"health ping, interval: %s, sampling: %d, timeout: %s, destination: %s",
+		s.HealthPing.Settings.Interval,
+		s.HealthPing.Settings.SamplingCount,
+		s.HealthPing.Settings.Timeout,
+		s.HealthPing.Settings.Destination,
+	))
+	return settings
+}
+
+func (s *LeastLoadStrategy) getNodesInfo(nodes []*node) ([]string, []*routing.OutboundInfo) {
+	titles := []string{"   ", "RTT STD+C    ", "RTT STD.     ", "RTT Avg.     ", "Hit  ", "Cost "}
+	hasCost := len(s.settings.Costs) > 0
+	if !hasCost {
+		titles = []string{"   ", "RTT STD.     ", "RTT Avg.     ", "Hit  "}
+	}
+	items := make([]*routing.OutboundInfo, 0)
+	for _, node := range nodes {
+		item := &routing.OutboundInfo{
+			Tag: node.Tag,
+		}
+		var status string
+		cost := fmt.Sprintf("%.2f", s.costs.Get(node.Tag))
+		switch node.applied {
+		case rttFailed:
+			status = "x"
+		case rttUntested:
+			status = "?"
+		case rttUnqualified:
+			status = ">"
+		default:
+			status = "OK"
+		}
+		if hasCost {
+			item.Values = []string{
+				status,
+				durationString(node.RTTDeviationCost),
+				durationString(node.RTTDeviation),
+				durationString(node.RTTAverage),
+				fmt.Sprintf("%d/%d", node.CountAll-node.CountFail, node.CountAll),
+				cost,
+			}
+		} else {
+			item.Values = []string{
+				status,
+				durationString(node.RTTDeviation),
+				durationString(node.RTTAverage),
+				fmt.Sprintf("%d/%d", node.CountAll-node.CountFail, node.CountAll),
+			}
+		}
+		items = append(items, item)
+	}
+	return titles, items
+}
+
+func durationString(d time.Duration) string {
+	if d <= 0 || d > time.Hour {
+		return "-"
+	}
+	return d.String()
+}
+
+func leastloadSort(nodes []*node) {
+	sort.Slice(nodes, func(i, j int) bool {
+		left := nodes[i]
+		right := nodes[j]
+		if left.applied != right.applied {
+			return left.applied < right.applied
+		}
+		if left.RTTDeviationCost != right.RTTDeviationCost {
+			return left.RTTDeviationCost < right.RTTDeviationCost
+		}
+		if left.RTTAverage != right.RTTAverage {
+			return left.RTTAverage < right.RTTAverage
+		}
+		if left.CountFail != right.CountFail {
+			return left.CountFail < right.CountFail
+		}
+		if left.CountAll != right.CountAll {
+			return left.CountAll > right.CountAll
+		}
+		return left.Tag < right.Tag
+	})
+}

+ 177 - 0
app/router/strategy_leastload_test.go

@@ -0,0 +1,177 @@
+package router
+
+import (
+	"testing"
+	"time"
+)
+
+func TestSelectLeastLoad(t *testing.T) {
+	settings := &StrategyLeastLoadConfig{
+		HealthCheck: &HealthPingConfig{
+			SamplingCount: 10,
+		},
+		Expected: 1,
+		MaxRTT:   int64(time.Millisecond * time.Duration(800)),
+	}
+	strategy := NewLeastLoadStrategy(settings, nil)
+	// std 40
+	strategy.PutResult("a", time.Millisecond*time.Duration(60))
+	strategy.PutResult("a", time.Millisecond*time.Duration(140))
+	strategy.PutResult("a", time.Millisecond*time.Duration(60))
+	strategy.PutResult("a", time.Millisecond*time.Duration(140))
+	// std 60
+	strategy.PutResult("b", time.Millisecond*time.Duration(40))
+	strategy.PutResult("b", time.Millisecond*time.Duration(160))
+	strategy.PutResult("b", time.Millisecond*time.Duration(40))
+	strategy.PutResult("b", time.Millisecond*time.Duration(160))
+	// std 0, but >MaxRTT
+	strategy.PutResult("c", time.Millisecond*time.Duration(1000))
+	strategy.PutResult("c", time.Millisecond*time.Duration(1000))
+	strategy.PutResult("c", time.Millisecond*time.Duration(1000))
+	strategy.PutResult("c", time.Millisecond*time.Duration(1000))
+	expected := "a"
+	actual := strategy.SelectAndPick([]string{"a", "b", "c", "untested"})
+	if actual != expected {
+		t.Errorf("expected: %v, actual: %v", expected, actual)
+	}
+}
+
+func TestSelectLeastLoadWithCost(t *testing.T) {
+	settings := &StrategyLeastLoadConfig{
+		HealthCheck: &HealthPingConfig{
+			SamplingCount: 10,
+		},
+		Costs: []*StrategyWeight{
+			{Match: "a", Value: 9},
+		},
+		Expected: 1,
+	}
+	strategy := NewLeastLoadStrategy(settings, nil)
+	// std 40, std+c 120
+	strategy.PutResult("a", time.Millisecond*time.Duration(60))
+	strategy.PutResult("a", time.Millisecond*time.Duration(140))
+	strategy.PutResult("a", time.Millisecond*time.Duration(60))
+	strategy.PutResult("a", time.Millisecond*time.Duration(140))
+	// std 60
+	strategy.PutResult("b", time.Millisecond*time.Duration(40))
+	strategy.PutResult("b", time.Millisecond*time.Duration(160))
+	strategy.PutResult("b", time.Millisecond*time.Duration(40))
+	strategy.PutResult("b", time.Millisecond*time.Duration(160))
+	expected := "b"
+	actual := strategy.SelectAndPick([]string{"a", "b", "untested"})
+	if actual != expected {
+		t.Errorf("expected: %v, actual: %v", expected, actual)
+	}
+}
+
+func TestSelectLeastExpected(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: nil,
+			Expected:  3,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 100},
+		{Tag: "b", applied: 200},
+		{Tag: "c", applied: 300},
+		{Tag: "d", applied: 350},
+	}
+	expected := 3
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}
+func TestSelectLeastExpected2(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: nil,
+			Expected:  3,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 100},
+		{Tag: "b", applied: 200},
+	}
+	expected := 2
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}
+func TestSelectLeastExpectedAndBaselines(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: []int64{200, 300, 400},
+			Expected:  3,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 100},
+		{Tag: "b", applied: 200},
+		{Tag: "c", applied: 250},
+		{Tag: "d", applied: 300},
+		{Tag: "e", applied: 310},
+	}
+	expected := 4
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}
+func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: []int64{200, 300, 400},
+			Expected:  3,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 500},
+		{Tag: "b", applied: 600},
+		{Tag: "c", applied: 700},
+		{Tag: "d", applied: 800},
+		{Tag: "e", applied: 900},
+	}
+	expected := 3
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}
+func TestSelectLeastLoadBaselines(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: []int64{200, 400, 600},
+			Expected:  0,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 100},
+		{Tag: "b", applied: 200},
+		{Tag: "c", applied: 300},
+	}
+	expected := 2
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}
+func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) {
+	strategy := &LeastLoadStrategy{
+		settings: &StrategyLeastLoadConfig{
+			Baselines: []int64{200, 400, 600},
+			Expected:  0,
+		},
+	}
+	nodes := []*node{
+		{Tag: "a", applied: 800},
+		{Tag: "b", applied: 1000},
+	}
+	expected := 0
+	ns := strategy.selectLeastLoad(nodes)
+	if len(ns) != expected {
+		t.Errorf("expected: %v, actual: %v", expected, len(ns))
+	}
+}

+ 15 - 0
app/router/strategy_leastping.go

@@ -5,6 +5,7 @@ package router
 
 import (
 	"context"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
 
 	core "github.com/v2fly/v2ray-core/v4"
 	"github.com/v2fly/v2ray-core/v4/app/observatory"
@@ -17,6 +18,20 @@ type LeastPingStrategy struct {
 	observatory extension.Observatory
 }
 
+// TODO Fix PlaceHolder
+
+func (l *LeastPingStrategy) Pick(candidates []string) string {
+	panic("implement me")
+}
+
+func (l *LeastPingStrategy) SelectAndPick(candidates []string) string {
+	panic("implement me")
+}
+
+func (l *LeastPingStrategy) GetInformation(tags []string) *routing.StrategyInfo {
+	panic("implement me")
+}
+
 func (l *LeastPingStrategy) InjectContext(ctx context.Context) {
 	l.ctx = ctx
 }

+ 38 - 0
app/router/strategy_random.go

@@ -0,0 +1,38 @@
+package router
+
+import (
+	"github.com/v2fly/v2ray-core/v4/common/dice"
+	"github.com/v2fly/v2ray-core/v4/features/routing"
+)
+
+// RandomStrategy represents a random balancing strategy
+type RandomStrategy struct{}
+
+// GetInformation implements the routing.BalancingStrategy.
+func (s *RandomStrategy) GetInformation(tags []string) *routing.StrategyInfo {
+	items := make([]*routing.OutboundInfo, 0)
+	for _, tag := range tags {
+		items = append(items, &routing.OutboundInfo{Tag: tag})
+	}
+	return &routing.StrategyInfo{
+		Settings:    []string{"random"},
+		ValueTitles: nil,
+		Selects:     items,
+		Others:      nil,
+	}
+}
+
+// SelectAndPick implements the routing.BalancingStrategy.
+func (s *RandomStrategy) SelectAndPick(candidates []string) string {
+	return s.Pick(candidates)
+}
+
+// Pick implements the routing.BalancingStrategy.
+func (s *RandomStrategy) Pick(candidates []string) string {
+	count := len(candidates)
+	if count == 0 {
+		// goes to fallbackTag
+		return ""
+	}
+	return candidates[dice.Roll(count)]
+}

+ 85 - 0
app/router/weight.go

@@ -0,0 +1,85 @@
+package router
+
+import (
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+type weightScaler func(value, weight float64) float64
+
+var numberFinder = regexp.MustCompile(`\d+(\.\d+)?`)
+
+// NewWeightManager creates a new WeightManager with settings
+func NewWeightManager(s []*StrategyWeight, defaultWeight float64, scaler weightScaler) *WeightManager {
+	return &WeightManager{
+		settings:      s,
+		cache:         make(map[string]float64),
+		scaler:        scaler,
+		defaultWeight: defaultWeight,
+	}
+}
+
+// WeightManager manages weights for specific settings
+type WeightManager struct {
+	settings      []*StrategyWeight
+	cache         map[string]float64
+	scaler        weightScaler
+	defaultWeight float64
+}
+
+// Get get the weight of specified tag
+func (s *WeightManager) Get(tag string) float64 {
+	weight, ok := s.cache[tag]
+	if ok {
+		return weight
+	}
+	weight = s.findValue(tag)
+	s.cache[tag] = weight
+	return weight
+}
+
+// Apply applies weight to the value
+func (s *WeightManager) Apply(tag string, value float64) float64 {
+	return s.scaler(value, s.Get(tag))
+}
+
+func (s *WeightManager) findValue(tag string) float64 {
+	for _, w := range s.settings {
+		matched := s.getMatch(tag, w.Match, w.Regexp)
+		if matched == "" {
+			continue
+		}
+		if w.Value > 0 {
+			return float64(w.Value)
+		}
+		// auto weight from matched
+		numStr := numberFinder.FindString(matched)
+		if numStr == "" {
+			return s.defaultWeight
+		}
+		weight, err := strconv.ParseFloat(numStr, 64)
+		if err != nil {
+			newError("unexpected error from ParseFloat: ", err).AtError().WriteToLog()
+			return s.defaultWeight
+		}
+		return weight
+	}
+	return s.defaultWeight
+}
+
+func (s *WeightManager) getMatch(tag, find string, isRegexp bool) string {
+	if !isRegexp {
+		idx := strings.Index(tag, find)
+		if idx < 0 {
+			return ""
+		}
+		return find
+	}
+	r, err := regexp.Compile(find)
+	if err != nil {
+		newError("invalid regexp: ", find, "err: ", err).AtError().WriteToLog()
+		return ""
+	}
+	return r.FindString(tag)
+}

+ 60 - 0
app/router/weight_test.go

@@ -0,0 +1,60 @@
+package router_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+)
+
+func TestWeight(t *testing.T) {
+	manager := router.NewWeightManager(
+		[]*router.StrategyWeight{
+			{
+				Match: "x5",
+				Value: 100,
+			},
+			{
+				Match: "x8",
+			},
+			{
+				Regexp: true,
+				Match:  `\bx0+(\.\d+)?\b`,
+				Value:  1,
+			},
+			{
+				Regexp: true,
+				Match:  `\bx\d+(\.\d+)?\b`,
+			},
+		},
+		1, func(v, w float64) float64 {
+			return v * w
+		},
+	)
+	tags := []string{
+		"node name, x5, and more",
+		"node name, x8",
+		"node name, x15",
+		"node name, x0100, and more",
+		"node name, x10.1",
+		"node name, x00.1, and more",
+	}
+	// test weight
+	expected := []float64{100, 8, 15, 100, 10.1, 1}
+	actual := make([]float64, 0)
+	for _, tag := range tags {
+		actual = append(actual, manager.Get(tag))
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("expected: %v, actual: %v", expected, actual)
+	}
+	// test scale
+	expected2 := []float64{1000, 80, 150, 1000, 101, 10}
+	actual2 := make([]float64, 0)
+	for _, tag := range tags {
+		actual2 = append(actual2, manager.Apply(tag, 10))
+	}
+	if !reflect.DeepEqual(expected2, actual2) {
+		t.Errorf("expected2: %v, actual2: %v", expected2, actual2)
+	}
+}

+ 14 - 0
common/session/context.go

@@ -14,6 +14,7 @@ const (
 	muxPreferedSessionKey
 	sockoptSessionKey
 	trackedConnectionErrorKey
+	handlerSessionKey
 )
 
 // ContextWithID returns a new context with the given ID.
@@ -132,3 +133,16 @@ func SubmitOutboundErrorToOriginator(ctx context.Context, err error) {
 func TrackedConnectionError(ctx context.Context, tracker TrackedRequestErrorFeedback) context.Context {
 	return context.WithValue(ctx, trackedConnectionErrorKey, tracker)
 }
+
+// ContextWithHandler returns a new context with handler
+func ContextWithHandler(ctx context.Context, handler *Handler) context.Context {
+	return context.WithValue(ctx, handlerSessionKey, handler)
+}
+
+// HandlerFromContext returns handler config in this context, or nil if not
+func HandlerFromContext(ctx context.Context) *Handler {
+	if handler, ok := ctx.Value(handlerSessionKey).(*Handler); ok {
+		return handler
+	}
+	return nil
+}

+ 6 - 0
common/session/session.go

@@ -78,6 +78,12 @@ type Sockopt struct {
 	Mark int32
 }
 
+// Handler is the handler setting for dispatching.
+type Handler struct {
+	// Tag of outbound handler.
+	Tag string
+}
+
 // SetAttribute attachs additional string attributes to content.
 func (c *Content) SetAttribute(name string, value string) {
 	if c.Attributes == nil {

+ 51 - 0
features/routing/health.go

@@ -0,0 +1,51 @@
+package routing
+
+import "time"
+
+// HealthChecker is the interface for health checkers
+type HealthChecker interface {
+	// StartScheduler starts the check scheduler
+	StartScheduler(selector func() ([]string, error))
+	// StopScheduler stops the check scheduler
+	StopScheduler()
+	// Check start the health checking for given tags.
+	Check(tags []string) error
+}
+
+// OutboundInfo holds information of an outbound
+type OutboundInfo struct {
+	Tag    string   // Tag of the outbound
+	Values []string // Information of the outbound, which can be different between strategies, like health ping RTT
+}
+
+// StrategyInfo holds strategy running information, like selected handlers and others
+type StrategyInfo struct {
+	Settings    []string        // Strategy settings
+	ValueTitles []string        // Value titles of OutboundInfo.Values
+	Selects     []*OutboundInfo // Selects of the strategy
+	Others      []*OutboundInfo // Other outbounds
+}
+
+// BalancingOverrideInfo holds balancing overridden information
+type BalancingOverrideInfo struct {
+	Until   time.Time
+	Selects []string
+}
+
+// BalancerInfo holds information of a balancer
+type BalancerInfo struct {
+	Tag      string // Tag of the balancer
+	Override *BalancingOverrideInfo
+	Strategy *StrategyInfo // Strategy and its running information
+}
+
+// RouterChecker represents a router that is able to perform checks for its balancers, and get statistics.
+type RouterChecker interface {
+	// CheckHanlders performs a health check for specified outbound hanlders.
+	CheckHanlders(tags []string) error
+	// CheckBalancers performs health checks for specified balancers,
+	// if not specified, check them all.
+	CheckBalancers(tags []string) error
+	// GetBalancersInfo get health info of specific balancer, if balancer not specified, get all
+	GetBalancersInfo(tags []string) ([]*BalancerInfo, error)
+}

+ 22 - 0
features/routing/strategy.go

@@ -0,0 +1,22 @@
+package routing
+
+import "time"
+
+// BalancingStrategy is the interface for balancing strategies
+type BalancingStrategy interface {
+	// Pick pick one outbound from candidates. Unlike the SelectAndPick(),
+	// it skips the select procedure (select all & pick one).
+	Pick(candidates []string) string
+	// SelectAndPick selects qualified nodes from candidates then pick one.
+	SelectAndPick(candidates []string) string
+	// GetInformation gets information of the strategy
+	GetInformation(tags []string) *StrategyInfo
+}
+
+// BalancingOverrider is the interface of those who can override
+// the selecting of its balancers
+type BalancingOverrider interface {
+	// OverrideSelecting overrides the selects of specified balancer, for 'validity'
+	// duration of time.
+	OverrideSelecting(balancer string, selects []string, validity time.Duration) error
+}

+ 2 - 0
go.sum

@@ -238,6 +238,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/pires/go-proxyproto v0.6.0 h1:cLJUPnuQdiNf7P/wbeOKmM1khVdaMgTFDLj8h9ZrVYk=

+ 3 - 0
infra/conf/api.go

@@ -10,6 +10,7 @@ import (
 	loggerservice "github.com/v2fly/v2ray-core/v4/app/log/command"
 	observatoryservice "github.com/v2fly/v2ray-core/v4/app/observatory/command"
 	handlerservice "github.com/v2fly/v2ray-core/v4/app/proxyman/command"
+	routerservice "github.com/v2fly/v2ray-core/v4/app/router/command"
 	statsservice "github.com/v2fly/v2ray-core/v4/app/stats/command"
 	"github.com/v2fly/v2ray-core/v4/common/serial"
 )
@@ -37,6 +38,8 @@ func (c *APIConfig) Build() (*commander.Config, error) {
 			services = append(services, serial.ToTypedMessage(&statsservice.Config{}))
 		case "observatoryservice":
 			services = append(services, serial.ToTypedMessage(&observatoryservice.Config{}))
+		case "routingservice":
+			services = append(services, serial.ToTypedMessage(&routerservice.Config{}))
 		default:
 			if !strings.HasPrefix(s, "#") {
 				continue

+ 30 - 5
infra/conf/router.go

@@ -3,10 +3,12 @@ package conf
 import (
 	"context"
 	"encoding/json"
+	"github.com/golang/protobuf/proto"
 	"strings"
 
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/common/platform"
+	"github.com/v2fly/v2ray-core/v4/common/serial"
 	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
 	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
 	rule2 "github.com/v2fly/v2ray-core/v4/infra/conf/rule"
@@ -24,11 +26,13 @@ type StrategyConfig struct {
 }
 
 type BalancingRule struct {
-	Tag       string               `json:"tag"`
-	Selectors cfgcommon.StringList `json:"selector"`
-	Strategy  StrategyConfig       `json:"strategy"`
+	Tag         string               `json:"tag"`
+	Selectors   cfgcommon.StringList `json:"selector"`
+	Strategy    StrategyConfig       `json:"strategy"`
+	FallbackTag string               `json:"fallbackTag"`
 }
 
+// Build builds the balancing rule
 func (r *BalancingRule) Build() (*router.BalancingRule, error) {
 	if r.Tag == "" {
 		return nil, newError("empty balancer tag")
@@ -40,17 +44,38 @@ func (r *BalancingRule) Build() (*router.BalancingRule, error) {
 	var strategy string
 	switch strings.ToLower(r.Strategy.Type) {
 	case strategyRandom, "":
+		r.Strategy.Type = strategyRandom
 		strategy = strategyRandom
+	case strategyLeastLoad:
+		strategy = strategyLeastLoad
 	case strategyLeastPing:
 		strategy = "leastPing"
 	default:
 		return nil, newError("unknown balancing strategy: " + r.Strategy.Type)
 	}
 
+	settings := []byte("{}")
+	if r.Strategy.Settings != nil {
+		settings = ([]byte)(*r.Strategy.Settings)
+	}
+	rawConfig, err := strategyConfigLoader.LoadWithID(settings, r.Strategy.Type)
+	if err != nil {
+		return nil, newError("failed to parse to strategy config.").Base(err)
+	}
+	var ts proto.Message
+	if builder, ok := rawConfig.(Buildable); ok {
+		ts, err = builder.Build()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return &router.BalancingRule{
-		Tag:              r.Tag,
-		OutboundSelector: []string(r.Selectors),
 		Strategy:         strategy,
+		StrategySettings: serial.ToTypedMessage(ts),
+		FallbackTag:      r.FallbackTag,
+		OutboundSelector: r.Selectors,
+		Tag:              r.Tag,
 	}, nil
 }
 

+ 79 - 0
infra/conf/router_strategy.go

@@ -1,6 +1,85 @@
 package conf
 
+import (
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/v2fly/v2ray-core/v4/app/router"
+)
+
 const (
 	strategyRandom    string = "random"
+	strategyLeastLoad string = "leastload"
 	strategyLeastPing string = "leastping"
 )
+
+var (
+	strategyConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
+		strategyRandom:    func() interface{} { return new(strategyEmptyConfig) },
+		strategyLeastLoad: func() interface{} { return new(strategyLeastLoadConfig) },
+	}, "type", "settings")
+)
+
+type strategyEmptyConfig struct {
+}
+
+func (v *strategyEmptyConfig) Build() (proto.Message, error) {
+	return nil, nil
+}
+
+type strategyLeastLoadConfig struct {
+	// note the time values of the HealthCheck holds is not
+	// 'time.Duration' but plain number, sice they were parsed
+	// directly from json
+	HealthCheck *router.HealthPingSettings `json:"healthCheck,omitempty"`
+	// weight settings
+	Costs []*router.StrategyWeight `json:"costs,omitempty"`
+	// ping rtt baselines (ms)
+	Baselines []int `json:"baselines,omitempty"`
+	// expected nodes count to select
+	Expected int32 `json:"expected,omitempty"`
+	// max acceptable rtt (ms), filter away high delay nodes. defalut 0
+	MaxRTT int `json:"maxRTT,omitempty"`
+	// acceptable failure rate
+	Tolerance float64 `json:"tolerance,omitempty"`
+}
+
+// Build implements Buildable.
+func (v *strategyLeastLoadConfig) Build() (proto.Message, error) {
+	config := &router.StrategyLeastLoadConfig{
+		HealthCheck: &router.HealthPingConfig{},
+	}
+	if v.HealthCheck != nil {
+		config.HealthCheck = &router.HealthPingConfig{
+			Destination:   v.HealthCheck.Destination,
+			Connectivity:  v.HealthCheck.Connectivity,
+			Interval:      int64(v.HealthCheck.Interval * time.Second),
+			Timeout:       int64(v.HealthCheck.Timeout * time.Second),
+			SamplingCount: int32(v.HealthCheck.SamplingCount),
+		}
+	}
+	config.Costs = v.Costs
+	config.Tolerance = float32(v.Tolerance)
+	if config.Tolerance < 0 {
+		config.Tolerance = 0
+	}
+	if config.Tolerance > 1 {
+		config.Tolerance = 1
+	}
+	config.Expected = v.Expected
+	if config.Expected < 0 {
+		config.Expected = 0
+	}
+	config.MaxRTT = int64(time.Duration(v.MaxRTT) * time.Millisecond)
+	if config.MaxRTT < 0 {
+		config.MaxRTT = 0
+	}
+	config.Baselines = make([]int64, 0)
+	for _, b := range v.Baselines {
+		if b <= 0 {
+			continue
+		}
+		config.Baselines = append(config.Baselines, int64(time.Duration(b)*time.Millisecond))
+	}
+	return config, nil
+}

+ 59 - 0
infra/conf/router_test.go

@@ -3,12 +3,14 @@ package conf_test
 import (
 	"encoding/json"
 	"testing"
+	"time"
 	_ "unsafe"
 
 	"github.com/golang/protobuf/proto"
 
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/common/net"
+	"github.com/v2fly/v2ray-core/v4/common/serial"
 	. "github.com/v2fly/v2ray-core/v4/infra/conf"
 )
 
@@ -68,6 +70,34 @@ func TestRouterConfig(t *testing.T) {
 					{
 						"tag": "b1",
 						"selector": ["test"]
+					},
+					{
+						"tag": "b2",
+						"selector": ["test"],
+						"strategy": {
+							"type": "leastload",
+							"settings": {
+								"healthCheck": {
+									"interval": 300,
+									"sampling": 2,
+									"timeout": 3,
+									"destination": "dest",
+									"connectivity": "conn"
+								},
+								"costs": [
+									{
+										"regexp": true,
+										"match": "\\d+(\\.\\d+)",
+										"value": 5
+									}
+								],
+								"baselines": [400, 600],
+								"expected": 6,
+								"maxRTT": 1000,
+								"tolerance": 0.5
+							}
+						},
+						"fallbackTag": "fall"
 					}
 				]
 			}`,
@@ -80,6 +110,35 @@ func TestRouterConfig(t *testing.T) {
 						OutboundSelector: []string{"test"},
 						Strategy:         "random",
 					},
+					{
+						Tag:              "b2",
+						OutboundSelector: []string{"test"},
+						Strategy:         "leastload",
+						StrategySettings: serial.ToTypedMessage(&router.StrategyLeastLoadConfig{
+							HealthCheck: &router.HealthPingConfig{
+								Interval:      int64(time.Duration(300) * time.Second),
+								SamplingCount: 2,
+								Timeout:       int64(time.Duration(3) * time.Second),
+								Destination:   "dest",
+								Connectivity:  "conn",
+							},
+							Costs: []*router.StrategyWeight{
+								{
+									Regexp: true,
+									Match:  "\\d+(\\.\\d+)",
+									Value:  5,
+								},
+							},
+							Baselines: []int64{
+								int64(time.Duration(400) * time.Millisecond),
+								int64(time.Duration(600) * time.Millisecond),
+							},
+							Expected:  6,
+							MaxRTT:    int64(time.Duration(1000) * time.Millisecond),
+							Tolerance: 0.5,
+						}),
+						FallbackTag: "fall",
+					},
 				},
 				Rule: []*router.RoutingRule{
 					{

+ 3 - 0
main/commands/all/api/api.go

@@ -15,6 +15,9 @@ var CmdAPI = &base.Command{
 		cmdGetStats,
 		cmdQueryStats,
 		cmdSysStats,
+		cmdBalancerCheck,
+		cmdBalancerInfo,
+		cmdBalancerOverride,
 		cmdAddInbounds,
 		cmdAddOutbounds,
 		cmdRemoveInbounds,

+ 47 - 0
main/commands/all/api/balancer_check.go

@@ -0,0 +1,47 @@
+package api
+
+import (
+	routerService "github.com/v2fly/v2ray-core/v4/app/router/command"
+	"github.com/v2fly/v2ray-core/v4/main/commands/base"
+)
+
+var cmdBalancerCheck = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api bc [--server=127.0.0.1:8080] [balancer]...",
+	Short:       "balancer health check",
+	Long: `
+Perform instant health checks for specific balancers. If no 
+balancer tag specified, check all balancers.
+
+> Make sure you have "RoutingService" set in "config.api.services" 
+of server config.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 balancer1 balancer2
+`,
+	Run: executeBalancerCheck,
+}
+
+func executeBalancerCheck(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := routerService.NewRoutingServiceClient(conn)
+	r := &routerService.CheckBalancersRequest{BalancerTags: cmd.Flag.Args()}
+	_, err := client.CheckBalancers(ctx, r)
+	if err != nil {
+		base.Fatalf("failed to perform balancer health checks: %s", err)
+	}
+}

+ 122 - 0
main/commands/all/api/balancer_info.go

@@ -0,0 +1,122 @@
+package api
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+
+	routerService "github.com/v2fly/v2ray-core/v4/app/router/command"
+	"github.com/v2fly/v2ray-core/v4/main/commands/base"
+)
+
+var cmdBalancerInfo = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...",
+	Short:       "balancer information",
+	Long: `
+Get information of specified balancers, including health, strategy 
+and selecting. If no balancer tag specified, get information of 
+all balancers.
+
+> Make sure you have "RoutingService" set in "config.api.services" 
+of server config.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 balancer1 balancer2
+`,
+	Run: executeBalancerInfo,
+}
+
+func executeBalancerInfo(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := routerService.NewRoutingServiceClient(conn)
+	r := &routerService.GetBalancersRequest{BalancerTags: cmd.Flag.Args()}
+	resp, err := client.GetBalancers(ctx, r)
+	if err != nil {
+		base.Fatalf("failed to get health information: %s", err)
+	}
+	sort.Slice(resp.Balancers, func(i, j int) bool {
+		return resp.Balancers[i].Tag < resp.Balancers[j].Tag
+	})
+	for _, b := range resp.Balancers {
+		showBalancerInfo(b)
+	}
+}
+
+func showBalancerInfo(b *routerService.BalancerMsg) {
+	sb := new(strings.Builder)
+	// Balancer
+	sb.WriteString(fmt.Sprintf("Balancer: %s\n", b.Tag))
+	// Strategy
+	sb.WriteString("  - Strategy:\n")
+	for _, v := range b.StrategySettings {
+		sb.WriteString(fmt.Sprintf("    %s\n", v))
+	}
+	// Override
+	if b.Override != nil {
+		sb.WriteString("  - Selecting Override:\n")
+		until := fmt.Sprintf("until: %s", b.Override.Until)
+		writeRow(sb, 0, nil, nil, until)
+		for i, s := range b.Override.Selects {
+			writeRow(sb, i+1, nil, nil, s)
+		}
+	}
+	formats := getColumnFormats(b.Titles)
+	// Selects
+	sb.WriteString("  - Selects:\n")
+	writeRow(sb, 0, b.Titles, formats, "Tag")
+	for i, o := range b.Selects {
+		writeRow(sb, i+1, o.Values, formats, o.Tag)
+	}
+	// Others
+	scnt := len(b.Selects)
+	if len(b.Others) > 0 {
+		sb.WriteString("  - Others:\n")
+		writeRow(sb, 0, b.Titles, formats, "Tag")
+		for i, o := range b.Others {
+			writeRow(sb, scnt+i+1, o.Values, formats, o.Tag)
+		}
+	}
+	os.Stdout.WriteString(sb.String())
+}
+
+func getColumnFormats(titles []string) []string {
+	w := make([]string, len(titles))
+	for i, t := range titles {
+		w[i] = fmt.Sprintf("%%-%ds ", len(t))
+	}
+	return w
+}
+
+func writeRow(sb *strings.Builder, index int, values, formats []string, tag string) {
+	if index == 0 {
+		// title line
+		sb.WriteString("        ")
+	} else {
+		sb.WriteString(fmt.Sprintf("    %-4d", index))
+	}
+	for i, v := range values {
+		format := "%-14s"
+		if i < len(formats) {
+			format = formats[i]
+		}
+		sb.WriteString(fmt.Sprintf(format, v))
+	}
+	sb.WriteString(tag)
+	sb.WriteByte('\n')
+}

+ 87 - 0
main/commands/all/api/balancer_override.go

@@ -0,0 +1,87 @@
+package api
+
+import (
+	"time"
+
+	routerService "github.com/v2fly/v2ray-core/v4/app/router/command"
+	"github.com/v2fly/v2ray-core/v4/main/commands/base"
+)
+
+var cmdBalancerOverride = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> selectors...",
+	Short:       "balancer select override",
+	Long: `
+Override a balancer's selecting in a duration of time.
+
+> Make sure you have "RoutingService" set in "config.api.services" 
+of server config.
+
+Once a balancer's selecting is overridden:
+
+- The selectors of the balancer won't apply.
+- The strategy of the balancer stops selecting qualified nodes 
+  according to its settings, doing only the final pick.
+
+Arguments:
+
+	-r, -remove
+		Remove the overridden
+
+	-b, -balancer
+		Tag of the balancer. Required
+
+	-v, -validity
+		Time minutes of the validity of overridden. Default 60
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -b balancer selector1 selector2
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -b balancer -r
+`,
+	Run: executeBalancerOverride,
+}
+
+func executeBalancerOverride(cmd *base.Command, args []string) {
+	var (
+		balancer string
+		validity int64
+		remove   bool
+	)
+	cmd.Flag.StringVar(&balancer, "b", "", "")
+	cmd.Flag.StringVar(&balancer, "balancer", "", "")
+	cmd.Flag.Int64Var(&validity, "v", 60, "")
+	cmd.Flag.Int64Var(&validity, "validity", 60, "")
+	cmd.Flag.BoolVar(&remove, "r", false, "")
+	cmd.Flag.BoolVar(&remove, "remove", false, "")
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+
+	if balancer == "" {
+		base.Fatalf("balancer tag not specified")
+	}
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	v := int64(0)
+	if !remove {
+		v = int64(time.Duration(validity) * time.Minute)
+	}
+	client := routerService.NewRoutingServiceClient(conn)
+	r := &routerService.OverrideSelectingRequest{
+		BalancerTag: balancer,
+		Selectors:   cmd.Flag.Args(),
+		Validity:    v,
+	}
+	_, err := client.OverrideSelecting(ctx, r)
+	if err != nil {
+		base.Fatalf("failed to perform balancer health checks: %s", err)
+	}
+}