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

route precendence #389

Closed
tomwganem opened this issue Nov 14, 2017 · 22 comments
Closed

route precendence #389

tomwganem opened this issue Nov 14, 2017 · 22 comments
Milestone

Comments

@tomwganem
Copy link

tomwganem commented Nov 14, 2017

My problem is this: I have several subdomains that I want routed to specific containers, like such:

urlprefix-foo.domain.com/
urlprefix-bar.domain.com/

But beyond my list of specific of subdomains, I want to redirect all other subdomains to just one container.

urlprefix-*.domain.com/

The problem is that when I put in a route like *.domain.com, it takes precedence over the other routes, so they no longer work.

I have a workaround for now, which is:

urlprefix-foo.domain.com/
urlprefix-bar.domain.com/
urlprefix-*.domain.com:9997/

But I would rather just be able to set a precedence/priority, something like:
urlprefix-foo.domain.com/ #=1
urlprefix-bar.domain.com/ #=2
urlprefix-*.domain.com/ #=100

@aaronhurt
Copy link
Member

Just a general question as it relates to some things we've been investigating as well. Do you specifically need route prioritization or would a general most specific match priority work?

With a most-specific-match first assuming you had the following tags:

urlprefix-foo.domain.com/foo
urlprefix-foo.domain.com/
urlprefix-*.domain.com/

A request to https://foo.domain.com/foo/bar would always hit the first route. Requests to https://foo.domain.com/asdf would hit the second and a request to https://bar.domain.dom/foo would hit the last 'catch all' route.

@tomwganem
Copy link
Author

What @leprechau is describing, a most-specific-match, is ultimately what we want.

@aaronhurt
Copy link
Member

It wouldn't be perfect but I think you could get very close to 'most-specific' by just checking the matches (urlprefixes) sorted by length (longest first). This might be easier to implement and also resolve the issue along with documentation of how it works?

@tomwganem
Copy link
Author

Sure, I would be fine with that implementation. It would solve my use-case.

@tomwganem
Copy link
Author

Although, just going by length would mean

a.domain.com
and
*.domain.com

would have the same precedence. I don't have any single character subdomains I need to be concerned about, but someone might.

@aaronhurt
Copy link
Member

Right, that was the 'very close' part of the above comment :) ... it's absolutely not perfect but it may be less logic than actually trying to analyze the specify of the match. I have no real skin in the game at the moment, just trying to float some ideas. I'm sure @magiconair has his own thoughts on the matter.

@aaronhurt
Copy link
Member

Looking at the code a bit this may be how it is already supposed to function:

https://github.com/fabiolb/fabio/blob/master/route/table.go#L90-L93

@aaronhurt
Copy link
Member

aaronhurt commented Nov 15, 2017

Looked through the code some more and found the sort.Sort() definitions. If I'm not mistaken it should work as you want now.

https://play.golang.org/p/ZfspewZRxC

Really curious about the exact routes you are using now. Doing it by length is definitely wrong after looking at the output of my playground test.

@aaronhurt
Copy link
Member

@tomwganem
Copy link
Author

I was unable to get tracing working. Not sure what i'm doing wrong. For reference, I am using docker image fabiolb/fabio:1.5.3-go1.9.2. Here are my routes

# Service Source Dest Options Weight
1 files *.asperafiles-dev.com/ http://172.22.0.17:80/   100.00%
2 a8-fe analytics.asperafiles-dev.com/ http://172.22.0.27:80/   100.00%
3 a8-be api-analytics.asperafiles-dev.com/ http://172.22.0.26:9595/   100.00%
4 api api.asperafiles-dev.com/ http://172.22.0.24:9292/   100.00%
5 files asperafiles-dev.com/ http://172.22.0.17:80/ 100.00%

So in the above routing table, most of all of our requests are going to the service called 'files', which will serve up a webpage with some javascript.

Here's me curling a page I know should be routed to the files container. The response is correct.

$ curl -v -H 'Trace: abc' -H 'Host: foobar.asperafiles-dev.com' http://localhost:9999
* Rebuilt URL to: http://localhost:9999/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9999 (#0)
> GET / HTTP/1.1
> Host: foobar.asperafiles-dev.com
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: abc
>
< HTTP/1.1 200 OK
< Cache-Control: max-age=0
< Content-Type: text/html; charset=utf-8
< Date: Wed, 15 Nov 2017 07:25:22 GMT
< Etag: W/"5a0be5a5-691"
< Expires: Wed, 15 Nov 2017 07:25:22 GMT
< Last-Modified: Wed, 15 Nov 2017 06:58:45 GMT
< Server: nginx
< Content-Length: 1681
<
<!DOCTYPE html> <html lang="en" ng-app="f4.run" ng-strict-di> <head> <meta charset="utf-8"> <title>Aspera Files</title> <meta name="viewport" content="width=device-width"> <meta name="robots" content="noindex, nofollow"> <base href="/"> <link rel="stylesheet" href="/assets/vendor.c48814cf.css"> <link rel="stylesheet" href="/assets/app.e79ecf9b.css"> </head> <body> <alert-at-top></alert-at-top> <ui-view></ui-view> <scripts> <script src="//d3gcli72yxqn2z.cloudfront.net/connect/v4/connectinstaller-4.min.js"></script> <script src="//d3gcli72yxqn2z.cloudfront.net/connect/v4/asperaweb-4.min.js"></script> <script src="/assets/vendor.861f9768.js"></script> <script src="/assets/app.53b516ce.js"></script> <script> angular.module('f4')
        .constant('API_URL','//api.asperafiles-dev.com/api/v1')
        .constant('ANALYTICS_FRONTEND_URL', '//analytics.asperafiles-dev.com')
        .constant('OAUTH_URL','//api.asperafiles-dev.com/api/v1')
        .constant('OAUTH_CLIENT_ID','f4.com')
        .constant('CONNECT_URL','//d3gcli72yxqn2z.cloudfront.net/connect/v4')
        .constant('ORGANIZATION',location.hostname.split('.')[0])
        .constant('LOGIN_NOTICE','')
        .constant('IBM_METRIC_URL', '//nebula-cdn.kampyle.com/we/28600/onsite/embed.js')
        ; </script> <script src="//assets.zendesk.com/embeddable_framework/main.js" data-ze-csp="true" async defer></script> <script> (window.IBM_Meta = {
          offeringName: 'IBM Aspera Files',
          language: 'en',
          offeringId: '5725Y72',
          quarterlyIntercept: 'light',
          testData: false,
          highLevelOfferingName: 'Aspera (SaaS)'
* Connection #0 to host localhost left intact
        }); </script> </scripts> </body> </html>% 

And now I will try to curl api.asperafiles-dev.com. As you can see, I am still getting routed to thefiles container.

$ curl -v -H 'Trace: abc' -H 'Host: api.asperafiles-dev.com' http://localhost:9999/api/v1/self
* Rebuilt URL to: http://localhost:9999/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9999 (#0)
> GET / HTTP/1.1
> Host: api.asperafiles-dev.com
> User-Agent: curl/7.54.0
> Accept: */*
> Trace: abc
>
< HTTP/1.1 200 OK
< Cache-Control: max-age=0
< Content-Type: text/html; charset=utf-8
< Date: Wed, 15 Nov 2017 07:18:47 GMT
< Etag: W/"5a0be5a5-691"
< Expires: Wed, 15 Nov 2017 07:18:47 GMT
< Last-Modified: Wed, 15 Nov 2017 06:58:45 GMT
< Server: nginx
< Content-Length: 1681
<
<!DOCTYPE html> <html lang="en" ng-app="f4.run" ng-strict-di> <head> <meta charset="utf-8"> <title>Aspera Files</title> <meta name="viewport" content="width=device-width"> <meta name="robots" content="noindex, nofollow"> <base href="/"> <link rel="stylesheet" href="/assets/vendor.c48814cf.css"> <link rel="stylesheet" href="/assets/app.e79ecf9b.css"> </head> <body> <alert-at-top></alert-at-top> <ui-view></ui-view> <scripts> <script src="//d3gcli72yxqn2z.cloudfront.net/connect/v4/connectinstaller-4.min.js"></script> <script src="//d3gcli72yxqn2z.cloudfront.net/connect/v4/asperaweb-4.min.js"></script> <script src="/assets/vendor.861f9768.js"></script> <script src="/assets/app.53b516ce.js"></script> <script> angular.module('f4')
        .constant('API_URL','//api.asperafiles-dev.com/api/v1')
        .constant('ANALYTICS_FRONTEND_URL', '//analytics.asperafiles-dev.com')
        .constant('OAUTH_URL','//api.asperafiles-dev.com/api/v1')
        .constant('OAUTH_CLIENT_ID','f4.com')
        .constant('CONNECT_URL','//d3gcli72yxqn2z.cloudfront.net/connect/v4')
        .constant('ORGANIZATION',location.hostname.split('.')[0])
        .constant('LOGIN_NOTICE','')
        .constant('IBM_METRIC_URL', '//nebula-cdn.kampyle.com/we/28600/onsite/embed.js')
        ; </script> <script src="//assets.zendesk.com/embeddable_framework/main.js" data-ze-csp="true" async defer></script> <script> (window.IBM_Meta = {
          offeringName: 'IBM Aspera Files',
          language: 'en',
          offeringId: '5725Y72',
          quarterlyIntercept: 'light',
          testData: false,
          highLevelOfferingName: 'Aspera (SaaS)'
* Connection #0 to host localhost left intact
        }); </script> </scripts> </body> </html>

@tino
Copy link
Contributor

tino commented Nov 15, 2017

I don't think it sorts correctly now, as it uses sort.String here, which yields: https://play.golang.org/p/-FwwAv39C0. There I've also tried sorting with the "*" removed, which makes sense for domains without url path I think.

The first question to answer is which part is more important: the domain part or the url part. Because basically all route definitions are domain.com/*, but you don't have to write that last *.

I would say this makes sense for a default order of precedence:

  1. foo.domain.com/bar/baz/
  2. /bar/baz/ <= Usefull if you always want to handle /auth/ centrally, or send /.well-known/challenge to letsencrypt
  3. *.domain.com/bar/baz/
  4. foo.domain.com/bar/
  5. foo.domain.com/
  6. *.domain.com/

This works based on length now, besides 2 (which might also be controversial), but it doesn't if we use:

  1. preeeefix.short.com/b/z/
  2. /b/z/ <= Usefull if you always want to handle /auth/ centrally, or send /.well-known/challenge to letsencrypt
  3. *.short.com/b/z/
  4. preeeefix.short.com/b/
  5. preeeefix.short.com/
  6. *.short.com/

@magiconair
Copy link
Contributor

Paths are already sorted from most to least specific. I think the issue is in the host matching.

@magiconair
Copy link
Contributor

magiconair commented Nov 15, 2017


https://github.com/fabiolb/fabio/blob/master/route/table.go#L287-L298

~~~You have setup where multiple entries match the hostname and in the current implementation the behavior is undefined. I think a better approach would be to find all matching entries, check for an exact match, and if none was found use the longest match.~~~

I spoke too soon. Still looking.

magiconair added a commit that referenced this issue Nov 15, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
@magiconair
Copy link
Contributor

The problem was in that function but not where I thought. The issue is that the matchingHosts function sorts the hostnames from least to most specific instead of the other way around.

The patch I've put in #390 fixes this but I still need to test whether it matches x.foo.com before *.foo.com as well.

@magiconair
Copy link
Contributor

I guess this should work since * is before any of the characters in the UTF-8 table.

magiconair added a commit that referenced this issue Nov 15, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
@magiconair
Copy link
Contributor

This fix doesn't work properly yet. There is still a random element in there. Looking

magiconair added a commit that referenced this issue Nov 16, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
@magiconair
Copy link
Contributor

Found it. This should work now.

magiconair added a commit that referenced this issue Nov 16, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
magiconair added a commit that referenced this issue Nov 16, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
@tomwganem
Copy link
Author

I can confirm that #390 fixes the behavior I was seeing 👍

magiconair added a commit that referenced this issue Nov 16, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
magiconair added a commit that referenced this issue Nov 16, 2017
This patch ensures that exact hostname matches are preferred over glob
matches.

Fixes #389
@pklingem
Copy link

pklingem commented Dec 4, 2017

I don't see this issue listed in the existing milestones, is this to be included in 1.6 or in a 1.5.x release? And is there an ETA on that release?

@magiconair magiconair added this to the 1.6 milestone Dec 4, 2017
@magiconair
Copy link
Contributor

I've tagged it now for 1.6 but I'll probably roll a 1.5.4 for this since the metrics refactor takes longer than expected.

@magiconair
Copy link
Contributor

Will be released this week.

@magiconair magiconair modified the milestones: 1.6, 1.5.4 Dec 9, 2017
@orthecreedence
Copy link

orthecreedence commented Sep 4, 2018

EDIT: Forgive me, I just found #506, which appears to be this exact issue.


Hello, I'm seeing an issue with route precedence in the 1.5.9 release.

We have the rules:

route add fe-app-stage *.stage.myapp.com/ http://172.27.20.146:22712/
route add fe-app-qa *.qa.myapp.com/ http://172.27.10.211:20979/
route add fe-app *.myapp.com/ http://172.27.20.123:21445/

Running curl 127.0.0.1:9999/ -H'Host: test.qa.myapp.com' -H'Trace: LOL' returns the result from the fe-app route:

2018/09/04 15:40:57 [TRACE] LOL Tracing test.qa.myapp.com/
2018/09/04 15:40:57 [TRACE] LOL Match *.myapp.com/
2018/09/04 15:40:57 [TRACE] LOL Routing to service fe-app on http://172.27.20.123:21445/

(Same result for test.stage.myapp.com)

I would expect it to take the longest/most specfic host first, and match the *.qa or *.stage routes before going to *.myapp.com

It appears that non-wildcard routes take precedence, but among wildcard routes themselves, there is noe specific precedence. Is there any way to work around this for now without having to use paths? Like any way to use regexes in the hosts so *.myapp.com could be (?!=(stage|qa).*).myapp.com?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants