Append string to backend name after using field to get substring of host header?

We have HAproxy 2.1.11 configured so that it handles 80/443 and routes to backends based on the subdomain (i.e. test.some.example.org goes to the backend named test), and want to extend this to doing range based (i.e. 3000-4000) rather than explicit ports as well.

We can’t directly just add bind *:3000-4000 ssl crt ... to our existing frontend definition, because we terminate SSL and both 80/443 get proxied to port 80 on the backends, and we don’t want to map 3000-4000 to port 80 too, but to 3000-4000.

So, I have basically copy-pasted the existing setup, and change the bind to the range, and set up the backend with same names but with _range appended to the end (i.e. there would be bother a test and test_range backend with the above example).

The problem I’m having is figuring out how to append _range to the backend name that we’re determining by using field to extract the subdomain from the host header.

Here’s the relevant bit of the original config:

frontend main
  bind *:80
  bind *:443 ssl crt /etc/haproxy/ssl/
  # Detect hyphens in subdomain (technically, anywhere, but we'd only see them on subdomain)
  acl rule_hyphen hdr(host) -m sub -- -
  # Set X-Forwarded-Proto according to whether SSL was used
  http-request set-header X-Forwarded-Proto https if { ssl_fc }
  http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
  # If a hyphen is present in subdomain, split on hyphen
  use_backend %[req.hdr(host),field(1,-)] if rule_hyphen
  # Split on .
  use_backend %[req.hdr(host),field(-4,.)]
  # Backend not found, default to the wild backend
  default_backend wild

Basically, for the use_backend lines, I want to have whatever the %[req.hdr(host),field(...)]... gets determined to be, have _range appended to the end of it. I.e. test.some.example.org is test after the host field extraction, and we need it to become test_range.

I have tried the following:
use_backend %[req.hdr(host),field(-4,.)]_range
use_backend concat(%[req.hdr(host),field(-4,.)],_range)

I’m not sure what else to try, the documentation doesn’t seem to really cover this use case with it’s examples.

Imho the syntax needs to be this:

use_backend %[concat((req.hdr(host),field(-4,.)),_range)]
  1. You trigger dynamic backend selection with %[ ], this needs to be on the outside, not somewhere on the middle
  2. req.hdr(host),field(-4,.) needs to be within brackets. otherwise you are passing 3 variables to concat (which will lead to a totally different results), as opposed to extract a specific field out of the host header and then appending a static string.

However I strongly suggest that you set a http response header to the restult of this and manually check it. When you got a string modifications done exactly like you want, only then you retry with use_backend. Otherwise you don’t know what the actual result looks like.

1 Like

Also see:

failed to parse use_backend rule '%[concat((req.hdr(host),field(1,-)),_range)]' : failed to parse sample expression <concat((req.hdr(host),field(1,-)),_range)> : unknown fetch method 'concat'

I also tried %[concat([... since you said req.hdr should be in brackets, but it gave the same error. I guess concat can’t be used there?

However, at your suggestion with testing with headers I did find that I could set a header like I originally tried, then reference that header for backend selection:

  http-request add-header X-Backend %[req.hdr(host),field(1,-)]_range if rule_hyphen
  http-request add-header X-Backend %[req.hdr(host),field(-4,.)]_range
  use_backend %[req.hdr(x-backend)]
  default_backend wild_range

So I guess that works, just annoying to have to do the extra step. And without having to remove it in every backend, I’ll end up with useless extra header to all requests, but that’s just me being a bit OCD, it won’t actually impact anything. I tried removing the header after use_backend but I guess the del-header is resolved before the switch to the backend (makes sense, otherwise presumably the del-header would never be reached), so it ends up using the default_backend because now the check for the header gets an empty string, fails to find the backend, and uses the default.

You can use a request variable as opposed to http headers:

http-request set-var-fmt(req.backend) %[req.hdr(host),field(-4,.)]_range
use_backend %[var(req.backend)]