Skip to content

Support path routes with port number in Host header #8490

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

Merged
merged 1 commit into from
Apr 28, 2016

Conversation

elyscape
Copy link
Contributor

@elyscape elyscape commented Apr 12, 2016

This deals with #8471 by adding a duplicate entry to the map files that also includes the port, which will match values of base that result when a client includes the port number in the Host header.

This deals with #8471 by removing ports from the Host header send by clients.

Fixes #8471.

elyscape added a commit to elyscape/origin-router-regexp that referenced this pull request Apr 20, 2016
This will ensure things don't break if openshift/origin#8490 is merged.
@knobunc
Copy link
Contributor

knobunc commented Apr 21, 2016

LGTM, @ramr PTAL

@smarterclayton
Copy link
Contributor

HAProxy doesn't handle this automatically?

@@ -254,6 +254,7 @@ backend be_secure_{{$cfgIdx}}
{{ range $idx, $cfg := $serviceUnit.ServiceAliasConfigs }}
{{ if and (ne $cfg.Host "") (eq $cfg.TLSTermination "")}}
{{$cfg.Host}}{{$cfg.Path}} {{$idx}}
{{$cfg.Host}}{{env "ROUTER_SERVICE_HTTP_PORT" "80"}}{{$cfg.Path}} {{$idx}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing separator between host and port. {{$cfg.Host}}:{{env "ROUTER_SERVICE_HTTP_PORT" "80"}}{{$cfg.Path}}.

@ramr
Copy link
Contributor

ramr commented Apr 21, 2016

So one issue I can see here (apart from the missing separator between host and port) is that we are adding another rule for the host:port combination - effectively doubling the number of rules. This would have a bigger performance impact as the number of routes/rules gets larger.
We use this to search the map using the base uri ala:
use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be. map)] or in other ACLs checking specific maps.
Probably better, if we can use a single rule with the port always added and have the matcher build the host:port/path in lieu of using base in the example above.

@smarterclayton
Copy link
Contributor

I would expect HAProxy to have some normalization possible.

On Wed, Apr 20, 2016 at 10:11 PM, Ram Ranganathan [email protected]
wrote:

So one issue I can see here (apart from the missing separator between host
and port) is that we are adding another rule for the host:port
combination - effectively doubling the number of rules. This would have a
bigger performance impact as the number of routes/rules gets larger.
We use this to search the map using the base uri ala:
use_backend
be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.
map)] or in other ACLs checking specific maps.
Probably better, if we can use a single rule with the port always added
and have the matcher build the host:port/path in lieu of using base in
the example above.


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
#8490 (comment)

@ramr
Copy link
Contributor

ramr commented Apr 21, 2016

@smarterclayton currently the base fetch method is used for matching - ala use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be. map)].
Base is basically hdr(host) + path which fails when the Host header contains a port number.

@smarterclayton
Copy link
Contributor

Is there no specific host method in haproxy that performs normalization?

On Wed, Apr 20, 2016 at 10:20 PM, Ram Ranganathan [email protected]
wrote:

@smarterclayton https://github.com/smarterclayton currently the base
fetch method is used for matching - ala use_backend
be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be. map)].
Base is basically hdr(host) + path which fails when the Host header
contains a port number.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#8490 (comment)

@elyscape
Copy link
Contributor Author

Good catch on the missing port separator. Fixed. I agree that finding a way to normalize base would be better from a performance standpoint but, based on some experiments I performed, it looks like altering the Host header does not update base. I could be mistaken, though.

@elyscape elyscape force-pushed the 8471-path-route-port branch from 310dc6c to fa4ca04 Compare April 21, 2016 04:38
@ramr
Copy link
Contributor

ramr commented Apr 21, 2016

Hmm, gave this some thought - we might be able to simplify this to use the existing map file entry. So what we really want here is that base should contain just the equivalent of hdr(host)[path] with the port number stripped off. So we could set a new custom header to that value and use it in the match.

For a first pass, I'd suggest verifying that if you do something like add:
http-request replace-value Host (.*):.* \1
in the public and public_ssl backends - does that solves your issue. I suspect it should as its just the match with the port number that is failing in your case. That above directive is basically stripping out the port number from the host header.

Now, we could very well use that as as solution - because the XFF headers to the backend would anyway indicate what port the request arrived on + the host. Other alternatives here would be:

  1. A wee bit less riskier would be to set a custom header with the equivalent of hdr(host)[path] and the port number stripped off. And use the custom header for matching in the maps.
  2. Alternatively have a knob for tuning that adds that http-request replace-value Host (.*):.* \1 to the generated haproxy config.
  3. I guess we could also push and pop the original Host header after we do the match - push in the frontend and restore it in the backend.

I'll mull on this a wee bit and see if any other ideas crop up.

Edited typos

@elyscape
Copy link
Contributor Author

For a first pass, I'd suggest verifying that if you do something like add:
http-request replace-value Host (.*):.* \1

I'm pretty sure I tried this and it didn't work, but I'll try it again.

@ramr
Copy link
Contributor

ramr commented Apr 22, 2016

@elyscape I tried that this am and it worked for me - so give it a whirl.

@elyscape
Copy link
Contributor Author

elyscape commented Apr 25, 2016

@ramr I just tried that again and it didn't work for me. Where in the frontend public and frontend public_ssl sections did you put it? I put it immediately after the tcp-request lines.

@ramr
Copy link
Contributor

ramr commented Apr 25, 2016

@elyscape
Copy link
Contributor Author

It doesn't work regardless of whether I put http-request after tcp-request or, as in the gist, before tcp-request.

@elyscape
Copy link
Contributor Author

As far as I can tell, http-request replace-value Host and http-request replace-header Host do not affect the value of base.

@elyscape
Copy link
Contributor Author

This is interesting. That does change the value of base, but only via connections that do not use SSL/TLS.

@elyscape
Copy link
Contributor Author

Looks like the problem was that the public_ssl frontend didn't have mode http set, which was intentional. It doesn't use path maps, though, instead sending to either be_tcp_$ROUTE, be_sni, or be_no_sni depending on if the route is set to just pass through traffic and whether it uses SNI. Passthrough routes don't support path-based routing, so we don't need to worry about them. Backends be_sni and be_no_sni simply redirect to frontends fe_sni and fe_no_sni, which do set mode http. Putting the http-request replace-value Host line in those makes this work. I'll update my pull request to make use of this instead.

@elyscape elyscape force-pushed the 8471-path-route-port branch from fa4ca04 to 7a73124 Compare April 25, 2016 23:40
@elyscape
Copy link
Contributor Author

Updated to use http-request replace-header Host instead.

@elyscape elyscape force-pushed the 8471-path-route-port branch from 7a73124 to 429f3d0 Compare April 25, 2016 23:55
@elyscape
Copy link
Contributor Author

I also went ahead and rebased it onto master.

@ramr
Copy link
Contributor

ramr commented Apr 26, 2016

@elyscape thanks, those changes look good. An integration test for this would be good - something in this test: https://github.com/openshift/origin/blob/master/test/integration/router_test.go#L390

The simplest check might be to do something similar to this test at: https://github.com/openshift/origin/blob/master/test/integration/router_test.go#L610

if err := waitForRoute(routeAddress, "www.example.com", "http", nil, tr.HelloPodAlternate); err != nil {   
        t.Fatalf("unexpected response: %q", err)   
}  

The fourth parameter to the waitForRoute call is the http headers - so just a check that sending a
request with headers ala Host: www.example.com:80 behaves as expected.

@elyscape
Copy link
Contributor Author

Added some integration tests. Please review. I'll squash the commits if everything checks out.

@@ -464,6 +464,10 @@ func TestRouterPathSpecificity(t *testing.T) {
if _, err := getRoute(routeAddress, "www.example.com", "http", nil, ""); err != ErrUnavailable {
t.Fatalf("unexpected response: %q", err)
}
//ensure you can curl path with port in Host header
if err := waitForRoute(routeTestAddress, "www.example.com:80", "http", nil, tr.HelloPodPath); err != 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.

The second argument here gets put into the Request.Host field, which is what determines the Host header.

@elyscape
Copy link
Contributor Author

As a side note, I noticed that the router's path-based integration tests only really go in-depth via HTTP. Since the issue can manifest on HTTP without manifesting on HTTPS and vice versa, it would be a good idea to add testing on that as well. Additionally, there is no path-based route testing for WebSockets.

I'll add these in a separate pull request once this is merged.

@@ -526,6 +533,9 @@ func TestRouterPathSpecificity(t *testing.T) {
if err := waitForRoute(routeAddress, "www.example.com", "http", nil, tr.HelloPod); err != nil {
t.Fatalf("unexpected response: %q", err)
}
if err := waitForRoute(routeTestAddress, "www.example.com:80", "http", nil, tr.HelloPodPath); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

expected response should be tr.HelloPod - same as above and not tr.HelloPodPath.

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'm not sure I understand. These new lines should be making requests against the path route. That's why it's testing against routeTestAddress instead of routeAddress. It looks like I should probably be using getRoute() instead of waitForRoute(), but that's a minor thing that I can fix.

Copy link
Contributor

Choose a reason for hiding this comment

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

my bad - its late ... ignore my comments - I glossed over the routeTestAddress part.

@ramr
Copy link
Contributor

ramr commented Apr 27, 2016

@elyscape some comments re: the tests. Running them for you here so you can see the results.

@ramr
Copy link
Contributor

ramr commented Apr 27, 2016

[test]

This is done to support path-based routing for clients that include the
port number in the Host header. While unusual, doing so isn't against
the HTTP spec, and some clients force it.

Fixes openshift#8471.
@elyscape elyscape force-pushed the 8471-path-route-port branch from 41b80f5 to 7b25ad6 Compare April 27, 2016 07:08
@@ -610,6 +626,9 @@ func TestRouterPathSpecificity(t *testing.T) {
if err := waitForRoute(routeAddress, "www.example.com", "http", nil, tr.HelloPodAlternate); err != nil {
t.Fatalf("unexpected response: %q", err)
}
if err := waitForRoute(routeTestAddress, "www.example.com:80", "http", nil, tr.HelloPodPath); err != 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.

Aha, I see. This needed to be tr.HelloPodAlternate. Fixed.

@elyscape
Copy link
Contributor Author

@ramr, the tests should be fixed now.

@ramr
Copy link
Contributor

ramr commented Apr 27, 2016

re[test]

@ramr
Copy link
Contributor

ramr commented Apr 27, 2016

[test]

@openshift-bot
Copy link
Contributor

Evaluated for origin test up to 7b25ad6

@openshift-bot
Copy link
Contributor

continuous-integration/openshift-jenkins/test SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pr_origin/3357/)

@ramr
Copy link
Contributor

ramr commented Apr 27, 2016

changes LGTM
@smarterclayton / @rajatchopra - merge magic please.

@rajatchopra
Copy link
Contributor

LGTM [merge]

@openshift-bot
Copy link
Contributor

openshift-bot commented Apr 28, 2016

continuous-integration/openshift-jenkins/merge SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pr_origin/3357/) (Image: devenv-rhel7_4057)

@openshift-bot
Copy link
Contributor

Evaluated for origin merge up to 7b25ad6

@openshift-bot openshift-bot merged commit 79b4182 into openshift:master Apr 28, 2016
@elyscape elyscape deleted the 8471-path-route-port branch April 28, 2016 20:34
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

Successfully merging this pull request may close these issues.

6 participants