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

fix(router) wildcard host matching not considered with plain host match #4152

Closed
wants to merge 1 commit into from

Conversation

bungle
Copy link
Member

@bungle bungle commented Jan 3, 2019

Summary

When two routes are specified with wildcard host and plain host, the wildcard host may be skipped in some cases. See the test suite change that hilights the problem.

Issues resolved

Fixes issue reported here:
#3094 (comment)

@thibaultcha
Copy link
Member

It seems like a couple of wildcard routing unit tests are currently failing in the current CI run?

@bungle bungle force-pushed the fix/router-hosts-matching branch from 7b67c98 to 326f81e Compare January 4, 2019 00:10
@bungle
Copy link
Member Author

bungle commented Jan 4, 2019

@thibaultcha thanks, it should be fixed now.

Copy link
Member

@thibaultcha thibaultcha left a comment

Choose a reason for hiding this comment

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

Agreed on the issue at hand, the added test proves the invalid behavior well (but should be ported to a unit test). However, I question the fix. It seems like quite a high price to pay to fix this...

@@ -986,23 +988,24 @@ function _M.new(routes)

-- host match

if plain_indexes.hosts[ctx.req_host] then
req_category = bor(req_category, MATCH_RULES.HOST)
Copy link
Member

Choose a reason for hiding this comment

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

This change can be highly detrimental in many scenarios.

@@ -512,7 +512,8 @@ end
do
local matchers = {
[MATCH_RULES.HOST] = function(route_t, ctx)
local host = route_t.hosts[ctx.hits.host or ctx.req_host]
local host = route_t.hosts[ctx.req_host] or
route_t.hosts[ctx.hits.host]
Copy link
Member

Choose a reason for hiding this comment

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

Here, we are ignoring the matched hint, which is thus rendered useless. In which case we could simply remove it. This change can be detrimental in some scenarios.

Copy link
Member Author

@bungle bungle Jan 9, 2019

Choose a reason for hiding this comment

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

What do you mean? If you remove route_t.hosts[ctx.hits.host] the tests will not pass, and if you remove route_t.hosts[ctx.req_host] the tests won't pass either. So it is not ignored (?).

assert.equal(routes[2].id, res.headers["kong-route-id"])
assert.equal(routes[2].service.id, res.headers["kong-service-id"])
assert.equal(routes[2].service.name, res.headers["kong-service-name"])
end)
Copy link
Member

Choose a reason for hiding this comment

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

This test is a duplicate of this unit test.

@bungle bungle force-pushed the fix/router-hosts-matching branch from 326f81e to c3787a9 Compare January 9, 2019 14:45
@bungle bungle force-pushed the fix/router-hosts-matching branch from c3787a9 to 3238871 Compare January 9, 2019 18:24
@bungle bungle added pr/do not merge pr/discussion This PR is being debated. Probably just a few details. and removed pr/please review labels Jan 9, 2019
thibaultcha added a commit that referenced this pull request Jul 3, 2019
Background
----------

The rules of wildcard hosts matching in the router state that:

1. Routes with more matching attributes take precedence over Routes with
   fewer matching attributes
2. Plain hosts take precedence over wildcard hosts

Consider the following scenario (which is a simplification of this
issue, and already covered **prior** to this patch):

    r1:
        hosts = { "*.route.com" }

    r2:
        hosts = { "plain.route.com" }

    local matched_route = router.select("GET", "/", "plain.route.com")
    -- matched_route == r2

In the above case, both Routes have a single matching attribute,
resulting in a tie for rule 1. described above.  Therefore, we fallback
to rule 2.: the plain host takes precedence over the wildcard one.

The above case is already covered by our test suite ("does not take
precedence over a plain host").

Issue
-----

For the case at hand, consider the following scenario:

    r1:
        hosts = { "*.route.com" }
        paths = { "/path1" }

    r2:
        hosts = { "plain.route.com" }
        paths = { "/path2" }

    local matched_route = router.select("GET", "/path1", "plain.route.com")
    -- /!\ matched_route should be r1, but prior to this patch was 'nil'

    local matched_route = router.select("GET", "/path2", "plain.route.com")
    -- matched_route == r2

In the above case, both Routes have two matching attributes, resulting
in a tie for rule 1; both Routes will therefore be evaluated at the same
time (as part of the same matching category).

**However**, the Hosts matcher failed to evaluate a Route's wildcard
`hosts` because it expected the indexer to have evaluated it and stored
the matching wildcard host in `ctx.hits.host`. But since a plain host
was matched (r1's `plain.route.com`), no such wildcard host header was
matched in the indexes, and `ctx.hits.host` was `nil`. Having not
matched r1, we evaluate r2 which doesn't match either (`/path1` !=
`/path2`).

It is worth noting that the router should work with empty indexes
anyway (in tests only), something that we may want to explore in the
future, for the sake of hardening its behavior and maybe catching a
couple of bugs along the way.

Notes
-----

It is worth noting that this patch can be breaking in its nature, given
that the router behavior highlighted by the new test will differ before
and after the patch.

This patches marks a test as pending because fixing the matcher in this
patch made another issue surfaced: categorized Routes are not sorted by
"sub-matching" priority order (see rule 2.); Routes with plain hosts
only should be evaluated first since they have precedence over Routes
with one or more wildcards. A subsequent commit will fix this and
re-enable the test case.

This patch also replaces the one in
#4152, which is more expensive and
relies on indexes to fix the issue, instead of fixing the matcher. It
was also lacking unit tests, which are more appropriate (and mandatory)
for router changes.

Replaces #4152
Fix #3090
@thibaultcha
Copy link
Member

Replaced by #4775

@thibaultcha thibaultcha closed this Jul 3, 2019
@thibaultcha thibaultcha deleted the fix/router-hosts-matching branch July 3, 2019 21:43
thibaultcha added a commit that referenced this pull request Jul 15, 2019
Background
----------

The rules of wildcard hosts matching in the router state that:

1. Routes with more matching attributes take precedence over Routes with
   fewer matching attributes
2. Plain hosts take precedence over wildcard hosts

Consider the following scenario (which is a simplification of this
issue, and already covered **prior** to this patch):

    r1:
        hosts = { "*.route.com" }

    r2:
        hosts = { "plain.route.com" }

    local matched_route = router.select("GET", "/", "plain.route.com")
    -- matched_route == r2

In the above case, both Routes have a single matching attribute,
resulting in a tie for rule 1. described above.  Therefore, we fallback
to rule 2.: the plain host takes precedence over the wildcard one.

The above case is already covered by our test suite ("does not take
precedence over a plain host").

Issue
-----

For the case at hand, consider the following scenario:

    r1:
        hosts = { "*.route.com" }
        paths = { "/path1" }

    r2:
        hosts = { "plain.route.com" }
        paths = { "/path2" }

    local matched_route = router.select("GET", "/path1", "plain.route.com")
    -- /!\ matched_route should be r1, but prior to this patch was 'nil'

    local matched_route = router.select("GET", "/path2", "plain.route.com")
    -- matched_route == r2

In the above case, both Routes have two matching attributes, resulting
in a tie for rule 1; both Routes will therefore be evaluated at the same
time (as part of the same matching category).

**However**, the Hosts matcher failed to evaluate a Route's wildcard
`hosts` because it expected the indexer to have evaluated it and stored
the matching wildcard host in `ctx.hits.host`. But since a plain host
was matched (r1's `plain.route.com`), no such wildcard host header was
matched in the indexes, and `ctx.hits.host` was `nil`. Having not
matched r1, we evaluate r2 which doesn't match either (`/path1` !=
`/path2`).

It is worth noting that the router should work with empty indexes
anyway (in tests only), something that we may want to explore in the
future, for the sake of hardening its behavior and maybe catching a
couple of bugs along the way.

Notes
-----

It is worth noting that this patch can be breaking in its nature, given
that the router behavior highlighted by the new test will differ before
and after the patch.

This patches marks a test as pending because fixing the matcher in this
patch made another issue surfaced: categorized Routes are not sorted by
"sub-matching" priority order (see rule 2.); Routes with plain hosts
only should be evaluated first since they have precedence over Routes
with one or more wildcards. A subsequent commit will fix this and
re-enable the test case.

This patch also replaces the one in
#4152, which is more expensive and
relies on indexes to fix the issue, instead of fixing the matcher. It
was also lacking unit tests, which are more appropriate (and mandatory)
for router changes.

Replaces #4152
Fix #3090
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr/discussion This PR is being debated. Probably just a few details.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants