diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index a0bea4cb3..1a44e92fe 100644 --- a/app/dispatcher/default.go +++ b/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 diff --git a/app/router/balancing.go b/app/router/balancing.go index 80f84df2b..8de382cf0 100644 --- a/app/router/balancing.go +++ b/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 +} diff --git a/app/router/balancing_override.go b/app/router/balancing_override.go new file mode 100644 index 000000000..0abc71bea --- /dev/null +++ b/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{} +} diff --git a/app/router/command/command.go b/app/router/command/command.go index 883e7151b..46a764be4 100644 --- a/app/router/command/command.go +++ b/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 { diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go index 5f15a88c7..f74005bed 100644 --- a/app/router/command/command.pb.go +++ b/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, }, diff --git a/app/router/command/command.proto b/app/router/command/command.proto index e7d485459..72db9105f 100644 --- a/app/router/command/command.proto +++ b/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 {} diff --git a/app/router/command/command_grpc.pb.go b/app/router/command/command_grpc.pb.go index 9d3dae9aa..4480a5bc3 100644 --- a/app/router/command/command_grpc.pb.go +++ b/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{ { diff --git a/app/router/command/command_test.go b/app/router/command/command_test.go index 9c6c0a8af..780448813 100644 --- a/app/router/command/command_test.go +++ b/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) { diff --git a/app/router/config.go b/app/router/config.go index 29b2096fe..13ad21db1 100644 --- a/app/router/config.go +++ b/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 } } diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 928f4b7a5..a20b94791 100644 --- a/app/router/config.pb.go +++ b/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, }, diff --git a/app/router/config.proto b/app/router/config.proto index 13cc19909..f7de591d9 100644 --- a/app/router/config.proto +++ b/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 { diff --git a/app/router/healthping.go b/app/router/healthping.go new file mode 100644 index 000000000..1c7ce4a86 --- /dev/null +++ b/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 +} diff --git a/app/router/healthping_result.go b/app/router/healthping_result.go new file mode 100644 index 000000000..a419fd80c --- /dev/null +++ b/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 +} diff --git a/app/router/healthping_result_test.go b/app/router/healthping_result_test.go new file mode 100644 index 000000000..decdde6bf --- /dev/null +++ b/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) + } +} diff --git a/app/router/ping.go b/app/router/ping.go new file mode 100644 index 000000000..ba192264a --- /dev/null +++ b/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 +} diff --git a/app/router/router.go b/app/router/router.go index 0583562ea..d09e9e144 100644 --- a/app/router/router.go +++ b/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 } diff --git a/app/router/router_health.go b/app/router/router_health.go new file mode 100644 index 000000000..b80956cdb --- /dev/null +++ b/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 +} diff --git a/app/router/router_test.go b/app/router/router_test.go index deb76f210..b594baafe 100644 --- a/app/router/router_test.go +++ b/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)) diff --git a/app/router/strategy_leastload.go b/app/router/strategy_leastload.go new file mode 100644 index 000000000..9a45270d1 --- /dev/null +++ b/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 + }) +} diff --git a/app/router/strategy_leastload_test.go b/app/router/strategy_leastload_test.go new file mode 100644 index 000000000..a097c4817 --- /dev/null +++ b/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)) + } +} diff --git a/app/router/strategy_leastping.go b/app/router/strategy_leastping.go index d9ba6b453..f16b04290 100644 --- a/app/router/strategy_leastping.go +++ b/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 } diff --git a/app/router/strategy_random.go b/app/router/strategy_random.go new file mode 100644 index 000000000..bd809e78b --- /dev/null +++ b/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)] +} diff --git a/app/router/weight.go b/app/router/weight.go new file mode 100644 index 000000000..f60d24c71 --- /dev/null +++ b/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) +} diff --git a/app/router/weight_test.go b/app/router/weight_test.go new file mode 100644 index 000000000..78fb69259 --- /dev/null +++ b/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) + } +} diff --git a/common/session/context.go b/common/session/context.go index 5dab24d6e..04266b59a 100644 --- a/common/session/context.go +++ b/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 +} diff --git a/common/session/session.go b/common/session/session.go index 2ee5cc46e..5e9c0afc9 100644 --- a/common/session/session.go +++ b/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 { diff --git a/features/routing/health.go b/features/routing/health.go new file mode 100644 index 000000000..d061c0f76 --- /dev/null +++ b/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) +} diff --git a/features/routing/strategy.go b/features/routing/strategy.go new file mode 100644 index 000000000..c17df8c86 --- /dev/null +++ b/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 +} diff --git a/go.sum b/go.sum index 09983bf63..e42114b69 100644 --- a/go.sum +++ b/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= diff --git a/infra/conf/api.go b/infra/conf/api.go index 0749e3db3..17ac223ac 100644 --- a/infra/conf/api.go +++ b/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 diff --git a/infra/conf/router.go b/infra/conf/router.go index bb5c83dc7..cfceead21 100644 --- a/infra/conf/router.go +++ b/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 } diff --git a/infra/conf/router_strategy.go b/infra/conf/router_strategy.go index b8536330c..2175ad5d5 100644 --- a/infra/conf/router_strategy.go +++ b/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 +} diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go index 672eda4bc..32e7b3e45 100644 --- a/infra/conf/router_test.go +++ b/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{ { diff --git a/main/commands/all/api/api.go b/main/commands/all/api/api.go index cd10148bf..5915d3479 100644 --- a/main/commands/all/api/api.go +++ b/main/commands/all/api/api.go @@ -15,6 +15,9 @@ var CmdAPI = &base.Command{ cmdGetStats, cmdQueryStats, cmdSysStats, + cmdBalancerCheck, + cmdBalancerInfo, + cmdBalancerOverride, cmdAddInbounds, cmdAddOutbounds, cmdRemoveInbounds, diff --git a/main/commands/all/api/balancer_check.go b/main/commands/all/api/balancer_check.go new file mode 100644 index 000000000..7bf171f57 --- /dev/null +++ b/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) + } +} diff --git a/main/commands/all/api/balancer_info.go b/main/commands/all/api/balancer_info.go new file mode 100644 index 000000000..afdf36d9c --- /dev/null +++ b/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') +} diff --git a/main/commands/all/api/balancer_override.go b/main/commands/all/api/balancer_override.go new file mode 100644 index 000000000..f675792b3 --- /dev/null +++ b/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) + } +}