Description
Bug description
This is more of a question than a bug. We (Arrikto) noticed the following behavior in Istio:
incoming connections from declared service ports are proxied through 127.0.0.1
but requests through undeclared ports are proxied through 127.0.0.6
. In the code, I see this refers to something called InboundPassthroughClusterIpv4
. @dntosas then found this interesting comment: https://github.com/istio/istio/pull/15906/files#r308491044
So it seems like some "magic" was necessary, but I don't understand why. Possible reasons we could think of are so that the upstream app knows if the downstream connection is through a declared or undeclared port. Or maybe it has nothing to do with that. cc'ing @howardjohn @lambdai based on the discussion in the review comments.
[x] Docs
[ ] Installation
[ ] Networking
[ ] Performance and Scalability
[ ] Extensions and Telemetry
[ ] Security
[ ] Test and Release
[ ] User Experience
[ ] Developer Infrastructure
[ ] Upgrade
Steps to reproduce the bug
- Setup a server and client Pod with a sidecar.
- Create a k8s service and declare port 9090 in the server.
- Start a netcat server in the server pod on port 9090
- Connect from client and see that the server sidecar binds to
127.0.0.1
for the sidecar<->netcat socket. - Start a netcat server in the server pod on port 9091 (undeclared).
- Connect from client and see that the server sidecar binds to
127.0.0.6
for the sidecar<->netcat socket.
Version (include the output of istioctl version --remote
and kubectl version --short
and helm version
if you used Helm)
Istio:
client version: 1.5.7 cluster-local-gateway version: ingressgateway version: 1.5.7 pilot version: 1.5.7 data plane version: 1.5.7 (16 proxies)
Kubernetes:
Client Version: v1.16.4
Server Version: v1.16.15
How was Istio installed?
Environment where bug was observed (cloud vendor, OS, etc)
Minikube
Activity
lambdai commentedon Dec 16, 2020
Thank you for the description!
Due to the traffic capture, the ultimate tcp connection to the service application is established by the proxy(envoy), so the peer ip seen by the service application can never be the client pod ip anyway.
Currently the only way to obtain the original client pod ip is to bypass the capture.
I can add the document somewhere in the wiki or FAQ
yanniszark commentedon Dec 16, 2020
@lambdai thanks for your answer! But I don't think it explains the reason for choosing that address.
Agreed, because the TCP traffic is proxied, the server can't see the client's original address. But WHY choose
127.0.0.6
? And why use127.0.0.6
when proxying an undeclared port, but127.0.0.1
when proxying a declared port?To reproduce what I'm saying:
nc -vlp 8081
ss -tunap
. You'll see that Envoy binds to127.0.0.1
for the sidecar<->server connection.nc -vlp 8082
ss -tunap
. You'll see that Envoy binds to127.0.0.6
for the sidecar<->server connection.Why does this distinction exist? In the linked PR, @howardjohn claimed there was some complex logic at play but it seems it was never documented. @howardjohn perhaps you recall what it was?
lambdai commentedon Dec 16, 2020
127.0.0.6
is chosen since it is a legit and free local ipv4 address in linux ipv4 stack. And.6
slightly matches 15006
. I admit there might be alternatives.Great experiment! The short answer is config simplifity.
There is some traffic that could lead to an infinite loop and there is traffic that won't cause an infinite loop.
The former traffic is the traffic hitting passthrough filter chain, which istio does not define service on that pod port with pod ip address. The 127.0.0.6 is used to mark the traffic as "inbound traffic" and will never hit outbound iptables rules.
The latter traffic which hits k8s service target port has the dest address 127.0.0.1 and won't cause an infinite loop. Binding 127.0.0.6 doesn't bring benefits so 127.0.0.1 is automatically chosen by the linux kernel. Theoretically, we can bind 127.0.0.6 as well at the cost of another syscall and some additional envoy config.
yanniszark commentedon Dec 18, 2020
@lambdai thanks, I think I almost got it, but there is an important piece of the puzzle missing. The address that Envoy proxies the tcp connection to.
127.0.0.1
. Envoy (server-sidecar) binds to127.0.0.1
and the server binds to127.0.0.1
as well.127.0.0.6
and the server binds to<pod_ip>
.I understand that the 2nd case would be indistinguishable from other cases (e.g., process in pod trying to talk to server in the same pod through a Service ClusterIP), so Envoy has to bind to a different port in order to recognize and exclude this traffic in IPTables.
So why this distinction in the destination IP? Why proxy to the
<pod_ip>
and not to localhost for undeclared ports? Here is my guess:<pod_ip>
, Istio avoids exposing undeclared ports (servers) that only listen to localhost. So it preserves the secure localhost network.But then my question is: why does Istio require services to bind to localhost or 0.0.0.0 for proxying them (https://istio.io/latest/docs/ops/deployment/requirements/#application-bind-address)? Couldn't Istio proxy traffic for a declared port whose server binds to
<pod_ip>
by using the127.0.0.6
trick and treating all cases the same?Fix inbound traffic routing to Envoy
Fix inbound traffic routing to Envoy
Fix inbound traffic routing to Envoy
yanniszark commentedon Mar 25, 2021
Ping @lambdai. I know it's been some time since this discussion and a lot of things are out of my mental cache (and yours as well probably). I'd love to be able to revisit this sometime soon.
lambdai commentedon Mar 25, 2021
I think that is old security practise: istio use only 127.0.0.1 as endpoint so if your service listens on 127.0.0.1 and you are protected by istio sidecar proxy. Listen on 0.0.0.0 is fine when you want both istio sidecar proxy access and istio sidecar proxy bypass.
This design is to address istio sidecar proxy access to pod ip.
https://docs.google.com/document/d/1j-5_XpeMTnT9mV_8dbSOeU7rfH-5YNtN_JJFZ2mmQ_w/edit#heading=h.xw1gqgyqs5b
8 remaining items