Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Istio中的透明代理问题

22
Jul
2020

Istio中的透明代理问题

By Alex
/ in PaaS
/ tags ServiceMesh
10 Comments
为何需要透明代理

Istio的Sidecar作为一个网络代理,它拦截入站、出站的网络流量。拦截入站流量后,会使用127.0.0.1作为源地址,将流量转发给本地服务进程。本地服务进程看不到真实源IP地址。

很多应用场景下,真实源IP地址是必须的,可能原因包括:

  1. IP地址作为标识的一部分。以ZooKeeper为例,它通过成员的IP地址来验证集群成员身份
  2. IP地址用于网络策略,或者用于审计目的

本文将设置这样的场景:一个启用了Istio Sidecar的Nginx Pod,需要被当前命名空间的另外一个Pod访问。我们将尝试解决Nginx不能看到真实的客户端IP地址的问题。

Envoy的现状

 

目前Envoy已经能够很好的支持IP Transparency了。 它提供了多种机制把真实源地址提供给上游服务。

http.original_src

真实源地址可以通过 x-forwarded-for这样的请求头获取,很多应用都能识别这种请求头。

Envoy还提供了 envoy.filters.http.original_src,此过滤器能够从请求头读取真实源地址,并修改底层TCP连接的源地址。此过滤器还能处理单一下游连接携带来自多个源的HTTP请求的情况。此过滤器的缺点包括:

  1. 下游连接必须正确设置了x-forwarded-for头
  2. 由于连接池方面的限制,会导致些许性能影响
  3. 配置较为复杂,可能需要路由的配合,即使在Sidecar场景(Envoy和上游在同一网络命名空间)下,也需要配置好iptables规则
listener.proxy_protocol

HAProxy代理协议提供了交换连接元数据的机制,这些元数据就包括真实源IP。Envoy通过监听器过滤器 envoy.filters.listener.proxy_protocol支持代理协议。此过滤器的缺点包括:

  1. 上游主机需要支持代理协议
  2. 仅仅支持TCP

该监听器过滤器可以和envoy.filters.listener.original_src联用。

listener.original_src

在受控部署环境下,通过监听器过滤器 envoy.filters.listener.original_src可以把下游连接源地址复制为上游连接的源地址。

这需要使用透明代理,让Envoy直接以下游地址向上游服务发起连接。对于上游服务,没有任何要求。此过滤器的缺点包括:

  1. Envoy要能够获得真实的下游地址
  2. 由于路由方面的限制,可能无法实现
  3. 由于连接池方面的限制,会导致些许性能影响

这个过滤器是让Istio能够解决透明代理问题的途径,回答一下对它的缺点的规避:

  1. Envoy获取真实下游IP地址,也就是入站连接的真实源地址:这可以通过TPROXY拦截模式让Envoy看到真实下游地址
  2. 路由方面的限制:不存在,因为Envoy和上游服务(入站连接需要访问的服务)在一个网络命名空间中,可以软件控制路由
Istio的现状

在两年前就有了关于此问题的Issue:https://github.com/istio/istio/issues/5679。到目前为止,Istio官方没有提供支持透明代理的方案。

关于拦截模式

Istio支持两种拦截模式:

  1. REDIRECT:使用iptables的REDIRECT目标来拦截入站请求,转给Envoy
  2. TPROXY:使用iptables的TPROXY目标来拦截入站请求,转给Envoy

你可以全局的设置默认拦截模式,也可以通过注解 sidecar.istio.io/interceptionMode: TPROXY给某个工作负载单独设置。

需要注意的是TPROXY模式解决的仅仅是Envoy看到的入站连接源IP地址的问题,被代理本地服务看到的地址仍然是127.0.0.1。

下面对比一下两种拦截模式下生成的iptables规则的差异:

TPROXY

mangle表的内容如下:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# iptables -t mangle -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination        
ISTIO_INBOUND  tcp  --  0.0.0.0/0            0.0.0.0/0          
 
Chain INPUT (policy ACCEPT)
target     prot opt source               destination        
 
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination        
 
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination        
 
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination        
 
Chain ISTIO_DIVERT (1 references)
target     prot opt source               destination        
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK set 0x539
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_INBOUND (1 references)
target     prot opt source               destination        
# 不拦截特殊端口
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
# 如果SRC_IP:SRC_PORT:DST_IP:DST_PORT已经建立拦截,则打标记,接受封包
ISTIO_DIVERT  tcp  --  0.0.0.0/0            0.0.0.0/0            socket
# 否则,如果目的地不是127.0.0.1,则重定向给Envoy
ISTIO_TPROXY  tcp  --  0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_TPROXY (1 references)
target     prot opt source               destination        
TPROXY     tcp  --  0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15001 mark 0x539/0xffffffff

可以看到,拦截的逻辑比较简单,仅仅改了 PREROUTING (关注进入的封包)链,增加以下逻辑:

  1. 对于一些特殊端口,不做拦截
  2. 对于已经建立了连接的封包,直接打标记1337并允许通过
  3. 对于目的地址不是127.0.0.1的封包,进行透明代理,发送给Envoy的15001监听器,给封包打标记1337

istio-init在启动工作负载之前会设置策略路由:

Shell
1
2
ip -f inet rule add fwmark 1337 lookup 133
ip -f inet route add local default dev lo table 133

这保证了目的地不是127.0.0.1的封包都会被15001处理,也就是所有外部请求都需要经过Envoy处理,而Envoy向本地被代理服务转发时,会使用目的地址127.0.0.1,不会被拦截。

nat表的内容如下:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 1271 packets, 76260 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain INPUT (policy ACCEPT 1271 packets, 76260 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain OUTPUT (policy ACCEPT 38 packets, 3183 bytes)
pkts bytes target     prot opt in     out     source               destination        
    7   420 ISTIO_OUTPUT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain POSTROUTING (policy ACCEPT 38 packets, 3183 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain ISTIO_IN_REDIRECT (2 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15006
 
Chain ISTIO_OUTPUT (1 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 RETURN     all  --  *      lo      127.0.0.6            0.0.0.0/0
# 下面根据UID进行匹配的规则,应该有问题。因为TPROXY模式下,UID固定为0,因此下面3条规则应该去掉
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner UID match 1337
    2   120 RETURN             all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner UID match 1337
    0     0 RETURN             all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner UID match 1337
# 根据用户不同决定行为,如果GID为1337,意味着是Envoy进程发起的封包,否则是其它进程发起的
# 对于将从lo发出的封包,如果用户是Envoy,目的地址非127.0.0.1的,则重定向到入站虚拟监听器15006
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner GID match 1337
# 对于将从lo发出的封包,如果用户不是Envoy,则允许通过。这保证了本机上的服务可以访问自己
    0     0 RETURN             all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner GID match 1337
# 对于将从非lo发出的封包,如果用户是Envoy,允许通过。这保证了Envoy可以访问外部
    5   300 RETURN             all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner GID match 1337
# 到这里,所有目的地址是127.0.0.1的都被允许
    0     0 RETURN             all  --  *      *       0.0.0.0/0            127.0.0.1          
# 重定向给出站虚拟监听器15001,可能情况:
# 对于将从非lo发出的封包,如果用户不是Envoy,目的地址不是本机,则重定向到出站虚拟监听器15001
#     这保证了服务的对外访问,需要经过Envoy代理
    0     0 ISTIO_REDIRECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_REDIRECT (1 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15001

基于UID匹配的3条规则,我觉得没有意义。原因是TPROXY模式下,运行Envoy的用户是0,而非1337,这个可以从istio-sidecar-injector这个Configmap中看出来:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
allowPrivilegeEscalation: {{ .Values.global.proxy.privileged }}
capabilities:
  {{ if or (eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY`) (eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true`) -}}
  add:
  # 如果是TPROXY模式,增加NET_ADMIN权限
  {{ if eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY` -}}
  - NET_ADMIN
  {{- end }}
  {{ if eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true` -}}
  - NET_BIND_SERVICE
  {{- end }}
  {{- end }}
  drop:
  - ALL
privileged: {{ .Values.global.proxy.privileged }}
readOnlyRootFilesystem: {{ not .Values.global.proxy.enableCoreDump }}
# 总是使用GID 1337运行Envoy
runAsGroup: 1337
fsGroup: 1337
{{ if or (eq (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) `TPROXY`) (eq (annotation .ObjectMeta `sidecar.istio.io/capNetBindService` .Values.global.proxy.capNetBindService) `true`) -}}
# 如果是TPROXY模式,则使用UID 0运行
runAsNonRoot: false
runAsUser: 0
{{- else -}}
# 否则,使用UID 1337运行
runAsNonRoot: true
runAsUser: 1337
{{- end }}

对nat表的更改发生在 OUTPUT 链(关注发出的封包)。核心逻辑:

  1. Envoy通过lo发出的,目的地址不是127.0.0.1的封包,重定向给入站监听器。根据观察,Envoy代理外部请求后,都是从lo发给127.0.0.1的,因此不会匹配此规则
  2. 允许本机的服务访问自身
  3. 服务对外发出的访问,必须经过Envoy

我们仔细分析一下重定向到的15001、15006是什么东西。这些端口是istio-iptables设置的,我们看一下它的帮助:

Shell
1
2
3
4
5
6
7
8
9
10
Script responsible for setting up port forwarding for Istio sidecar.
 
Usage:
  istio-iptables [flags]
 
Flags:
  -p, --envoy-port string             Specify the envoy port to which redirect all TCP traffic
                                          (default $ENVOY_PORT = 15001)
  -z, --inbound-capture-port string   Port to which all inbound TCP traffic to the pod/VM should be redirected to
                                          (default $INBOUND_CAPTURE_PORT = 15006)

看样子15006是需要将所有入站流量重定向到的端口,而在TPROXY中将入站流量都重定向到15001,这两端口如何分工?

这里Dump一下它们的配置。15001的:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// istioctl proxy-config listener nginx-84c66c7fb9-95wrd  --port 15001 -o json
[
    {
        "name": "virtualOutbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15001
            }
        },
        "filterChains": [
            {
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
                            "cluster": "PassthroughCluster",
                        }
                    }
                ]
            }
        ],
        // 使用原始的(被透明代理之前的)连接的目标地址来判断,由哪个监听器(Envoy进程内)来处理连接
        // 如果找不到这样的监听器,则当前监听器来处理,也就是Passthrough
        "useOriginalDst": true,
        // 可以作为TPROXY的目标,和useOriginalDst联用
        "transparent": true,
        // 期望的、相对于Envoy的流量方向
        "trafficDirection": "OUTBOUND"
    }
]
 
// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=PassthroughCluster -o json
{
    "name": "PassthroughCluster",
    "type": "ORIGINAL_DST",
    "connectTimeout": "1s",
    "lbPolicy": "CLUSTER_PROVIDED"
}

可以看到,这个监听器非常简单,仅仅是做穿透处理。从它的名字virtualOutbound和字段trafficDirection上来看,它是用来处理从Pod向外发起的流量的。但是iptables却把入站流量发给它,似乎有些矛盾?

再看看15006的配置:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// istioctl proxy-config listener nginx-84c66c7fb9-95wrd  --port 15001 -o json
[
    {
        "name": "virtualInbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15006
            }
        },
        "filterChains": [
            // 兜底的过滤器链
            {
                "filterChainMatch": {
                    "prefixRanges": [
                        {
                            "addressPrefix": "0.0.0.0",
                            "prefixLen": 0
                        }
                    ]
                },
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "typedConfig": {
                            "statPrefix": "InboundPassthroughClusterIpv4",
                            "cluster": "InboundPassthroughClusterIpv4"
                        }
                    }
                ]
            },
            // 匹配请求本地Nginx进程的流量
            {
                "filterChainMatch": {
                    "destinationPort": 80,
                    "prefixRanges": [
                        {
                            "addressPrefix": "172.27.155.72",
                            "prefixLen": 32
                        }
                    ]
                },
                "filters": [
                    {
                        "name": "envoy.http_connection_manager",
                        "typedConfig": {
                            "statPrefix": "inbound_172.27.155.72_80",
                            "routeConfig": {
                                "name": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
                                "virtualHosts": [
                                    {
                                        "name": "inbound|http|80",
                                        "domains": [
                                            "*"
                                        ],
                                        "routes": [
                                            {
                                                "name": "default",
                                                "route": {
                                                    "cluster": "inbound|80|http|nginx.default.svc.k8s.gmem.cc"
                                                }
                                            }
                                        ]
                                    }
                                ]
                            }
                        }
                    }
                ],
            }
        ],
        "listenerFilters": [
            {
                "name": "envoy.listener.original_dst"
            },
            {
                "name": "envoy.listener.tls_inspector"
            }
        ],
        "transparent": true,
        "trafficDirection": "INBOUND"
    }
]
 
 
// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=InboundPassthroughClusterIpv4 -o json
{
    "name": "InboundPassthroughClusterIpv4",
    "type": "ORIGINAL_DST",
    "connectTimeout": "1s",
    "lbPolicy": "CLUSTER_PROVIDED",
    "upstreamBindConfig": {
        // 绑定新创建上游连接时使用的源地址
        "sourceAddress": {
            "address": "127.0.0.6",
            "portValue": 0
        }
    }
}
 
 
// istioctl proxy-config cluster nginx-84c66c7fb9-95wrd  --fqdn=nginx.default.svc.k8s.gmem.cc  --direction inbound -o json
[
    {
        "name": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
        "type": "STATIC",
        "loadAssignment": {
            "clusterName": "inbound|80|http|nginx.default.svc.k8s.gmem.cc",
            "endpoints": [
                {
                    "lbEndpoints": [
                        {
                            "endpoint": {
                                "address": {
                                    "socketAddress": {
                                        "address": "127.0.0.1",
                                        "portValue": 80
                                    }
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
]

可以看到,这个监听器叫virtualInbound,从它的名字和配置trafficDirection上来看,它是用来处理从外面发给Pod的流量的,它明确的定义了处理连接的集群,127.0.0.1:80,即本地Nginx服务。 

REDIRECT

此模式下,mangle表没有变动,Istio只修改了nat表。入站、出站流量的处理都在此完成:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Chain PREROUTING (policy ACCEPT 23 packets, 1380 bytes)
pkts bytes target     prot opt in     out     source               destination        
   23  1380 ISTIO_INBOUND  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain INPUT (policy ACCEPT 23 packets, 1380 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain OUTPUT (policy ACCEPT 21 packets, 1675 bytes)
pkts bytes target     prot opt in     out     source               destination        
    5   300 ISTIO_OUTPUT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain POSTROUTING (policy ACCEPT 21 packets, 1675 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain ISTIO_INBOUND (1 references)
pkts bytes target     prot opt in     out     source               destination        
# 特殊端口不处理
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    1    60 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
   22  1320 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
# 其它的一律转发给15006
    0     0 ISTIO_IN_REDIRECT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15006
 
Chain ISTIO_OUTPUT (1 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 RETURN     all  --  *      lo      127.0.0.6            0.0.0.0/0          
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner UID match 1337
    0     0 RETURN     all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner UID match 1337
    5   300 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner UID match 1337
    0     0 ISTIO_IN_REDIRECT  all  --  *      lo      0.0.0.0/0           !127.0.0.1            owner GID match 1337
    0     0 RETURN     all  --  *      lo      0.0.0.0/0            0.0.0.0/0            ! owner GID match 1337
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner GID match 1337
    0     0 RETURN     all  --  *      *       0.0.0.0/0            127.0.0.1          
    0     0 ISTIO_REDIRECT  all  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_REDIRECT (1 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15001

可以看到,REDIRECT模式下,处理进入封包的逻辑是完全一样的。 

REDIRECT模式下,将入站流量重定向给15006,这很好理解,因为15006是 virtualInbound监听器嘛。

有何区别

从Nginx的日志上看,不管是REDIRECT还是TPROXY模式,看到的IP都不是真实IP,没有区别。

Envoy访问日志也没有任何区别,至少可以说,在REDIRECT模式下,Envoy也是可以看到真实源IP的:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开始时间                请求方法 原始地址  协议 响应码 响应标记
[2020-04-22T12:52:23.278Z] "GET  /    HTTP/1.1" 200   -
  # 元数据mixer状态  # 上游传输失败原因   接受字节数   发送字节数  耗时 上游访问耗时
  "-"                "-"                 0            612         0 0
  # x-forwarded-for头     User Agent
  "-"                   "curl/7.67.0"
  # 请求ID                                AUTHORITY  上游主机
  "d05b5196-c413-9003-be2a-6b2841efe4e1" "nginx" "127.0.0.1:80"
  # 上游集群
  inbound|80|http|nginx.default.svc.k8s.gmem.cc
  # 访问上游使用的本地地址    下游访问本机使用目的地址   下游远程地址
  127.0.0.1:33024          172.27.155.70:80        172.27.155.74:45326
  # 请求的服务名称                               路由名称
  outbound_.80_._.nginx.default.svc.k8s.gmem.cc default

TPROXY模式下,Envoy也没有使用真实源IP来请求上游集群。

感觉这TPROXY很鸡肋,从https://github.com/istio/istio/issues/5679上看到的,它的价值是: 

Contrary to REDIRECT, TPROXY doesn't perform NAT, and therefore preserves both source and destination IP addresses and ports of inbound connections. One benefit is that the source.ip attributes reported by Mixer for inbound connections will always be correct, unlike when using REDIRECT.

也就是说,TPROXY模式下允许Mixer获得真实源IP地址。

EnvoyFilter

目前Istio支持一种自定义资源EnvoyFilter,使用它,你可以对生成的Envoy配置进行深度定制。比如添加监听器过滤器:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: nginx-original-src
  namespace: default
spec:
  workloadSelector:
    labels:
      app: nginx
  configPatches:
  - applyTo: LISTENER
    match:
      context: SIDECAR_INBOUND
      listener:
        portNumber: 80
    patch:
      operation: MERGE
      value:
        listenerFilters:
        - name: envoy.listener.original_src

像上面这个过滤器,它为入站监听器添加了envoy.listener.original_src这个监听器过滤器。生成的配置如下:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// istioctl proxy-config listener nginx-84c66c7fb9-7mfwz   --port 80 --type http -o json
 
...
        "deprecatedV1": {
            "bindToPort": false
        },
        "listenerFilters": [
            {
                "name": "envoy.listener.tls_inspector"
            },
            {
                "name": "envoy.listener.original_src",
            }
        ],
        "listenerFiltersTimeout": "0.100s",
        "continueOnListenerFiltersTimeout": true,
        "trafficDirection": "INBOUND"
    }
]
如何实现透明代理
原理
典型场景

tproxy-classic

关键点:

  1. 路由器发现目的地址、源地址是REAL_SERVER:80的,且不是来自透明代理的封包,都会路由给透明代理。而不是路由给服务器、客户端
  2. 透明代理能够在非本机IP地址上监听,例如REAL_SERVER:80
  3. 透明代理能够以非本机IP地址发起TCP连接,例如以客户端的IP地址

第一条,可能需要硬件支持。

后面两条,可以由透明代理在软件上支持,相关套接字选项:

  1. IP_FREEBIND:允许绑定非本地的,或者尚不存在的IP地址
  2. IP_TRANSPARENT:在套接字上启用透明代理。该选项运行应用程序绑定非本地地址,并使用这个外部地址来扮演客户端、服务器角色。需要CAP_NET_ADMIN权限才能启用

此外,根据实际需要,“透明度”可以变化:

  1. 如果仅仅想让客户端觉得透明,那么代理可以直接使用自己的IP地址请求服务器。这样服务器看不到客户端真实IP
  2. 如果服务器仅仅需要知道客户端真实IP,不关心真实端口,那么代理可以用客户端地址+任意端口发起请求
  3. 如果需要绝对透明,则代理必须以客户端地址+客户端端口发起请求
Sidecar场景

在Envoy Sidecar部署场景下,情况变的简单,透明代理和服务器位于同一台主机内部,这意味着:

  1. 不需要路由器/网关的配合
  2. 代理请求的目的地址可以从真实服务器地址换为127.0.0.1

可以实现透明代理的通信模型如下:

tproxy-envoy-expected

Istio的问题

在Istio的TPROXY拦截模式下,实际的通信模型如下:

tproxy-envoy-actual

 

差别似乎仅仅是Envoy用127.0.0.1作为源地址,而非客户端真实IP,向服务器发送请求。

使用EnvoyFilter,为virtualOutbound所引用的,80监听器配置一个EnvoyFilter,配置envoy.listener.original_src,可以让Envoy访问服务器时使用真实客户端IP,解决我们的问题吗?

我们参考3.2节配置好EnvoyFilter,然后从外部访问Pod的Nginx服务,很遗憾,并不能正常工作,curl给出的错误是:

Shell
1
upstream connect error or disconnect/reset before headers. reset reason: connection failure

从Envoy访问日志上看:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[2020-04-23T09:18:42.434Z] "GET / HTTP/1.1" 503
# 日志格式取决于配置/版本。通过
#   kubectl exec nginx-tproxy-774fb7958c-t2lnk -c istio-proxy -- curl 0:15000/config_dump | grep .log_format
# 响应标记:
#   LR   本地重置
#   UH   没有健康的上游主机,和503一起发送
#   UF   连接到上游主机时失败,和503一起发送
#   UO   针对上游的访问溢出(断路器触发),和503一起发送
#   NR   没有匹配的路由,和404一起发送
#   URX  请求被拒绝,原因是超过上游的最大重试次数,或者TCP最大连接尝试次数
 
#  上游连接失败     收  发   耗时
   UF "-" "-"      0   91   999   - "-" "curl/7.67.0" "747cdfcb-5d1e-9ac0-8858-33aa1b1eaa4d"
"nginx" "127.0.0.1:80" inbound|80|http|nginx.default.svc.k8s.gmem.cc
# 访问上游使用的本地地址    下游访问本机使用目的地址   下游远程地址
-                       172.27.155.94:80         172.27.155.90:56356 outbound_.80_._.nginx.default.svc.k8s.gmem.cc default

存在如下异常:

  1. 访问上游时使用的源地址为空了
  2. 响应标记UF,耗时999,提示连接不到上游服务器 

为什么连接不到上游服务器?我们尝试通过iptables日志诊断一下。在Nginx的例子里,数据报的特点是,源或目的端口为80,因此增加以下规则:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 删除基于UID匹配的规则,因为TPROXY模式下Envoy的运行用户是0而非1337
iptables -t nat -D ISTIO_OUTPUT 2
iptables -t nat -D ISTIO_OUTPUT 2
iptables -t nat -D ISTIO_OUTPUT 2
 
# 增加入站流量TPROXY规则日志
iptables -t mangle -I ISTIO_INBOUND 5 -p tcp --dport 80 -j LOG --log-prefix "b-tproxy: " --log-tcp-sequence --log-uid
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -j LOG --log-prefix "a-tproxy: " --log-tcp-sequence --log-uid
 
# 在nat表的OUTPUT链,需要增加源、目标端口是80的,分别对应服务向Envoy发出、Envoy向服务发出的封包
iptables -t nat -I ISTIO_OUTPUT 6 -p tcp --dport 80 -j LOG --log-prefix 't-redir-*-*-*-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 6 -p tcp --sport 80 -j LOG --log-prefix 'f-redir-*-*-*-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 5 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-*-*-1: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 5 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-*-*-1: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 4 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-*-*-*-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 4 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-*-*-*-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 3 -p tcp --dport 80 -j LOG --log-prefix 't-rturn-*-l-*-*-!1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 3 -p tcp --sport 80 -j LOG --log-prefix 'f-rturn-*-l-*-*-!1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 2 -p tcp --dport 80 -j LOG --log-prefix 't-inred-*-l-*-!1-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 2 -p tcp --sport 80 -j LOG --log-prefix 'f-inred-*-l-*-!1-1337: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 1 -p tcp --dport 80 -j LOG --log-prefix 't-inred-*-l-6-*: ' --log-tcp-sequence --log-uid
iptables -t nat -I ISTIO_OUTPUT 1 -p tcp --sport 80 -j LOG --log-prefix 'f-inred-*-l-6-*: ' --log-tcp-sequence --log-uid

拦截到的日志:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# [30714.928765] 客户端往POD的连接,首次SYN,TPROXY之前
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=35338   SPT=57252 DPT=80 SEQ=1901693983    SYN  
# 没有出现a-tproxy,说明SYN被TPROXY拦截,发往15001,也就是Envoy
 
# Envoy往Nginx的连接,出站,首次SYN,注意看到SRC是172.27.155.90:44297,和客户端172.27.155.90:57252的IP一致,端口用了新的
# 没有启用EnvoyFilter时是这样:
# inred-*-l-6-*: IN= OUT=lo SRC=127.0.0.1 DST=127.0.0.1 ...
# 可以看到EnvoyFilter达到我们的目的:传递真实源IP
 
t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337
# 由于GID是1337,因此下面的规则匹配,ACCEPT,封包发出去了
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  UID=0 GID=1337
 
# Envoy往Nginx的连接,入站,由于目的地址是127.0.0.1,因此不TPROXY
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=33217   SPT=44297 DPT=80 SEQ=147818454    SYN  
 
 
 
# [30715.971504] 一秒过了,客户端往POD的连接,二次SYN
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=60309   SPT=57258 DPT=80 SEQ=829877388    SYN  
# Envoy往Nginx的连接,出站,二次SYN
t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  UID=0 GID=1337
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=51130   SPT=51761 DPT=80 SEQ=3083321507    SYN  
 
# [30717.046657] 一秒过了,客户端往POD的连接,三次SYN
b-tproxy: IN=eth0 OUT=  SRC=172.27.155.90 DST=172.27.155.108     ID=8963   SPT=57268 DPT=80 SEQ=3705219877    SYN  
t-inred-*-l-6-*: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337
t-inred-*-l-*-!1-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337
t-rturn-*-l-*-*-!1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337
t-rturn-*-*-*-*-1337: IN= OUT=lo SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  UID=0 GID=1337
b-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN  
a-tproxy: IN=lo OUT=  SRC=172.27.155.90 DST=127.0.0.1     ID=12974   SPT=48739 DPT=80 SEQ=2548447881    SYN

可以看到:

  1. 客户端向Pod发请求,被TPROXY给Envoy 15001
  2. Envoy 15001是透明套接字,因此它虽然客户端请求的DPT=80,它也接收并处理了
  3. Envoy执行代理,通过lo向127.0.0.1:80发送请求,注意这里它使用的源地址是客户端地址,这意味着我们的EnvoyFilter起作用了
  4. Envoy代理的请求,通过lo入站,由于目的地址是127.0.0.1,因此不被TPROXY,通过PREROUTING - mangle链

此外,在OUTPUT链中nat表里,好像根据SPT=80无法匹配,所以看不到任何f-开头的日志。此链对于nat表来说,应该是用于做DNAT,Istio生成的规则遵循了这一点,REDIRECT可以看作是一种DNAT。Istio的规则有基于源IP进行匹配的,我基于源端口为何不行,目前不清楚。

换个位置来诊断吧,目前我们已经明确,Envoy接收到请求后,会冒充客户端源IP向localhost:80发请求,此请求已经通过PREROUTING-mangle。它有没有被Nginx接收到?

我们可以在INPUT-mangle上做日志,如果能监控到发往127.0.0.1:80的封包,就可以认定Nginx接收到了,因为整个Iptables中没有设置INPUT链的任何拦截规则。

Shell
1
2
3
iptables -t mangle -I INPUT 1 -p tcp -d 127.0.0.1/32 --dport 80 -j LOG --log-prefix='input-mangle-d80: '
iptables -t nat -I INPUT 1 -p tcp -d 127.0.0.1/32  --dport 80 -j LOG --log-prefix='input-nat-d80: '
iptables -t filter -I INPUT 1 -p tcp -d 127.0.0.1/32  --dport 80 -j LOG --log-prefix='input-filter-d80: '

日志如下:

Shell
1
2
[3612374.269256] input-mangle-d80: IN=lo OUT= SRC=172.27.252.159 DST=127.0.0.1 SPT=40283 DPT=80 SYN
[3612374.269276] input-filter-d80: IN=lo OUT= SRC=172.27.252.159 DST=127.0.0.1 SPT=40283 DPT=80 SYN

nat表仍然没有日志,看样子是在DNAT时,不能使用源端口匹配,SNAT时,不能使用目的端口匹配。

不过从日志上,从lo端口进入的、Envoy仿冒客户端身份发往127.0.0.1:80的封包,的确是通过iptables了。

那么,应该是Nginx没有给出应答。我们需要监控一下源是Nginx,目的是客户端真实IP地址的出站封包的流向: 

Shell
1
2
iptables -t mangle -R POSTROUTING 1 -p tcp -d 172.27.252.159/32 -s 127.0.0.1/32 \
         --sport 80  -j LOG --log-prefix='pr-mangle-to-clientip: '

日志如下:

Shell
1
2
3
pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=54969 ACK SYN URGP=0
pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=50979 ACK SYN URGP=0
pr-mangle-to-clientip: IN= OUT=eth0 SRC=127.0.0.1 DST=172.27.252.159 PROTO=TCP SPT=80 DPT=54969 ACK SYN URGP=0

相似的日志会连续出现很多条。我们可以看到Nginx收到首次握手SYN后,尝试ACK+SYN,但是一致没有收到第三次握手信息…… 原因很明显,出口网卡是eth0,封包发走了,没有返回给Envoy代理。

到这里,问题就算定位完毕了。 

解决方案

我们需要保证,对于Envoy以客户端IP发起的,给Nginx的请求,它的响应能够原路返回。响应的封包具有以下特点:

  1. 源地址(请求封包的目的地址)是 127.0.0.1,因为Envoy总是向127.0.0.1发请求
  2. 目的地址(请求封包的源地址)不是本机地址,因为Envoy发请求时,FREEBIND源地址为客户端IP

我们需要将这种封包,从lo网卡,而非eth0路由出去。 可以使用下面的iptables规则:

Shell
1
2
iptables -t mangle -I OUTPUT 1 -s 127.0.0.1/32 ! -d 127.0.0.1/32 \
    -j MARK --set-xmark 0x539/0xffffffff

再次访问服务,Nginx可以看到真实客户端IP地址了:

Shell
1
2
3
4
# TPORXY mode without envoyfilter
127.0.0.1 - - [24/Apr/2020:02:58:53 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.67.0" "-"
# TPROXY mode + envoyfilter, iptable rule applied
172.27.252.159 - - [24/Apr/2020:05:52:04 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.67.0" "-"

到此为止,问题解决。初步测试,没有发现负面效果,已经将此方案提交社区讨论。

提交社区

此方案已经通过PR https://github.com/istio/istio/pull/23275 合并到上游Istio仓库的master分支(1.7dev),并将自动Cherry Pick到1.6版本。

1.5版本的逻辑稍有不同,仅仅在我Fork的Istio中实现:https://github.com/gmemcc/istio/tree/release-1.5.1-patch,不准备提交到上游Istio仓库。

1.6版本TPROXY问题

在此版本中验证时,发现TPROXY模式损坏,无限循环自我请求。我已经提起Issue:23369。

解决无限循环的方法是把TPROXY目标从15001改为15006。我一直就怀疑为什么要把入站流量重定向给出站监听器15001,现在想想,最初只有一个“虚拟监听器”15001,最近版本的Istio才拆分为virtualInbound(15006)、virtualOutbound(15001)两个,在这个变更过程中,TPROXY相关代码没有跟着改动。

问题23369

解决透明代理源IP的PR 23275并没有达到预期效果,问题原因参考ISSUE 23369。

即使按照上节的方法,将TPROXY目标从15001改为15006,也仅仅能解决无限自我请求的问题。新得到的错误信息是:upstream connect error or disconnect/reset before headers. reset reason: local reset

抓包分析

我们从10.0.0.1发起针对启用了Sidecar的、IP地址为172.27.0.10的请求。可以在Nginx Pod的网络命名空间中看到如下连接信息:

Shell
1
2
3
4
netstat -nt
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      1 10.0.0.1:50829          172.27.0.10:80          SYN_SENT

源地址为10.0.0.1:50829套接字,应该是Envoy发起上游请求时创建的,因为我们配置了监听器过滤器original_src。

但是,这个套接字的状态一直是SYN_SENT,这提示它没有收到答复。结合抓包结果:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 从客户端发起的原始包,源端口39062
10.0.0.1.39062 > 172.27.0.10.80: Flags [S]
# Envoy给的ACK
172.27.0.10.80 > 10.0.0.1.39062: Flags [S.]
# 客户端发起HTTP请求
10.0.0.1.39062 > 172.27.0.10.80: Flags [.]
10.0.0.1.39062 > 172.27.0.10.80: Flags [P.] GET / HTTP/1.1
# Envoy给的ACK
172.27.0.10.80 > 10.0.0.1.39062
# Envoy向上游发起请求,注意这里它不是发给127.0.0.1,而是Pod IP
# 尽管目的地址是172.27.0.10.80,这个包仍然是从lo发出去的
# 当从本机访问时,不论使用哪个目的IP时,默认都会从lo出去
10.0.0.1.50829 > 172.27.0.10.80: Flags [S]
# Nginx给出ACK,但是这个ACK没有收到,所以SYN+ACK反复了几次
# 实际上这些封包都从eth0发出去了
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 > 172.27.0.10.80: Flags [S]
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 > 172.27.0.10.80: Flags [S]
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
10.0.0.1.50829 > 172.27.0.10.80: Flags [S]
172.27.0.10.80 > 10.0.0.1.50829: Flags [S.]
# Envoy没有收到上游应答,认为服务不可用
172.27.0.10.80 > 10.0.0.1.39062  HTTP/1.1 503 Service Unavailable
# 终止连接
10.0.0.1.39062 > 172.27.0.10.80: Flags [.]
10.0.0.1.39062 > 172.27.0.10.80: Flags [F.]

可以看到,新版本的Istio,向上游发请求时,使用的目的地址是原始Dest地址,而不是127.0.0.1,因此, PR 23275也就失效了。

在当前的场景下,Envoy以客户端真实IP、通过lo向Nginx进程发起TCP连接,这个是OK的。但是回程报文从容器eth0发走了。回程报文到达宿主机后,被丢弃。

解决方案

我们需要识别,哪些请求是Envoy代表客户端转发的,并把这些请求的响应封包发回给Envoy,而不是通过eth0发送出去。

早前版本可以根据目的地址识别,现在直接来自客户端的、Envoy代表客户端转发的请求(以及响应),连接5元组完全一样,这意味着无法从IP地址上进行区分了。

幸运的是,iptables支持的CONNMARK目标可以在连接级别上打标记,这意味着往返报文可以共享信息。此外,original_src支持为封包设置标记,我们可以利用这一特性识别Envoy代表客户端发出的封包。结合这两点,我们可以得到23369的解决方案。

首先,我们需要为监听器过滤器original_src增加一个参数:

JSON
1
2
3
4
5
6
7
{
    "name": "envoy.listener.original_src",
    "typedConfig": {
        "@type": "type.googleapis.com/envoy.extensions.filters.listener.original_src.v3.OriginalSrc",
        "mark": 1337
    }
},

这样,Envoy请求上游(Nginx)时,发出的封包具有标记 1337。

然后,我们增加如下iptables规则:

Shell
1
2
3
4
5
# Envoy发出的封包,被Nginx处理之前,获取封包标记,保存为连接标记
iptables  -t mangle -I PREROUTING -m mark     --mark 1337  -j CONNMARK --save-mark
# Nginx处理请求...
# Nginx返回的响应封包,被打上从连接标记上取得的1337标记
iptables  -t mangle -I OUTPUT     -m connmark --mark 1337 -j CONNMARK --restore-mark

结合现有的策略路由,Nginx的回程封包就会从lo发出,并被Envoy接收到了。

到这一步,会出现先前的无限自我请求问题,这是由于规则:

Shell
1
2
3
Chain ISTIO_TPROXY (1 references)
pkts bytes target     prot opt in     out     source               destination        
    8   480 TPROXY     tcp  --  *      *       0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15006 mark 0x539/0xffffffff

该规则要求,只要目的地址不是127.0.0.1的请求,都会重定向到15006。在前面我们已经发现,TPROXY模式下访问上游Nginx不像先前版本那样使用127.0.0.1作为目的地址,因此这个规则必须要处理。

我的做法是,在它的前面做个判断,如果具有标记1337(意味着这是Envoy和上游Nginx之间的通信),就不走ISTIO_TPROXY:

Shell
1
iptables -t mangle -I ISTIO_INBOUND 5 -p tcp -m mark --mark 0x539   -j RETURN

修改后mangle表的整体内容如下:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# iptables -t mangle -L -n -v
Chain PREROUTING (policy ACCEPT 6280 packets, 680K bytes)
pkts bytes target     prot opt in     out     source               destination        
1163K   97M CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match 0x539 CONNMARK save
1440K  115M ISTIO_INBOUND  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain INPUT (policy ACCEPT 7459 packets, 817K bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain OUTPUT (policy ACCEPT 6126 packets, 781K bytes)
pkts bytes target     prot opt in     out     source               destination        
1107K   93M CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            connmark match  0x539 CONNMARK restore
    0     0 MARK       tcp  --  *      *       127.0.0.1           !127.0.0.1            MARK set 0x539
 
Chain POSTROUTING (policy ACCEPT 6126 packets, 781K bytes)
pkts bytes target     prot opt in     out     source               destination        
 
Chain ISTIO_DIVERT (1 references)
pkts bytes target     prot opt in     out     source               destination        
1308K  107M MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK set 0x539
1308K  107M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_INBOUND (1 references)
pkts bytes target     prot opt in     out     source               destination        
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
14058 1047K RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15021
4713  814K RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
   39  7165 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match 0x539
1308K  107M ISTIO_DIVERT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
113K 6778K ISTIO_TPROXY  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0          
 
Chain ISTIO_TPROXY (1 references)
pkts bytes target     prot opt in     out     source               destination        
    8   480 TPROXY     tcp  --  *      *       0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15006 mark 0x539/0xffffffff

从Nginx Pod外部访问、从Nginx Pod内部访问localhost以及Pod IP,一切行为正常,解决方案有效。 

此方案将通过PR 28363提交社区讨论。

← Cilium学习笔记
Galaxy学习笔记 →
10 Comments On This Topic
  1. 回复
    Alex
    2020/04/27

    值得注意的一点,1.6版本的监听器结构有很大变化,它将所有入站监听器都聚合到单个virtualInbound中,作为它的过滤器链,现在virtualInbound是唯一的INBOUND监听器。

  2. 回复
    Yann
    2020/09/27

    您好,我是从Github找到您的Blog,非常详细的文章,谢谢。我在做基本测试中,发现了一个问题,已经开了一个issue,https://github.com/istio/istio/issues/27565,是关于service to service call的。

    • 回复
      Alex
      2020/09/28

      TPROXY模式最近几个版本一直有问题,我也提了Issue,不过没人响应:(

  3. 回复
    will
    2021/04/30

    您好,请问什么时候redirect也会改变source ip? 我的理解包括自己的测试都是只改destination的port. 但是这个issue中却说有可能会改ip。谢谢!

    • 回复
      Alex
      2021/05/06

      先前的版本,不管是REDIRECT还是TPROXY模式,Envoy向上游(当前Pod)转发请求的时候,都不是使用原始请求的源IP,而是用127.0.0.1。这是因为上游和Envoy在同一个网络命名空间内,使用127.0.0.1是默认行为。

      不管是REDIRECT还是TPROXY模式,Envoy都有能力得到原始请求的源IP,并且使用此IP作为source ip向上游发起请求(IP_TRANSPARENT)。

      根据大家对TPROXY(透明代理)的行为的一般性期望,它应当让C/S端都感觉不到Proxy的存在。因此有了相关的Issue和PR

  4. 回复
    will
    2021/04/30

    抱歉忘了链接issue https://github.com/istio/istio/issues/5679#issuecomment-497429841

  5. 回复
    point
    2022/01/11

    tproxy一个比较大的好处就是不用nat表,不会出现contrack over了

  6. 回复
    G
    2022/02/08

    透传 src ip 基于 connmark,还是会有 ct table full 的场景吧

    • 回复
      point
      2022/07/26

      redirect才需要conntrack表,tproxy不需要

  7. 回复
    qg
    2023/05/31

    我有一个问题是这里面的envoy是要注入到POD内部的,假设不允许注入POD内部,那么有没有办法实现这种透明代理模式呢?

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 服务网格的现状和未来
  • Istio Mixer与Envoy的交互机制解读
  • Istio学习笔记
  • Istio Pilot与Envoy的交互机制解读
  • Envoy学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2