Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Routing: Statistics and Service Support #229

Closed
wants to merge 1 commit into from

Conversation

Vigilans
Copy link
Contributor

@Vigilans Vigilans commented Sep 26, 2020

Closes #105. This PR:

  1. exposes RoutingService to user through json config:
  "api": {
    "tag": "api",
    "services": ["RoutingService"]
  }
  1. introduces support of routing statistics:
  "stats": {
    "routing": {
      "enabled": true
    }
  }

With this config, Stats Manager will registers a Stats Channel with a key specified in feature/stats:

if config.Routing != nil {
	m.registerChannelInternal(stats.RoutingStatsKey, NewChannel(config.Routing))
}

Where this specific channel will be retrieved by both publisher and subscriber side:

  • Publisher: Dispatcher
d.routingStats = sm.GetChannel(stats.RoutingStatsKey)
  • Subscriber: Routing service (also as Publisher for TestRoute API)
common.Must(s.v.RequireFeatures(func(router routing.Router, manager stats.Manager) {
	RegisterRoutingServiceServer(server, NewRoutingServer(router, manager.GetChannel(stats.RoutingStatsKey)))
}))

So a routing statistics push model is established.

@Vigilans
Copy link
Contributor Author

Client test code:

package main

import (
	"context"
	"fmt"

	"google.golang.org/grpc"
	"v2ray.com/core/app/router/command"
)

func main() {
	conn, err := grpc.DialContext(context.Background(), "127.0.0.1:10086", grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		fmt.Println(err)
		return
	}
	client := command.NewRoutingServiceClient(conn)
	stream, err := client.SubscribeRoutingStats(context.Background(), &command.SubscribeRoutingStatsRequest{})
	if err != nil {
		fmt.Println(err)
		return
	}
	for {
		route, err := stream.Recv()
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println(route)
	}
}

with related configs:

  "routing": {
    "rules": [
      {
        "type": "field",
        "inboundTag": ["api"],
        "outboundTag": "api"
      },
  },
  "inbounds": [
    {
      "tag": "api",
      "protocol": "dokodemo-door",
      "listen": "127.0.0.1",
      "port": 10086,
      "settings": {
        "address": "127.0.0.1"
      }
    }
  ],
  "stats": {
    "routing": {
      "enabled": true
    }
  },
  "api": {
    "tag": "api",
    "services": ["RoutingService"]
  },

Result:

InboundTag:"http"  Network:TCP  SourceIPs:"\x00\x00\x01"  TargetIPs:"'\x9cEO"  TargetIPs:"ܵ&\x94"  SourcePort:56744  TargetPort:80  TargetDomain:"baidu.com"  Protocol:"http/1.1"  Attributes:{key:":method"  value:"GET"}  Attributes:{key:":path"  value:"/"}  Attributes:{key:"accept"  value:"*/*"}  Attributes:{key:"user-agent"  value:"curl/7.52.1"}  OutboundTag:"direct"
InboundTag:"dns"  Network:UDP  TargetIPs:"rrrr"  TargetPort:53  Protocol:"dns"  OutboundTag:"direct"
InboundTag:"http"  Network:TCP  SourceIPs:"\x00\x00\x01"  TargetIPs:"\xac٣\xee"  TargetIPs:"$\x04h\x00@\x05\x08\x03\x00\x00\x00\x00\x00\x00 \x0e"  SourcePort:57002  TargetPort:80  TargetDomain:"google.com"  Protocol:"http/1.1"  Attributes:{key:":method"  value:"GET"}  Attributes:{key:":path"  value:"/"}  Attributes:{key:"accept"  value:"*/*"}  Attributes:{key:"user-agent"  value:"curl/7.52.1"}  OutboundTag:"proxy"
InboundTag:"dns"  Network:UDP  TargetIPs:"\x08\x08\x08\x08"  TargetPort:53  Protocol:"dns"  OutboundTag:"proxy"
InboundTag:"dns"  Network:UDP  TargetIPs:"\x08\x08\x08\x08"  TargetPort:53  Protocol:"dns"  OutboundTag:"proxy"
InboundTag:"dns"  Network:UDP  TargetIPs:"\x08\x08\x08\x08"  TargetPort:53  Protocol:"dns"  OutboundTag:"proxy"
InboundTag:"dns"  Network:UDP  TargetIPs:"\xdf\x05\x05\x05"  TargetPort:53  Protocol:"dns"  OutboundTag:"direct"
InboundTag:"dns"  Network:UDP  TargetIPs:"\x01\x01\x01\x01"  TargetPort:53  Protocol:"dns"  OutboundTag:"proxy"
InboundTag:"http"  Network:TCP  SourceIPs:"\x00\x00\x01"  TargetIPs:"'\x9cEO"  TargetIPs:"ܵ&\x94"  SourcePort:57058  TargetPort:80  TargetDomain:"baidu.com"  Protocol:"http/1.1"  Attributes:{key:":method"  value:"GET"}  Attributes:{key:":path"  value:"/"}  Attributes:{key:"accept"  value:"*/*"}  Attributes:{key:"user-agent"  value:"curl/7.52.1"}  OutboundTag:"direct"
InboundTag:"dns"  Network:UDP  TargetIPs:"rrrr"  TargetPort:53  Protocol:"dns"  OutboundTag:"direct"
InboundTag:"dns"  Network:UDP  TargetIPs:"w\x1d\x1d\x1d"  TargetPort:53  Protocol:"dns"  OutboundTag:"direct"

Client after Killing V2ray Process:

rpc error: code = Unavailable desc = transport is closing

Client subscribing after there's already a client subscriber:

rpc error: code = Unknown desc = v2ray.com/core/app/stats: Number of subscribers has reached limit

@Vigilans
Copy link
Contributor Author

Vigilans commented Sep 26, 2020

@kslr There're too many Config struct exported without a comment in infra/conf. Personally I think the exported codacy check may be ignored for infra/conf/*.go.

It is impossible to disable exported comment check in golint as per golang/lint#186, golang/lint#389.

According to Codacy Configuration File, files with glob pattern could be disabled for some engines, like tool (golint), category (duplication). However, the 'exported' issue is a Code Pattern, and a Code Pattern is not documented whether it could be ignored using engines field. If this field does not apply to Code Pattern, the config may have to use a broader scope Category with the tag 'Code Style'.

@Vigilans Vigilans force-pushed the vigilans/stats-routing branch from f65e454 to 7fee8c1 Compare September 26, 2020 17:04
@kslr
Copy link
Contributor

kslr commented Sep 26, 2020

We can ignore certain folders

Comment on lines +280 to +282
if d.routingStats != nil {
d.routingStats.Publish(route)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routing statistics is published from Dispather instead of Router, so as to:

  1. By not placing publishing logic in PickRoute, we can control whether the returned route should be published.
  2. By not placing stats channel in Router, router can be kept as a more light-weight feature.

infra/conf/stats.go Outdated Show resolved Hide resolved
Comment on lines 27 to 35
return &stats.ChannelConfig{
SubscriberLimit: 1,
BufferSize: 16,
BroadcastTimeout: 100,
}, nil
Copy link
Contributor Author

@Vigilans Vigilans Sep 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value of ChannelConfig is set in config building process. All Channels exposed to client side are default to have at most 1 subscriber.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为什么默认值是1呢,是考虑什么情况?

Copy link
Contributor Author

@Vigilans Vigilans Sep 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref #105:

To regulate the usage of routing stats service:

  • When routing stats is enabled, at most 1 client can subscribe to the gRPC stream channel. If one decides to enable routing stats, it should take responsibility to make sure its own client (GUI or user written program) subscribes successfully, so routing stats will not be managed by third-party applications.

Copy link
Contributor Author

@Vigilans Vigilans Sep 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, wouldn't it be unsafe if a gRPC service with these APIs is discovered in the first place? Should api service be implemented with some authentication mechanism like grpc's TLS credentials? This may also help export api service remotely.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The api can be protected by routing, but we can also add TLS Credentials

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, now we just need to make it configurable, and the default is 1(?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will tweak the code to find a not ugly implementation, since integer value defaults to 0 and 0 means not limited in 'SubscriberLimit', unlike nil which can be viewed as field not set.

This is a breaking change

If the authentication is implemented, should it be disabled if credential/token not set for backward compatibility, or be required as breaking change to enforce security update?

Copy link
Contributor

@kslr kslr Sep 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the workload is the same, I suggest compatible. But if needed, this is not a problem.

set the listen and port to solve security problems

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set the listen and port to solve security problems

More hint on how to configure listen and port?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

屏幕快照 2020-09-28 下午6 54 49

@Vigilans Vigilans force-pushed the vigilans/stats-routing branch 2 times, most recently from db15c22 to a2c8270 Compare September 27, 2020 15:36
Comment on lines +10 to +14
// ChannelConfig is the JSON config for app/stats.Channel.
type ChannelConfig struct {
Enabled bool `json:"enabled"`
SubscriberLimit *int32 `json:"subscriberLimit"`
}
Copy link
Contributor Author

@Vigilans Vigilans Sep 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subscriberLimit is exposed to user through JSON config now. Its value still defaults to 1, setting it to 0 to disable the limit on subscribers.

enabled field is still kept, to not confuse the user with setting an empty object {} to enable the channel. I also found the existence of enabled field in MuxConfig and SniffingConfig.

Comment on lines +22 to +58
runMultiTestCase(t, []TestCase{
{
Input: `{}`,
Parser: createParser(),
Output: (*stats.ChannelConfig)(nil),
},
{
Input: `{
"enabled": true
}`,
Parser: createParser(),
Output: &stats.ChannelConfig{
SubscriberLimit: 1,
BufferSize: 16,
BroadcastTimeout: 100,
},
},
{
Input: `{
"enabled": true,
"subscriberLimit": 0
}`,
Parser: createParser(),
Output: &stats.ChannelConfig{
SubscriberLimit: 0,
BufferSize: 16,
BroadcastTimeout: 100,
},
},
{
Input: `{
"subscriberLimit": 0
}`,
Parser: createParser(),
Output: (*stats.ChannelConfig)(nil),
},
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test of parsing Channel JSON config is added.

@Vigilans Vigilans force-pushed the vigilans/stats-routing branch from a2c8270 to 37b561a Compare September 27, 2020 15:46
@kslr
Copy link
Contributor

kslr commented Sep 30, 2020

发现一个测试错误

=== RUN   TestSerivceTestRoute
panic: Fail in goroutine after TestServiceSubscribeRoutingStats has completed

goroutine 10 [running]:
testing.(*common).Fail(0xc00017a300)
	/Users/runner/hostedtoolcache/go/1.15.2/x64/src/testing/testing.go:680 +0x125
testing.(*common).Error(0xc00017a300, 0xc000149e48, 0x1, 0x1)
	/Users/runner/hostedtoolcache/go/1.15.2/x64/src/testing/testing.go:780 +0x78
v2ray.com/core/app/router/command_test.TestServiceSubscribeRoutingStats.func4(0xc00014e440, 0xc000040960, 0xc00000f5c0, 0xc000033b40, 0x8, 0x8, 0xc00017a300, 0xc00007cfc0, 0xc0000409c0)
	/Users/runner/work/v2ray-core/v2ray-core/app/router/command/command_test.go:167 +0xd13
created by v2ray.com/core/app/router/command_test.TestServiceSubscribeRoutingStats
	/Users/runner/work/v2ray-core/v2ray-core/app/router/command/command_test.go:91 +0x885
FAIL	v2ray.com/core/app/router/command	0.243s

@Vigilans
Copy link
Contributor Author

发现一个测试错误

=== RUN   TestSerivceTestRoute
panic: Fail in goroutine after TestServiceSubscribeRoutingStats has completed

goroutine 10 [running]:
testing.(*common).Fail(0xc00017a300)
	/Users/runner/hostedtoolcache/go/1.15.2/x64/src/testing/testing.go:680 +0x125
testing.(*common).Error(0xc00017a300, 0xc000149e48, 0x1, 0x1)
	/Users/runner/hostedtoolcache/go/1.15.2/x64/src/testing/testing.go:780 +0x78
v2ray.com/core/app/router/command_test.TestServiceSubscribeRoutingStats.func4(0xc00014e440, 0xc000040960, 0xc00000f5c0, 0xc000033b40, 0x8, 0x8, 0xc00017a300, 0xc00007cfc0, 0xc0000409c0)
	/Users/runner/work/v2ray-core/v2ray-core/app/router/command/command_test.go:167 +0xd13
created by v2ray.com/core/app/router/command_test.TestServiceSubscribeRoutingStats
	/Users/runner/work/v2ray-core/v2ray-core/app/router/command/command_test.go:91 +0x885
FAIL	v2ray.com/core/app/router/command	0.243s

Caused by:

=== RUN   TestServiceSubscribeRoutingStats
    command_test.go:182: rpc error: code = Unknown desc = v2ray.com/core/app/router/command: Receiving upstream statistics failed.
--- FAIL: TestServiceSubscribeRoutingStats (0.01s)

I should have exited the goroutine after the error is sent to error channel.

if err != nil {
	errCh <- err
        // return // A return is missing here
}

Investigating why upstream statistics failed to receive.

@Vigilans
Copy link
Contributor Author

Vigilans commented Sep 30, 2020

@kslr found the reason...the services blocked on stream.Send(genMessage(route)) and the message failed to successfully sent in 100ms, reaching the timeout setting of stats channel, thus the subscriber is forced to be unsubscribed, resulting in the error.

(This rarely happens in real scenario since the channel has a buffer, and routing stats are not sent in a moment, so there are more time left for service stream to send messages. But in the test all testcases are published at once.)

I am seeking to tweak the implementation of broadcast logic.

@kslr
Copy link
Contributor

kslr commented Nov 16, 2020

@Vigilans are you ok?

@github-actions
Copy link
Contributor

It has been open 120 days with no activity. Remove stale label or comment or this will be closed in 5 days

@github-actions github-actions bot added the Stale label Mar 17, 2021
@github-actions github-actions bot closed this Mar 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Discuss] Enable user access to detailed routing information
2 participants