Routing via custom HTTP headers and DTAB

Hey guys! Have a question about routing using headers.

Right now I’ve configured a dtab based on a marathon namer and a /$/io.buoyant.http.domainToPathPfx rewriting namer. I do want to save this config, but add an identifier - or any other module/block - to route requests using a custom header key, let’s call it x-app-route. I want Linkerd to extract the value of this header and somehow route it to the (Marathon) namer in the dtab, which will resolve it to the right bound name.

I’m not sure what’s the proper way of achieving this. Right now I’ve tried using an identifier from kind io.l5d.header.token, with a token configured for x-app-route already mentioned. My dtab looks something like that:

    /domain/<DOMAIN_SEGMENTS>    => /#/io.l5d.marathon;
    /app-route/<DOMAIN_SEGMENTS> => /#/io.l5d.marathon;
    /host                        => /$/io.buoyant.http.domainToPathPfx/domain;
    /host                        => /$/io.buoyant.http.domainToPathPfx/app-route/x-app-route;
    /svc                         => /host;

So I was hoping that Linkerd would take the header token (key=x-app-route, value=bound name) using the identifier and resolve it to /domain/name/and/segments/bound-name using the dtab posted above.

Was I wrong? Is there a smarter way of doing it? Would be glad to know :slightly_smiling_face: Thanks!

1 Like

We chatted a bit on the linkerd slack but it would be great to see a screenshot of your dtab playground UI to see how the names are delegating right now. It would also be helpful if you could provide an example of how you would like x-app-route header values to map to marathon names.

Hi Alex, thanks so much for your help!

Let’s say we have a service sending a request in our cluster. Its request looks something like:

HTTP_HEADER
Host: app1.folder2.dcos.exmaple.com

This is our basic dtab configuration:

/domain/example/com => /#/io.l5d.marathon;
/host => /$/io.buoyant.http.domainToPathPfx/domain;
/svc => /host;

The actual “domain name” is the part after the word “domain” in the first rule. In the default HTTP Host header, the “domain name” is written after the app name.

Linkerd takes the default HTTP Host header value and rewrites it to a path similar to /domain/example/com/app1 to continue resolution, since we’re using the domainToPathPfx rewriting namer. And indeed, this resolution path is passed to the (Marathon) namer and get bound to an IP:PORT pattern.

But now we want to change this. The new requests will look like this:
Request #1:

HTTP_HEADER
Host: service.dcos.example.com
x-app-route: app1.folder2

Request #2:

HTTP_HEADER
Host: service.dcos.exmaple.com
x-app-route: app2.folder3

We need:
(1) Dtab to use the Marathon namer based only on the default HTTP Host header, which will contain just the string service.dcos.example.com for all services;
(2) take the right service name from the custom x-app-route header value and continue resolution using the Marathon namer.

Ideally we would like the new (resolve based on custom header value) functionality to work alongside the existing functionality (resolve based on domain prefix).

I think your dtab for (1) is mostly correct (just needs com/example instead of example/com)

/domain/com/example=>/#/io.l5d.marathon;
/host=>/$/io.buoyant.http.domainToPathPfx/domain;
/svc=>/host

For (2) you would want to use the header token identifier and a similar dtab

/domain=>/#/io.l5d.marathon;
/host=>/$/io.buoyant.http.domainToPathPfx/domain;
/svc=>/host

But I’m not sure what you mean when you say they should work alongside each other. Do you want to route based on the x-app-route header if it exists and fall back to the host header otherwise?

To do that, you could specify both identifiers so that it attempts to use the x-app-route header if it exists and falls back to the host header otherwise:

identifier:
- kind: io.l5d.header.token
  header: x-app-route
- kind: io.l5d.header.token
  header: host

and use a combined dtab:

/domain=>/#/io.l5d.marathon;
/domain/com/example=>/#/io.l5d.marathon;
/host=>/$/io.buoyant.http.domainToPathPfx/domain;
/svc=>/host
1 Like

We want Linkerd to use the custom x-app-route header to understand which
service should get the request; and the default Host header to resolve the
domain name. But both of these resolutions will be made using the dtab and
Marathon namer.

I think that your configuration should work, just not sure if we would get
full resolution out of the custom header, since it will only contain the
"app1.folder2" segment (DC/OS ID), and nothing concerning the domain name.

If I’ll have two identifiers and Linkerd will see only the x-app-route
header containing just the DC/OS ID, how would it add the domain name to
the bound/client name (after resolution)?

When you say that Linkerd should use the Host header to resolve the domain name… I’m not sure exactly what you mean. In configuration (2) above, Linkerd will route requests with x-app-route: app1.folder2 to the marathon app called app1 in the folder2 group. Linkerd will leave the Host header unchanged so the request will still have service.dcos.example.com as the Host header.

Thank you for you suggestions and help :slight_smile:

I’ve added the identifier and dtab configurations you’ve suggested. I’ve deployed a test version of Linekrd with ports 7770, 4130, 4131 for admin, outgoing and incoming and tried running some curl commands. I’m trying to reach a service which called service1.folder2 and has an endpoint called /v1/health.

When I’m running locally:
curl -H 'x-app-route: service1.folder2' -H 'Host: service1.folder2.dcos.domain.name' 127.1:4130/v1/health
I indeed get a proper respose.

But when I’m running just:
curl -H 'x-app-route: service1.folder2' -H 'Host: dcos.domain.name' 127.1:4130/v1/health
I get only:

No hosts are available for /svc/dcos.domain.name, Dtab.base=[<DTAB_CONFIG>], Dtab.local=[]. Remote Info: Not Available

So we’re missing the point, because we’re not able to resolve based on a header for service name and another header for domain name. Also, you can see from the response that probably the only header Linkerd uses is the default HTTP Host header, because it receives the service name from x-app-route header but kind of ignores it.

Can you share your Linkerd config? If you have your interpreter configured like:

identifier:
- kind: io.l5d.header.token
  header: x-app-route
- kind: io.l5d.header.token
  header: host

then it should use the x-app-route header if it exists.

Here it is :slight_smile:
Changed the dtab, of course, since don’t want all these names on the internet.
Wherever I write something like /DOMAIN/SEGMENTS - it’s just our domain names. The word “domain” not in capital letters in the dtab is just the string domain. I believe we have your identifier configuration though.

---
admin:
  ip: 0.0.0.0
  port: 9990

routers:
- protocol: http
  servers:
  - port: 4140
    ip: 0.0.0.0
  dtab: >-
    /domain/DOMAIN/SEGMENTS => /#/io.l5d.marathon;
    /domain/DOMAIN/SEGMENTS => /#/io.l5d.marathon;
    /domain/DOMAIN/SEGMENTS => /domain/DOMAIN/SEGMENTS;
    /host                   => /$/io.buoyant.http.domainToPathPfx/domain;
    /svc                    => /host;
  label: outgoing
  bindingTimeoutMs: 15000
- protocol: http
  servers:
  - port: 4141
    ip: 0.0.0.0
  label: incoming
  interpreter:
    kind: default
    transformers:
    - kind: io.l5d.localhost
  identifier:
  - kind: io.l5d.header.token
    header: x-app-route
  - kind: io.l5d.header.token
    header: host

telemetry:
- kind: io.l5d.influxdb

namers:
- kind: io.l5d.marathon
  prefix: /io.l5d.marathon
  host: marathon.mesos
  port: 8080
  useHealthCheck: true

Hi Alex -

I think that what I’ve failed to explain is that we don’t want to use one or another header - but both, together.

Maybe it’s better to think about it as concatenation: take the app1.folder2 segment from x-app-route header, but the domain segments from the default HTTP Host header (again, in the default header we will put a constant string like service.dcos.example.com for all our services).

That way, Linkerd should get a clear picture of:

  • A) Which namer to use, from Host;
  • B) App’s full name for resolvent, from x-app-route.

In principal, as I see it, that way this command could work: curl -H 'x-app-route: service1.folder2' -H 'Host: service.dcos.domain.name' 127.1:4130/v1/health (Let’s say the address is a working endpoint).

So sorry if I didn’t explain myself properly, I’ve needed time to comprehend better as well.

What do you think?

Hi @jacobgo. Thanks for providing your config file, it’s helpful.

I see a two things that should be added to the outgoing router block in your config file. Assuming you intend to route like this:
request -> outgoing linkerd -> incoming linkerd -> destination

  1. add an io.l5d.port transformer to instruct the outgoing linkerd to route to the incoming linkerd on the destination host.
  2. add an identifier block to instruct the outgoing linkerd to route based on this information.
routers:
- protocol: http
  servers:
  - port: 4140

  ...

  label: outgoing
  interpreter:
    kind: default
    transformers:
    # tranform all outgoing requests to deliver to incoming linkerd port 4141
    - kind: io.l5d.port
      port: 4141
  identifier:
  - kind: io.l5d.header.token
    header: x-app-route
  - kind: io.l5d.header.token
    header: host