From 4d5a4f4cb6af26b36e5a1a4ecacbdaf3071692f4 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 18 Sep 2020 17:30:59 +0800 Subject: [PATCH] Routing: Implement Route interface as the routing result of Router --- app/dispatcher/default.go | 3 +- app/router/router.go | 87 +++++++++++------------- app/router/router_test.go | 20 +++--- features/routing/dns/context.go | 44 ++++++++++++ features/routing/dns/errors.generated.go | 9 +++ features/routing/router.go | 24 +++++-- 6 files changed, 122 insertions(+), 65 deletions(-) create mode 100644 features/routing/dns/context.go create mode 100644 features/routing/dns/errors.generated.go diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index c5aaf84f3..d74f243ed 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -266,7 +266,8 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport. } if d.router != nil && !skipRoutePick { - if tag, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil { + if route, err := d.router.PickRoute(routing_session.AsRoutingContext(ctx)); err == nil { + tag := route.GetOutboundTag() if h := d.ohm.GetHandler(tag); h != nil { newError("taking detour [", tag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) handler = h diff --git a/app/router/router.go b/app/router/router.go index 7e04c554d..de8321ba8 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -9,24 +9,12 @@ import ( "v2ray.com/core" "v2ray.com/core/common" - "v2ray.com/core/common/net" "v2ray.com/core/features/dns" "v2ray.com/core/features/outbound" "v2ray.com/core/features/routing" + routing_dns "v2ray.com/core/features/routing/dns" ) -func init() { - common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { - r := new(Router) - if err := core.RequireFeatures(ctx, func(d dns.Client, ohm outbound.Manager) error { - return r.Init(config.(*Config), d, ohm) - }); err != nil { - return nil, err - } - return r, nil - })) -} - // Router is an implementation of routing.Router. type Router struct { domainStrategy Config_DomainStrategy @@ -35,6 +23,13 @@ type Router struct { dns dns.Client } +// Route is an implementation of routing.Route. +type Route struct { + routing.Context + outboundGroupTags []string + outboundTag string +} + // Init initializes the Router. func (r *Router) Init(config *Config, d dns.Client, ohm outbound.Manager) error { r.domainStrategy = config.DomainStrategy @@ -74,39 +69,43 @@ func (r *Router) Init(config *Config, d dns.Client, ohm outbound.Manager) error } // PickRoute implements routing.Router. -func (r *Router) PickRoute(ctx routing.Context) (string, error) { - rule, err := r.pickRouteInternal(ctx) +func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) { + rule, ctx, err := r.pickRouteInternal(ctx) if err != nil { - return "", err + return nil, err } - return rule.GetTag() + tag, err := rule.GetTag() + if err != nil { + return nil, err + } + return &Route{Context: ctx, outboundTag: tag}, nil } -func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, error) { +func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) { if r.domainStrategy == Config_IpOnDemand { - ctx = ContextWithDNSClient(ctx, r.dns) + ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) } for _, rule := range r.rules { if rule.Apply(ctx) { - return rule, nil + return rule, ctx, nil } } if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 { - return nil, common.ErrNoClue + return nil, ctx, common.ErrNoClue } - ctx = ContextWithDNSClient(ctx, r.dns) + ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) // Try applying rules again if we have IPs. for _, rule := range r.rules { if rule.Apply(ctx) { - return rule, nil + return rule, ctx, nil } } - return nil, common.ErrNoClue + return nil, ctx, common.ErrNoClue } // Start implements common.Runnable. @@ -124,34 +123,24 @@ func (*Router) Type() interface{} { return routing.RouterType() } -// ContextWithDNSClient creates a new routing context with domain resolving capability. Resolved domain IPs can be retrieved by GetTargetIPs(). -func ContextWithDNSClient(ctx routing.Context, client dns.Client) routing.Context { - return &resolvableContext{Context: ctx, dnsClient: client} +// GetOutboundGroupTags implements routing.Route. +func (r *Route) GetOutboundGroupTags() []string { + return r.outboundGroupTags } -type resolvableContext struct { - routing.Context - dnsClient dns.Client - resolvedIPs []net.IP +// GetOutboundTag implements routing.Route. +func (r *Route) GetOutboundTag() string { + return r.outboundTag } -func (ctx *resolvableContext) GetTargetIPs() []net.IP { - if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { - return ips - } - - if len(ctx.resolvedIPs) > 0 { - return ctx.resolvedIPs - } - - if domain := ctx.GetTargetDomain(); len(domain) != 0 { - ips, err := ctx.dnsClient.LookupIP(domain) - if err == nil { - ctx.resolvedIPs = ips - return ips +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + r := new(Router) + if err := core.RequireFeatures(ctx, func(d dns.Client, ohm outbound.Manager) error { + return r.Init(config.(*Config), d, ohm) + }); err != nil { + return nil, err } - newError("resolve ip for ", domain).Base(err).WriteToLog() - } - - return nil + return r, nil + })) } diff --git a/app/router/router_test.go b/app/router/router_test.go index 0ed5f033d..8c1aec0aa 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -45,9 +45,9 @@ func TestSimpleRouter(t *testing.T) { })) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) - if tag != "test" { + if tag := route.GetOutboundTag(); tag != "test" { t.Error("expect tag 'test', bug actually ", tag) } } @@ -86,9 +86,9 @@ func TestSimpleBalancer(t *testing.T) { })) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) - if tag != "test" { + if tag := route.GetOutboundTag(); tag != "test" { t.Error("expect tag 'test', bug actually ", tag) } } @@ -121,9 +121,9 @@ func TestIPOnDemand(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) - if tag != "test" { + if tag := route.GetOutboundTag(); tag != "test" { t.Error("expect tag 'test', bug actually ", tag) } } @@ -156,9 +156,9 @@ func TestIPIfNonMatchDomain(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) - tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) - if tag != "test" { + if tag := route.GetOutboundTag(); tag != "test" { t.Error("expect tag 'test', bug actually ", tag) } } @@ -190,9 +190,9 @@ func TestIPIfNonMatchIP(t *testing.T) { common.Must(r.Init(config, mockDns, nil)) ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 80)}) - tag, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) common.Must(err) - if tag != "test" { + if tag := route.GetOutboundTag(); tag != "test" { t.Error("expect tag 'test', bug actually ", tag) } } diff --git a/features/routing/dns/context.go b/features/routing/dns/context.go new file mode 100644 index 000000000..fca58701e --- /dev/null +++ b/features/routing/dns/context.go @@ -0,0 +1,44 @@ +package dns + +//go:generate errorgen + +import ( + "v2ray.com/core/common/net" + "v2ray.com/core/features/dns" + "v2ray.com/core/features/routing" +) + +// ResolvableContext is an implementation of routing.Context, with domain resolving capability. +type ResolvableContext struct { + routing.Context + dnsClient dns.Client + resolvedIPs []net.IP +} + +// GetTargetIPs overrides original routing.Context's implementation. +func (ctx *ResolvableContext) GetTargetIPs() []net.IP { + if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { + return ips + } + + if len(ctx.resolvedIPs) > 0 { + return ctx.resolvedIPs + } + + if domain := ctx.GetTargetDomain(); len(domain) != 0 { + ips, err := ctx.dnsClient.LookupIP(domain) + if err == nil { + ctx.resolvedIPs = ips + return ips + } + newError("resolve ip for ", domain).Base(err).WriteToLog() + } + + return nil +} + +// ContextWithDNSClient creates a new routing context with domain resolving capability. +// Resolved domain IPs can be retrieved by GetTargetIPs(). +func ContextWithDNSClient(ctx routing.Context, client dns.Client) routing.Context { + return &ResolvableContext{Context: ctx, dnsClient: client} +} diff --git a/features/routing/dns/errors.generated.go b/features/routing/dns/errors.generated.go new file mode 100644 index 000000000..ba70372f0 --- /dev/null +++ b/features/routing/dns/errors.generated.go @@ -0,0 +1,9 @@ +package dns + +import "v2ray.com/core/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/features/routing/router.go b/features/routing/router.go index f473431ae..2acc96514 100644 --- a/features/routing/router.go +++ b/features/routing/router.go @@ -7,12 +7,26 @@ import ( // Router is a feature to choose an outbound tag for the given request. // -// v2ray:api:beta +// v2ray:api:stable type Router interface { features.Feature - // PickRoute returns a tag of an OutboundHandler based on the given context. - PickRoute(ctx Context) (string, error) + // PickRoute returns a route decision based on the given routing context. + PickRoute(ctx Context) (Route, error) +} + +// Route is the routing result of Router feature. +// +// v2ray:api:stable +type Route interface { + // A Route is also a routing context. + Context + + // GetOutboundGroupTags returns the detoured outbound group tags in sequence before a final outbound is chosen. + GetOutboundGroupTags() []string + + // GetOutboundTag returns the tag of the outbound the connection was dispatched to. + GetOutboundTag() string } // RouterType return the type of Router interface. Can be used to implement common.HasType. @@ -31,8 +45,8 @@ func (DefaultRouter) Type() interface{} { } // PickRoute implements Router. -func (DefaultRouter) PickRoute(ctx Context) (string, error) { - return "", common.ErrNoClue +func (DefaultRouter) PickRoute(ctx Context) (Route, error) { + return nil, common.ErrNoClue } // Start implements common.Runnable.