问题背景

在之前使用 ServiceEntry 和 EnvoyFilter 来接入 dubbo 服务到 istio 中时,最开始一直疑惑,当 consumer 端使用 ServiceEntry 中的 host 去访问,但 EnvoyFilter 中直接使用的是 addresses 和端口来进行过滤器匹配的,下发的 envoy 配置中也是用的 ip+port,ServiceEntry 并不会生成 service,那 host 到 ip 是如何转换的呢?

具体的例子参考 手动接入 dubbo 到 istio,设置的 ServiceEntry 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: dubbo-demoservice
  namespace: meta-dubbo
  annotations:
    interface: org.apache.dubbo.samples.api.GreetingService
spec:
  addresses:
  - 240.240.0.20
  hosts:
    - org.apache.dubbo.samples.api.greetingservice
  # ...

envoy 端收到的配置中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    {
     "name": "240.240.0.3_20880",
     "active_state": {
      "listener": {
       "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
       "name": "240.240.0.3_20880",
       "address": {
        "socket_address": {
         "address": "240.240.0.3",
         "port_value": 20880
        }
       },
       // ...
      },
      "last_updated": "2023-03-16T06:50:22.529Z"
     }
    },

DNS 逻辑

Kubernetes 中 DNS 逻辑

Kubernetes 的 DNS 实现基于 CoreDNS,是一种轻量级的 DNS 服务器。Kubernetes 中每个 service 会配置一个 DNS 名称和 cluster ip,DNS 名称格式为:..svc.cluster.local。DNS 相关 pods 和 service 为:

1
2
3
4
5
root@dev:~# kubectl get -nkube-system svc kube-dns
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.43.0.10   <none>        53/UDP,53/TCP,9153/TCP   36d
root@dev:~# kubectl get -nkube-system pods |grep dns
coredns-597584b69b-ctbcv                    1/1     Running     0          36d

在 pods 中查 DNS 服务器的配置/etc/resolv.conf 为:

1
2
3
search meta-dubbo.svc.cluster.local svc.cluster.local cluster.local openstacklocal
nameserver 10.43.0.10
options ndots:5

nameserver 指向 kube-dns 的 cluster ip,即 pod 先通过请求 dns 文件中配置 dns 服务器,由 dns 服务器做域名解析转换成对应的 cluster ip,然后通过 cluster ip 去访问。

Istio 中 DNS 测试

当在 Istio 设置了前面提到的 ServiceEntry 后,用 nslookup 测试解析过程,发现仍然是去 dns 文件中的服务器请求的:

1
2
3
4
5
6
root@dubbo-sample-provider-6c84cbc484-6nq4r:/# nslookup org.apache.dubbo.samples.api.greetingservice
Server:    10.43.0.10
Address:  10.43.0.10#53

Name:  org.apache.dubbo.samples.api.greetingservice
Address: 240.240.0.20

ServiceEntry 没有生成对应的 service,在 kubernetes 的 coredns 中应该是没有信息的,为啥还是会去 coredns 请求的呢?istio 中 envoy 会劫持流量,难道是被 envoy 转到 istio 去获取的? 为了验证此想法,在 pods 外面也测试了下 dns 解析结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@dev:~# nslookup kube-dns.kube-system.svc.cluster.local 10.42.0.4
Server:    10.42.0.4
Address:  10.42.0.4#53

Name:  kube-dns.kube-system.svc.cluster.local
Address: 10.43.0.10

root@dev:~# nslookup org.apache.dubbo.samples.api.greetingservice 10.42.0.4
Server:    10.42.0.4
Address:  10.42.0.4#53

** server can't find org.apache.dubbo.samples.api.greetingservice: NXDOMAIN

可以发现,kubernetes 原生的 service 可以找到,ServiceEntry 中的 host 没有找到,因此确定不是去 coredns 解析的。

ISTIO_META_DNS_CAPTURE 配置

Istio 可以捕获 DNS 请求,以提高网格的性能和可用性。 当 Istio 代理 DNS 时,所有来自应用程序的 DNS 请求将会被重定向到 Sidecar,因为 Sidecar 存储了域名到 IP 地址的映射。如果请求被 Sidecar 处理,它将直接给应用返回响应,避免了对上游 DNS 服务器的往返。反之,请求将按照标准的/etc/resolv.conf DNS 配置向上游转发。Istio 的 DNS 有以下两个作用:

  • ServiceEntry 地址可以被解析,而不需要自定义 DNS 服务配置
  • 减少了 kube-dns 的负载,并且提高了性能

Istio 是通过“ISTIO_META_DNS_CAPTURE”配置来开启 DNS 代理的,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cat <<EOF | istioctl install -y -f -
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    defaultConfig:
      proxyMetadata:
        # Enable basic DNS proxying
        ISTIO_META_DNS_CAPTURE: "true"
        # Enable automatic address allocation, optional
        ISTIO_META_DNS_AUTO_ALLOCATE: "true"
EOF

在 consumer 的 pod 描述中可以看到,在 init 容器 istio-init 和 sidecar 容器 istio-proxy 中都设置了 ISTIO_META_DNS_CAPTURE 环境变量,通过这两个容器来使 dns 生效的。

 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
  containers:
  - args:
    - proxy
    - sidecar
    - --domain
    - $(POD_NAMESPACE).svc.cluster.local
    - --proxyLogLevel=debug
    - --proxyComponentLogLevel=misc:error
    - --log_output_level=default:debug
    - --concurrency
    - "2"
    env:
    - name: JWT_POLICY
      value: third-party-jwt
    # ...
    - name: ISTIO_META_DNS_CAPTURE
      value: "true"
    image: docker.io/istio/proxyv2:1.16.1
    imagePullPolicy: IfNotPresent
    name: istio-proxy
    # ...
  initContainers:
  - args:
    - istio-iptables
    - -p
    - "15001"
    - -z
    - "15006"
    - -u
    - "1337"
    - -m
    - REDIRECT
    - -i
    - '*'
    - -x
    - ""
    - -b
    - '*'
    - -d
    - 15090,15021,15020
    - --log_output_level=default:debug
    env:
    - name: ISTIO_META_DNS_CAPTURE
      value: "true"
    image: docker.io/istio/proxyv2:1.16.1
    imagePullPolicy: IfNotPresent
    name: istio-init
    # ...

查看 iptables 规则:

 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
root@dev:~# ctr containers list | grep consumer
12a5e7f5939159d2d1c7b66d673234387724d22efe42ad87160aa40d4c46c4c6    127.0.0.1/consumer:v0.0.1                              io.containerd.runc.v2
9df1e53b97a2f6ae2725e9d8c9c7fa0d7f93c59c54cb4adafd65f671c5cedc86    127.0.0.1/consumer:v0.0.1                              io.containerd.runc.v2
root@dev:~# ctr task list | grep 12a5e7f5939159d2d1c7b66d673234387724d22efe42ad87160aa40d4c46c4c6
12a5e7f5939159d2d1c7b66d673234387724d22efe42ad87160aa40d4c46c4c6    3777207    RUNNING

root@dev:~# nsenter -n --target 3777207
# -n:用数字形式显示输出结果,如显示主机的 IP 地址而不是主机名
root@dev:~# iptables -t nat -nL --line-number
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination
1    ISTIO_INBOUND  tcp  --  0.0.0.0/0            0.0.0.0/0

Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
num  target     prot opt source               destination
1    ISTIO_OUTPUT  tcp  --  0.0.0.0/0            0.0.0.0/0
2    RETURN     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:53 owner UID match 1337
3    RETURN     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:53 owner GID match 1337
4    REDIRECT   udp  --  0.0.0.0/0            10.43.0.10           udp dpt:53 redir ports 15053

Chain POSTROUTING (policy ACCEPT)
num  target     prot opt source               destination

Chain ISTIO_INBOUND (1 references)
num  target     prot opt source               destination
1    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15008
2    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
3    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15021
4    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
5    ISTIO_IN_REDIRECT  tcp  --  0.0.0.0/0            0.0.0.0/0

Chain ISTIO_IN_REDIRECT (3 references)
num  target     prot opt source               destination
1    REDIRECT   tcp  --  0.0.0.0/0            0.0.0.0/0            redir ports 15006

Chain ISTIO_OUTPUT (1 references)
num  target     prot opt source               destination
1    RETURN     all  --  127.0.0.6            0.0.0.0/0
2    ISTIO_IN_REDIRECT  tcp  --  0.0.0.0/0           !127.0.0.1            tcp dpt:!53 owner UID match 1337
3    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:!53 ! owner UID match 1337
4    RETURN     all  --  0.0.0.0/0            0.0.0.0/0            owner UID match 1337
5    ISTIO_IN_REDIRECT  all  --  0.0.0.0/0           !127.0.0.1            owner GID match 1337
6    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:!53 ! owner GID match 1337
7    RETURN     all  --  0.0.0.0/0            0.0.0.0/0            owner GID match 1337
8    REDIRECT   tcp  --  0.0.0.0/0            10.43.0.10           tcp dpt:53 redir ports 15053
9    RETURN     all  --  0.0.0.0/0            127.0.0.1
10   ISTIO_REDIRECT  all  --  0.0.0.0/0            0.0.0.0/0

Chain ISTIO_REDIRECT (1 references)
num  target     prot opt source               destination
1    REDIRECT   tcp  --  0.0.0.0/0            0.0.0.0/0            redir ports 15001

其中有涉及到 dns 规则或者端口的有:

  • OUTPUT 链中的第 2 条:udp 协议,端口为 53,且 UID 为 1337(Envoy 代理用户),表示 envoy 发出的 dns 流量,则直接返回,把流量发送到任意目的地址。
  • OUTPUT 链中的第 3 条:udp 协议,端口为 53,且 GID 为 1337(Envoy 代理组),表示 envoy 发出的 dns 流量,则直接返回,把流量发送到任意目的地址。
  • OUTPUT 链中的第 4 条:udp 协议,端口为 53,目的 ip 为 kube-dns 的地址,表示应用容器发出的 dns 请求,则转发到本地 15053。
  • ISTIO_OUTPUT 链中的第 8 条:tcp 协议,端口为 53,目的 ip 为 kube-dns 的地址,表示应用容器发出的 dns 请求,则转发到本地 15053。

源码分析

istio-iptables dns 源码

istio-iptables 是 istio 用在代理的 init 容器中,用来初始化 iptables 规则。对于 dns 捕获来说,也就是设置规则捕获 dns 相关的流量。 下面代码中获取了环境变量中 dns 捕获标识,初始化启动配置:

 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
// istio/tools/istio-iptables/pkg/cmd/root.go
var (
  // 开启 dns 拦截
  dnsCaptureByAgent = env.Register("ISTIO_META_DNS_CAPTURE", false,
    "If set to true, enable the capture of outgoing DNS packets on port 53, redirecting to istio-agent on :15053").Get()
)

func bindFlags(cmd *cobra.Command, args []string) {
  // 获取所有环境变量
  viper.AutomaticEnv()
  viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

  envoyPort := "15001"
  inboundPort := "15006"
  inboundTunnelPort := "15008"
    // ...
  if err := viper.BindPFlag(constants.RedirectDNS, cmd.Flags().Lookup(constants.RedirectDNS)); err != nil {
    handleError(err)
  }
  viper.SetDefault(constants.RedirectDNS, dnsCaptureByAgent)
}

func constructConfig() *config.Config {
  cfg := &config.Config{
    RedirectDNS:             viper.GetBool(constants.RedirectDNS),
    CaptureAllDNS:           viper.GetBool(constants.CaptureAllDNS),
        // ...
  }
    // 查找 DNS 的域名服务器。只会在 dns 开启情况下,且出现一些理论上的隐蔽问题,比如读取/etc/resolv.conf 文件失败
    // 加入开启了捕获所有的 dns 请求,不需要读 dns 配置文件,所有到 53 端口的流量都会被捕获
  if cfg.RedirectDNS && !cfg.CaptureAllDNS {
    dnsConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf")
    if err != nil {
      panic(fmt.Sprintf("failed to load /etc/resolv.conf: %v", err))
    }
    cfg.DNSServersV4, cfg.DNSServersV6 = netutil.IPsSplitV4V6(dnsConfig.Servers)
  }
  return cfg
}

在 Run 函数中,如果设置了 dns 代理,则设置为:

 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
// istio/tools/istio-iptables/pkg/capture/run.go
func (cfg *IptablesConfigurator) Run() {
  redirectDNS := cfg.cfg.RedirectDNS
  cfg.logConfig()
    // ...
  for _, uid := range split(cfg.cfg.ProxyUID) {
        // 在使用 VIP 时,通过 Envoy 将应用程序的调用重定向到自身
        // 比如:appN => Envoy (client) => Envoy (server) => appN.
    if redirectDNS {
      // 当 dns 捕获开启时,跳过端口 53,这可以保证不出现:app => istio-agent => Envoy inbound => dns server
      // 而保证流程为:app => istio-agent => dns server
      // 选择从本地 lo 网络接口发起的,且目的地址不为“127.0.0.1”
      // 指定是 tcp 规则,且目的端口不为 53
      // 匹配发起者用户为 uid,则转发到 ISTIOINREDIRECT 规则
      // 这条规则匹配 ISTIO_OUTPUT 中的第 2 条
      // 2    ISTIO_IN_REDIRECT  tcp  --  0.0.0.0/0           !127.0.0.1            tcp dpt:!53 owner UID match 1337
      cfg.iptables.AppendVersionedRule("127.0.0.1/32", "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT,
        "-o", "lo", "!", "-d", constants.IPVersionSpecific,
        "-p", "tcp", "!", "--dport", "53",
        "-m", "owner", "--uid-owner", uid, "-j", constants.ISTIOINREDIRECT)
    } else {
      cfg.iptables.AppendVersionedRule("127.0.0.1/32", "::1/128", iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT,
        "-o", "lo", "!", "-d", constants.IPVersionSpecific,
        "-m", "owner", "--uid-owner", uid, "-j", constants.ISTIOINREDIRECT)
    }
        // 在使用 endpoint 地址时,不会通过 Envoy 将应用程序的调用重定向到自身
    // 比如:appN => appN by lo
        // 如果通过 OutboundIPRangesInclude 明确设置了回环,那么就不要返回
    if !ipv4RangesInclude.HasLoopBackIP && !ipv6RangesInclude.HasLoopBackIP {
      if redirectDNS {
        // 用户可能在本地有一个 DNS 服务器,为了处理这种情况,从这条规则中排除了端口 53
        // 从本地接口请求,目的端口不为 53,且不是 uid 的用户发起的
        // 这条规则匹配 ISTIO_OUTPUT 中的第 3 条
        // 3    RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:!53 ! owner UID match 1337
        cfg.iptables.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT, "-o", "lo", "-p", "tcp",
          "!", "--dport", "53",
          "-m", "owner", "!", "--uid-owner", uid, "-j", constants.RETURN)
      } else {
        cfg.iptables.AppendRule(iptableslog.UndefinedCommand, constants.ISTIOOUTPUT, constants.NAT,
          "-o", "lo", "-m", "owner", "!", "--uid-owner", uid, "-j", constants.RETURN)
      }
    }
  }
    // ...
  if redirectDNS {
    if cfg.cfg.CaptureAllDNS {
      // 重定向所有 53 端口上的 TCP dns 流量到 agent 的端口 15053 上,这在 CNI 场景下很有用,因为 pod 的 dns 服务器地址还不能确定
      cfg.iptables.AppendRule(iptableslog.UndefinedCommand,
        constants.ISTIOOUTPUT, constants.NAT,
        "-p", constants.TCP,
        "--dport", "53",
        "-j", constants.REDIRECT,
        "--to-ports", constants.IstioAgentDNSListenerPort)
    } else {
      for _, s := range cfg.cfg.DNSServersV4 {
                // 将在端口 53 上请求/etc/resolv.conf 中所有服务器的 TCP dns 流量重定向到 agent 的 15053 端口
                // 避免重定向所有 IP 范围,来避免当有本地 dns 服务器时出现无限循环,比如:app -> istio dns server -> dnsmasq -> upstream
                // 这保证了不会从 dnsmasq 获取请求,并发送回 agent 的 dns 服务器
        // 这条匹配 ISTIO_OUTPUT 的第 8 条,重定向到 agent 监听的 15053 端口
        // 8    REDIRECT   tcp  --  0.0.0.0/0            10.43.0.10           tcp dpt:53 redir ports 15053
        cfg.iptables.AppendRuleV4(iptableslog.UndefinedCommand,
          constants.ISTIOOUTPUT, constants.NAT,
          "-p", constants.TCP,
          "--dport", "53",
          "-d", s+"/32",
          "-j", constants.REDIRECT,
          "--to-ports", constants.IstioAgentDNSListenerPort)
      }
            // ...
    }
  }

  if redirectDNS {
    HandleDNSUDP(
      AppendOps, cfg.iptables, cfg.ext, "",
      cfg.cfg.ProxyUID, cfg.cfg.ProxyGID,
      cfg.cfg.DNSServersV4, cfg.cfg.DNSServersV6, cfg.cfg.CaptureAllDNS,
      ownerGroupsFilter)
  }
    // ...
  cfg.executeCommands()
}

这里只展示了部分 dns 相关代码,其他涉及 dns iptables 规则的代码和上面类似,比如像 udp 的 dns 规则、针对用户组的 dns 规则。

istio-agent dns 源码

istio-agent 跟上面一样,也是从环境变量获取值,然后写到 Agent 的配置结构体里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// istio/pilot/cmd/pilot-agent/options/agent.go
func NewAgentOptions(proxy *model.Proxy, cfg *meshconfig.ProxyConfig) *istioagent.AgentOptions {
  o := &istioagent.AgentOptions{
    XDSRootCerts:             xdsRootCA,
    CARootCerts:              caRootCA,
        // ...
    DNSCapture:                  DNSCaptureByAgent.Get(), // DNSCaptureByAgent 值是从环境变量“ISTIO_META_DNS_CAPTURE”获取的
    DNSForwardParallel:          DNSForwardParallel.Get(), // 是否并行转发 dns 请求
    DNSAddr:                     DNSCaptureAddr.Get(),  // DNSCaptureAddr 值是从环境变量获取的,默认为“localhost:15053”
  }
  return o
}

在 Agent 的启动函数 Run 里面,调用 initLocalDNSServer 来开启 dns 服务器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// istio/pkg/istio-agent/agent.go
func (a *Agent) Run(ctx context.Context) (func(), error) {
  var err error
  if err = a.initLocalDNSServer(); err != nil {
    return nil, fmt.Errorf("failed to start local DNS server: %v", err)
  }
    // ...
}

func (a *Agent) initLocalDNSServer() (err error) {
  // 如果开启 dns 捕获,且是 sidecar 代理时,则启动本地 dns 服务器
  if a.cfg.DNSCapture && a.cfg.ProxyType == model.SidecarProxy {
    if a.localDNSServer, err = dnsClient.NewLocalDNSServer(a.cfg.ProxyNamespace, a.cfg.ProxyDomain, a.cfg.DNSAddr,
      a.cfg.DNSForwardParallel); err != nil {
      return err
    }
    a.localDNSServer.StartDNS()
  }
  return nil
}

通过 NewLocalDNSServer 创建 dns 服务器,里面持有 dns 下游服务器配置,通过 StartDNS 启动 tcp 和 udp 的 dns 代理:

 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
// istio/pkg/dns/client/dns.go
func NewLocalDNSServer(proxyNamespace, proxyDomain string, addr string, forwardToUpstreamParallel bool) (*LocalDNSServer, error) {
  h := &LocalDNSServer{
    proxyNamespace:            proxyNamespace,
    forwardToUpstreamParallel: forwardToUpstreamParallel,
  }

  resolvConf := "/etc/resolv.conf"
    // ...
    // 使用 resolv.conf 中的服务器解析不知道的域名
  dnsConfig, err := dns.ClientConfigFromFile(resolvConf)
  if err != nil {
    log.Warnf("failed to load /etc/resolv.conf: %v", err)
    return nil, err
  }

    // 不像传统的 dns 解析器,不需要附加搜索命名空间,因为 agent 是一个 dns 拦截器
    // 只需要检查域名在本地域名表中是否存在,如果没有,就将查询转发给上游解析器
  if dnsConfig != nil {
    for _, s := range dnsConfig.Servers {
      h.resolvConfServers = append(h.resolvConfServers, net.JoinHostPort(s, dnsConfig.Port))
    }
    h.searchNamespaces = dnsConfig.Search
  }
  addresses := []string{addr}
  // 分别启动 udp 和 tcp 的代理
  for _, ipAddr := range addresses {
    for _, proto := range []string{"udp", "tcp"} {
      proxy, err := newDNSProxy(proto, ipAddr, h)
      if err != nil {
        return nil, err
      }
      h.dnsProxies = append(h.dnsProxies, proxy)

    }
  }
  return h, nil
}

// 启动 dns 代理
func (h *LocalDNSServer) StartDNS() {
  for _, p := range h.dnsProxies {
    go p.start()
  }
}

// 使用的“github.com/miekg/dns”库,需实现 ServeDNS 这个 Handler 接口
// 里面会 dnsProxies.ServeDNS 函数,然后调用到 LocalDNSServer.ServeDNS 函数
func (h *LocalDNSServer) ServeDNS(proxy *dnsProxy, w dns.ResponseWriter, req *dns.Msg) {
  requests.Increment()
  var response *dns.Msg
  // dns 查询表
  lp := h.lookupTable.Load()
  hostname := strings.ToLower(req.Question[0].Name)
  // ...
  lookupTable := lp.(*LookupTable)
  var answers []dns.RR
  // 名称总会以“.”结束,期望每个请求只有一个域名
  answers, hostFound := lookupTable.lookupHost(req.Question[0].Qtype, hostname)
  if hostFound {
    response = new(dns.Msg)
    response.SetReply(req)
    response.Authoritative = true
    response.Answer = answers
    // 打乱回复,保证能做到 DNS 负载均衡,比如 headless 服务需要,也和 kube-dns 的行为一致
    if len(answers) > 0 {
      roundRobinResponse(response)
    }
    log.Debugf("response for hostname %q (found=true): %v", hostname, response)
  } else {
    // 缓存没找到,去上游 dns 服务器请求
    response = h.upstream(proxy, req, hostname)
  }
  response.Truncate(size(proxy.protocol, req))
  _ = w.WriteMsg(response)
}

上面 lookupTable 是本地的域名缓存,从 initXdsProxy 函数中可以看到域名信息是通过 xds 获取的,然后更新到 lookupTable 的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func initXdsProxy 函数 (ia *Agent) (*XdsProxy, error) {
  if ia.localDNSServer != nil {
    proxy.handlers[v3.NameTableType] = func(resp *anypb.Any) error {
      var nt dnsProto.NameTable
      if err := resp.UnmarshalTo(&nt); err != nil {
        log.Errorf("failed to unmarshal name table: %v", err)
        return err
      }
      ia.localDNSServer.UpdateLookupTable(&nt)
      return nil
    }
  }
}

istio-discovery dns 源码

获取域名信息的服务发现类型是 nds,nds 在 istio-discovery 中对应的 Generate 实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// istio/pilot/pkg/xds/nds.go
func (n NdsGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource, req *model.PushRequest) model.Resources {
    if !ndsNeedsPush(req) {
        return nil
    }
    nt := n.Server.ConfigGenerator.BuildNameTable(proxy, push)
    if nt == nil {
        return nil
    }
    resources := model.Resources{util.MessageToAny(nt)}
    return resources
}

上面的 Generate 最终会调用到 BuildNameTable 函数,生成一个域名和关联 ip 的表格,提供给代理解析 dns 使用:

 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
// istio/pkg/dns/server/name_table.go
func BuildNameTable(cfg Config) *dnsProto.NameTable {
  if cfg.Node.Type != model.SidecarProxy {
    return nil
  }

  out := &dnsProto.NameTable{
    Table: make(map[string]*dnsProto.NameTable_NameInfo),
  }
  // 将 sidecar 可见的所有 service 返回
  for _, svc := range cfg.Node.SidecarScope.Services() {
    // 返回服务器地址,具体到该节点所在的集群
    svcAddress := svc.GetAddressForProxy(cfg.Node)
    var addressList []string
    hostName := svc.Hostname
    if svcAddress != constants.UnspecifiedIP {
      if !netutil.IsValidIPAddress(svcAddress) {
        continue
      }
      addressList = append(addressList, svcAddress)
    } else {
      // 两种情况会走这,headless 服务,或者 ServiceEntry 自动分配 ip 逻辑无法分配 ip
      if svc.Resolution == model.Passthrough && len(svc.Ports) > 0 {
        for _, instance := range cfg.Push.ServiceInstancesByPort(svc, svc.Ports[0].Port, nil) {
          sameNetwork := cfg.Node.InNetwork(instance.Endpoint.Network)
          sameCluster := cfg.Node.InCluster(instance.Endpoint.Locality.ClusterID)
          // 对于 headless 服务,用 pods ip 填充 dns 表
          if instance.Endpoint.SubDomain != "" && sameNetwork {
            // 示例:mysql-0.mysql.default.svc.cluster.local
            parts := strings.SplitN(hostName.String(), ".", 2)
            if len(parts) != 2 {
              continue
            }
            address := []string{instance.Endpoint.Address}
            shortName := instance.Endpoint.HostName + "." + instance.Endpoint.SubDomain
            host := shortName + "." + parts[1] // Add cluster domain.
            nameInfo := &dnsProto.NameTable_NameInfo{
              Ips:       address,
              Registry:  string(svc.Attributes.ServiceRegistry),
              Namespace: svc.Attributes.Namespace,
              Shortname: shortName,
            }

            if _, f := out.Table[host]; !f || sameCluster {
              out.Table[host] = nameInfo
            }
          }
          skipForMulticluster := !cfg.MulticlusterHeadlessEnabled && !sameCluster
          if skipForMulticluster || !sameNetwork {
            // 只处理本地集群的 endpoints,因为如果返回跨集群的 pods ip 了,就绕过网关
            continue
          }
          addressList = append(addressList, instance.Endpoint.Address)
        }
      }
      if len(addressList) == 0 {
        continue
      }
    }

    nameInfo := &dnsProto.NameTable_NameInfo{
      Ips:      addressList,
      Registry: string(svc.Attributes.ServiceRegistry),
    }
    if svc.Attributes.ServiceRegistry == provider.Kubernetes &&
      !strings.HasSuffix(hostName.String(), "."+constants.DefaultClusterSetLocalDomain) {
      nameInfo.Namespace = svc.Attributes.Namespace
      nameInfo.Shortname = svc.Attributes.Name
    }
    out.Table[hostName.String()] = nameInfo
  }
  return out
}

参考