istio: NodePort port incorrectly used in Host header for request routing

Kuberntes: 1.11.1 on minikube v0.28.2 Istio: 1.0.2

I’ve been using Istio on minikube with the ingress gateway using a service of type NodePort.

Most examples in the docs use host * in both their gateways and outward facing virtual services. This works fine, but the moment you want to start trying more complex things and need to actually start routing requests on host name, things fall apart.

Consider the following gateway:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: example
  namespace: example
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - hosts:
    - '*'
    port:
      number: 80
      name: http
      protocol: HTTP

And the following virtual service:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: www
  namespace: example
spec:
  hosts:
  - www.example.com
  gateways:
  - example
  http:
  - route:
    - destination:
        host: www

Now suppose that in my local /etc/hosts file, I added the following entry (192.168.99.100 is the IP of my minikube vm):

192.168.99.100 www.example.com

I now get the node port for the ingress gateway:

$ kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}'
31380

Now I try and open http://www.example.com:31380 in my browser.

This does not work. The reason why is clear. Ingress gateway access logs show entries like the following:

[2018-09-18T04:06:14.693Z] "GET / HTTP/1.1" 404 NR 0 0 0 - "172.17.0.1" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" "44659f89-1292-9c6e-8c74-3217db2fd088" "www.example.com:31380" "-"

The host is being, incorrectly, imo, determined to be www.example.com:31380.

I’ll explain why I believe this is incorrect.

By RFC, the Host header sent by a user agent (e.g. browser) may contain trailing port information. In the absence of this, port 80 is assumed for URLs using HTTP as the protocol and 443 is assumed for URLs using HTTPS as the protocol. All browsers that I have checked, omit 80 and 443, respectively for those circumstances, but otherwise do include the port number.

In this case, I can confirm that my browser sends the Host header with the value www.example.com:31380.

This being the case, it seems unsurprising that the gateway sees the Host header as www.example.com:31380– that is, after all, what it was sent… however

imo, it’s wrong for the gateway to make routing decisions on the basis of this value. Why? Because the request was received on the gateway’s port 80. i.e. The fact that the user agent requested www.example.com:31380 is of no consequence once the gateway has received the request on port 80. imo, it should interpret the Host header as <host:receiving port>-- www.example.com:80 in this case-- and then, by established convention, drop the :80 or :443 if applicable. Here, this would result in www.example.com. This request would be routed correctly.

If there’s any doubt that the behavior I am advocating for here is sensible, please consider that this is how Nginx works.

In Nginx, I have modified the log format as follows:

log_format  main  '$remote_addr !!! $host !!! $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

I’m running this in Kubernetes, with a service of type NodePort– port 30399

With the following entry in my local /etc/hosts:

192.168.99.100 www.foo.com

And a request from my browser to http://www.foo.com:30399

I see access logs such as this:

172.17.0.1 !!! www.foo.com !!! [18/Sep/2018:16:35:28 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" "-"

Note that Nginx sensibly disregarded the :30399.

Last, I should acknowledge that there are various spots in the Istio documentation that point out the difficulties of using Istio on minkube and driving traffic through a browser-- where headers aren’t as easily manipulated as they are with curl. The suggested workaround is to only use * as the host in both gateways and virtual services. The problem with this workaround, of course, is that its far too constraining for anyone to attempt anything non-trivial with Istio on minikube…

That difficulty and the need for the very constraining workaround could be remediated by correcting this bug.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 41 (21 by maintainers)

Most upvoted comments

I have checked this issue on the istio version (1.4.6, 1.4.7, 1.5.2) and it’s still there. So if you are using a custom ports to connect to service - envoy will receive header as domain:port and it will cause a problem that envoy can not match expected domain name and received domain:port. As a temporary workaround - you can create a nginx proxy with config like:

## tcp LB  and SSL passthrough for backend ##
stream {
    upstream apigee {
        server K8S-NODE-IP:NODEPORT;
    }

    log_format basic '$remote_addr [$time_local] '
                 '$protocol $status $bytes_sent $bytes_received '
                 '$session_time "$upstream_addr" '
                 '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';

    access_log /var/log/nginx/apigee_access.log basic;
    error_log  /var/log/nginx/apigee_error.log;

    server {
        listen 443;
        proxy_pass apigee;
        proxy_next_upstream on;
    }
}

http://nginx.org/en/docs/stream/ngx_stream_core_module.html

curl -v http://IP:PORT/hello
*   Trying IP...
* TCP_NODELAY set
* Connected to IP (IP) port NODEPORT (#0)
> GET /hello HTTP/1.1
> Host: IP:NODEPORT
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 60
< server: istio-envoy
< date: Tue, 28 Apr 2020 10:41:36 GMT
< x-envoy-upstream-service-time: 176
<
Hello version: v1, instance: helloworld-v1-578dd69f69-6srxc
* Connection #0 to host IP left intact
* Closing connection 0
curl -v -H "Host:helloworld.test.com" http://helloworld.test.com:NODEPORT/hello
*   Trying IP...
* TCP_NODELAY set
* Connected to helloworld.test.com (IP) port NODEPORT (#0)
> GET /hello HTTP/1.1
> Host:helloworld.test.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 60
< server: istio-envoy
< date: Tue, 28 Apr 2020 10:45:10 GMT
< x-envoy-upstream-service-time: 165
<
Hello version: v2, instance: helloworld-v2-776f74c475-q7p7q
* Connection #0 to host helloworld.test.com left intact
* Closing connection 0
istioctl version
client version: 1.5.0
control plane version: 1.5.2
data plane version: 1.5.2 (5 proxies)
Istio 1.4.7:
envoy --version envoy  version: f777dd13e28edc4ff394e98cd82c82ca4a2bcc71/1.12.0/Clean/RELEASE/BoringSSL

Istio 1.5.2:
envoy --version
envoy  version: 54fd05bc1e3e59a155651e5c84e4138d849e8d58/1.13.1-dev/Clean/RELEASE/BoringSSL
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: helloworld-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    #- "*"
    - "helloworld.test.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: helloworld
spec:
  hosts:
  - "helloworld.test.com"
  #- "*"
  gateways:
  - helloworld-gateway
  http:
  - match:
    - uri:
        exact: /hello
    route:
    - destination:
        host: helloworld
        port:
          number: 5000

@yann-soubeyrand yes, that is correct. We actually ignore the port in some cases, by domain:*, but if you have a wildcard in the domain like *.example.com:* will not work since envoy only supports a single wildcard.

Noting down here that in case this blocks you from using istio ingress on a non 80/443 port (maybe internally in your company network), you can do the following:

As a result, your requests will have <host>:31400 on the host header and this will match the ingress configuration. Moreover, the ingress configuration will also include <host> without the port, so you can also set your external Load Balancer to use this port as a backend if you like.