Fabio: route precendence

Created on 14 Nov 2017  路  22Comments  路  Source: fabiolb/fabio

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

Most helpful comment

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.

All 22 comments

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.

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

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?

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

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.

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.

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

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.

@tomwganem Could you post a trace log?

https://github.com/fabiolb/fabio/wiki/Features#request-tracing

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>

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/

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

~The issue is in matchingHosts which assumes that there is only one entry that matches the host.~

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.

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.

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

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

Found it. This should work now.

I can confirm that #390 fixes the behavior I was seeing 馃憤

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?

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.

Will be released this week.

EDIT: Forgive me, I just found https://github.com/fabiolb/fabio/issues/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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

doctorj picture doctorj  路  10Comments

composer22 picture composer22  路  14Comments

axingblog picture axingblog  路  3Comments

magiconair picture magiconair  路  12Comments

drawks picture drawks  路  12Comments