# istio dns拦截

## k8s原生dns在istio场景的问题

### dns的作用

DNS解析是Kubernetes上任何应用程序基础架构的重要组成部分.当您的应用程序代码尝试访问Kubernetes集群中的另一个服务甚至是Internet上的服务时,它必须先查找与该服务的主机名相对应的IP地址,然后再启动与该服务的连接.此名称查找过程通常称为服务发现。在Kubernetes中,server(无论是kube-dnsCoreDNS还是CoreDNS)将服务的主机名解析为唯一的不可路由的虚拟IP(VIP),如果它是clusterIP类型的服务.在kube-proxy每个节点上这个VIP映射到该服务的一组pod,并随机选择一个pod进行转发。使用服务网格时,边车的工作原理就流量转发而言与kube-proxy相同。

下图描述了当今DNS的作用:

![](http://img.rocdu.top/20201117/role-of-dns-today.png)

### DNS带来的问题

尽管DNS在服务网格中的作用似乎微不足道,但它始终代表着将网格扩展到VM并实现无缝多集群访问的方式。

#### 虚拟机访问Kubernetes服务

考虑到VM带有sidecar的情况。如下图所示,VM上的应用程序会查找Kubernetes集群内服务的IP地址,因为它们通常无法访问集群的DNS服务器。

![虚拟机访问Kubernetes服务时的DNS解析问题](http://img.rocdu.top/20201117/vm-dns-resolution-issues.png)

如果有人愿意参与一些涉及dnsmasq和使用NodePort服务对kube-dns进行外部暴露的复杂变通方法,从技术上讲,可以在虚拟机上使用kube-dns作为域名服务器:假设您设法说服集群管理员这样做.即使这样,您仍在打开许多安全问题的大门.归根结底,对于那些组织能力和领域专业知识有限的人来说,这些解决方案通常超出范围。

#### 没有VIP的外部TCP服务

不仅网状网络中的VM遭受DNS问题。为了使Sidecar能够准确地区分网格外部的两个不同TCP服务之间的流量,这些服务必须位于不同的端口上,或者它们需要具有全局唯一的VIP,就像clusterIP分配给Kubernetes服务一样。但是,如果没有VIP,该怎么办？云托管服务(例如托管数据库)通常没有VIP。取而代之的是,提供者的DNS服务器返回实例IP之一,然后可由应用程序直接访问这些实例IP。例如,考虑以下两个服务条目,它们指向两个不同的AWS RDS服务:

```
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: db1
  namespace: ns1
spec:
  hosts:
  - mysql–instance1.us-east-1.rds.amazonaws.com
  ports:
  - name: mysql
    number: 3306
    protocol: TCP
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: db2
  namespace: ns1
spec:
  hosts:
  - mysql–instance2.us-east-1.rds.amazonaws.com
  ports:
  - name: mysql
    number: 3306
    protocol: TCP
  resolution: DNS
```

边车上有一个监听器 0.0.0.0:3306,该监听器从公共DNS服务器查找mysql-instance1.us-east1.rds.amazonaws.com的IP地址并将流量转发给它。它无法将流量路由至db2因为它无法区分到达的流量 0.0.0.0:3306是绑定db1还是绑定db2。实现此目的的唯一方法是将解析设置为NONE,使Sidecar将端口上的所有流量盲目转发3306到应用程序请求的原始IP。这类似于在防火墙上打一个洞,使所有流量都可以3306传入端口,而与目标IP无关。为了使流量畅通,现在您不得不在系统的安全性上做出妥协。

#### 为远程集群中的服务解析DNS

多集群网格的DNS限制是众所周知的。如果没有笨拙的解决方法(例如在调用方名称空间中创建存根服务),则一个集群中的服务无法查找其他集群中服务的IP地址。

### 控制DNS

总而言之,DNS在Istio中一直是一个棘手的问题。现在是时候杀死那只野兽了。我们(Istio网络团队)决定以对您(最终用户)完全透明的方式彻底解决该问题。我们的首次尝试涉及利用Envoy的DNS代理。事实证明这是非常不可靠的,并且由于Envoy使用的c-ares DNS库普遍缺乏复杂性,因此总体上令人失望。为了解决这个问题,我们决定在Go语言编写的Istio sidecar代理中实现DNS代理。我们能够优化实现,以处理我们要解决的所有场景,而不会影响规模和稳定性。我们使用的Go DNS库与可扩展DNS实现(例如CoreDNS,Consul,Mesos等)使用的库相同。

从Istio 1.8开始,Sidecar上的Istio代理将附带由Istiod动态编程的缓存DNS代理。Istiod基于Kubernetes服务和集群中的服务条目,为应用程序可以访问的所有服务推送主机名到IP地址的映射。来自应用程序的DNS查找查询被Pod或VM中的Istio代理透明地拦截并提供服务。如果查询是针对网格中的服务,则无论该服务所在的集群是什么,代理都会直接对应用程序做出响应。如果不是,它将查询转发到/etc/resolv.conf中定义的上游域名服务器。下图描述了当应用程序尝试使用其主机名访问服务时发生的交互。

![](http://img.rocdu.top/20201117/dns-interception-in-istio.png)

正如您将在以下各节中看到的那样,DNS代理功能已在Istio的许多方面产生了巨大的影响。

#### 降低DNS服务器的负载并提高解析度

集群中Kubernetes DNS server上的负载急剧下降,因为Istio在Pod内几乎解决了所有DNS查询。集群上的网格使用范围越大,DNS服务器上的负载就越小。在Istio代理中实现自己的DNS代理使我们能够实现出色的优化,例如CoreDNS auto-path,而不会出现CoreDNS当前面临的正确性问题。

要了解此优化的影响,让我们在标准Kubernetes集群中采用简单的DNS查找方案,而无需为Pod进行任何自定义DNS设置-即,默认/etc/resolv.conf中设置为ndots:5。当您的应用程序启动DNS查找 productpage.ns1.svc.cluster.local时,它会在按原查询主机之前将DNS搜索名称空间作为DNS查询的一部分附加在/etc/resolv.conf(例如ns1.svc.cluster.local)中。结果,实际上发出的第一个DNS查询看起来像 productpage.ns1.svc.cluster.local.ns1.svc.cluster.local,当不涉及Istio时,它将不可避免地使DNS解析失败。如果您 /etc/resolv.conf有5个搜索名称空间,则应用程序将为每个搜索名称空间发送两个DNS查询,一个用于IPv4 A记录,另一个用于IPv6 AAAA记录,然后是最后一对查询,其中包含代码中使用的确切主机名。在建立连接之前,该应用程序将为每个主机执行12个DNS查找查询！

使用Istio实现的CoreDNS样式自动路径技术,Sidecar代理将检测到在第一个查询中查询的真实主机名,并将cname记录 返回productpage.ns1.svc.cluster.local为该DNS响应的一部分以及的A/AAAA记录 productpage.ns1.svc.cluster.local。现在,收到此响应的应用程序可以立即提取IP地址,并继续建立与该IP的TCP连接。Istio代理中的智能DNS代理将DNS查询数量从12个大大减少到2个！

#### 虚拟机到Kubernetes集成

由于Istio代理对网格内的服务执行了本地DNS解析,因此从VM进行的Kubernetes服务的DNS查找查询现在将成功完成,而无需笨拙的变通办法来暴露kube-dns 到集群外部。现在,无缝解析集群中内部服务的能力将简化您到微服务的旅程,因为VM现在可以访问Kubernetes上的微服务,而无需通过API网关进行其他级别的间接访问。

#### 尽可能自动分配VIP

您可能会问,代理中的此DNS功能如何解决区分在同一端口上没有VIP的多个外部TCP服务的问题？

从Kubernetes获得启发,Istio现在将自动将不可路由的VIP(来自E类子网)分配给此类服务,只要它们不使用通配符主机即可。边车上的Istio代理将使用VIP作为来自应用程序的DNS查找查询的响应。现在,Envoy可以清楚地区分绑定到每个外部TCP服务的流量,并将其转发到正确的目标。通过引入DNS代理,您将不再需要`resolution: NONE`用于非通配TCP服务,从而改善了整体安全性。Istio在通配符外部服务(例如`*.us-east1.rds.amazonaws.com`)方面无济于事。您将不得不诉诸NONE解析模式来处理此类服务。

#### 多集群DNS查找

对于喜欢冒险的人来说,尝试编织一个多集群网格,其中应用程序直接调用远程集群中名称空间的内部服务,DNS代理功能非常方便。您的应用程序可以解析任何名称空间中任何集群上的Kubernetes服务,而无需在每个集群中创建存根Kubernetes服务。

DNS代理的优势超出了Istio当前描述的多集群模型。在Tetrate,我们在客户的多集群部署中广泛使用此机制,以使Sidecar能够为网格中所有集群的入口网关处暴露的主机解析DNS,并通过相互的TLS访问它们。

## istio DNS实现原理

### 简介

在istio1.8中为了支持DNS解析功能,并且实现了dns cache,不需要通过search域进行多次查询,例如解析bar.foo.svc.cluster.local可能需要依次解析bar.foo.svc.cluster.local.foo.svc.cluster.local/bar.foo.svc.cluster.local.svc.cluster.local/bar.foo.svc.cluster.local.cluster.local/bar.foo.svc.cluster.local,这是因为search域的配置影响,而istio pilot-agent dns代理则只需要一次解析,提高了解析速度。

为了支持dns cache,则需要控制面下发网格内部的现有服务的域名IP配置到agent,为了解决数据传输问题引入了NDS(NameTable Service Discovery)协议,其url定义为`type.googleapis.com/istio.networking.nds.v1.NameTable`。

本位将探索NDS资源的下发方式及客户端的dns解析原理

### pilot-agent

在通过以下方式安装istio后

```
 istioctl install --set profile=demo  --set meshConfig.defaultConfig.proxyMetadata.ISTIO_META_DNS_CAPTURE='\"true\"'
```

pilot-agent的istio-iptables将在添加iptables规则时将53的dns请求重定向到pilot-agent同时启动dns server

```
func (sa *Agent) initLocalDNSServer(isSidecar bool) (err error) {
    if sa.cfg.DNSCapture && sa.cfg.ProxyXDSViaAgent && isSidecar {
        if sa.localDNSServer, err = dns.NewLocalDNSServer(sa.cfg.ProxyNamespace, sa.cfg.ProxyDomain); err != nil {
            return err
        }
        sa.localDNSServer.StartDNS()
    }
    return nil
}
```

#### NewLocalDNSServer

根据集群域及命名空间获取LocalDNSServer

```
func NewLocalDNSServer(proxyNamespace, proxyDomain string) (*LocalDNSServer, error) {
    h := &LocalDNSServer{
        proxyNamespace: proxyNamespace,
    }
    // proxyDomain可以包含使其成为冗余的名称空间.我们只需要.svc.cluster.local部分
    parts := strings.Split(proxyDomain, ".")
    if len(parts) > 0 {
        if parts[0] == proxyNamespace {
            parts = parts[1:]
        }
        h.proxyDomainParts = parts
        h.proxyDomain = strings.Join(parts, ".")
    }
    // 使用本地的resolv.conf作为dns代理配置
    dnsConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf")
    if err != nil {
        log.Warnf("failed to load /etc/resolv.conf: %v", err)
        return nil, err
    }
     // 与传统的DNS解析器不同,不需要将搜索名称空间附加到查询再解析。这是因为代理充当应用程序进行的DNS查询的DNS拦截器。应用程序的解析器已经向我们发送了DNS查询,每个DNS搜索名称空间都有一个。我们只需要在本地命名表中检查此名称是否存在.如果没有,我们会将查询原样转发给上游解析器。
    if dnsConfig != nil {
        for _, s := range dnsConfig.Servers {
            h.resolvConfServers = append(h.resolvConfServers, s+":53")
        }
        h.searchNamespaces = dnsConfig.Search
    }

    if h.udpDNSProxy, err = newDNSProxy("udp", h); err != nil {
        return nil, err
    }
    if h.tcpDNSProxy, err = newDNSProxy("tcp", h); err != nil {
        return nil, err
    }

    return h, nil
}
```

**newDNSProxy**

将LocalDNSServer作为resolver传递给对应的tcp和udp proxy

#### StartDNS

分别启动tcpDNSProxy和udpDNSProxy

#### ServeDNS

对于miekg/dns来说handler需要实现ServeDNS也就是LocalDNSServer的ServeDNS方法

```
func (h *LocalDNSServer) ServeDNS(proxy *dnsProxy, w dns.ResponseWriter, req *dns.Msg) {
    var response *dns.Msg

    if len(req.Question) == 0 {
        response = new(dns.Msg)
        response.SetReply(req)
        response.Rcode = dns.RcodeNameError
    } else {
        // 获取 LookupTable
        lp := h.lookupTable.Load()
        if lp == nil {
            response = new(dns.Msg)
            response.SetReply(req)
            response.Rcode = dns.RcodeNameError
            _ = w.WriteMsg(response)
            return
        }
        lookupTable := lp.(*LookupTable)
        var answers []dns.RR

        //获取要解析的主机名
        hostname := strings.ToLower(req.Question[0].Name)
        // 判断LookupTable中是否存在,存在则直接返回
        answers, hostFound := lookupTable.lookupHost(req.Question[0].Qtype, hostname)

        if hostFound {
            response = new(dns.Msg)
            response.SetReply(req)
            response.Answer = answers
            if len(answers) == 0 {
                // 我们在预编译的已知主机列表中找到了该主机,但是该查询类型没有有效记录.所以返回NXDOMAIN
                response.Rcode = dns.RcodeNameError
            }
        } else {
            // 我们没有在内部缓存中找到主机.向上游查询并按原样返回响应。
            response = h.queryUpstream(proxy.upstreamClient, req)
        }
    }

    _ = w.WriteMsg(response)
}
```

#### 更新本地缓存

```
if p.localDNSServer != nil && len(resp.Resources) > 0 {
    var nt nds.NameTable
    // TODO we should probably send ACK and not update nametable here
    if err = ptypes.UnmarshalAny(resp.Resources[0], &nt); err != nil {
        log.Errorf("failed to unmarshall name table: %v", err)
    }
    p.localDNSServer.UpdateLookupTable(&nt)
}
```

通过UpdateLookupTable更新localDNSServer数据

### pilot-discovery

在istio的xds实现中分为以下两个channel

* reqChannel 用于处理xds请求
* pushChannel 用于主动推送xds数据变更

他们最终都将调用 pushXds

#### pushXds

```
func (s *DiscoveryServer) pushXds(con *Connection, push *model.PushContext,
    currentVersion string, w *model.WatchedResource, req *model.PushRequest) error {
    if w == nil {
        return nil
    }
    gen := s.findGenerator(w.TypeUrl, con)
    if gen == nil {
        return nil
    }

    t0 := time.Now()

    cl := gen.Generate(con.proxy, push, w, req)
    if cl == nil {
        // If we have nothing to send, report that we got an ACK for this version.
        if s.StatusReporter != nil {
            s.StatusReporter.RegisterEvent(con.ConID, w.TypeUrl, push.Version)
        }
        return nil // No push needed.
    }
    defer func() { recordPushTime(w.TypeUrl, time.Since(t0)) }()

    resp := &discovery.DiscoveryResponse{
        TypeUrl:     w.TypeUrl,
        VersionInfo: currentVersion,
        Nonce:       nonce(push.Version),
        Resources:   cl,
    }

    err := con.send(resp)
    if err != nil {
        recordSendError(w.TypeUrl, con.ConID, err)
        return err
    }

    // Some types handle logs inside Generate, skip them here
    if _, f := SkipLogTypes[w.TypeUrl]; !f {
        adsLog.Infof("%s: PUSH for node:%s resources:%d", v3.GetShortType(w.TypeUrl), con.proxy.ID, len(cl))
    }
    return nil
}
```

通过对应的TypeUrl,获取对应的Generator

```
s.findGenerator(w.TypeUrl, con)
```

然后从根据客户端的配置生产成对应的资源

```
cl := gen.Generate(con.proxy, push, w, req)
```

对于nds服务发现类型其对应的url为`type.googleapis.com/istio.networking.nds.v1.NameTable` 其对应的Generate实现为

```
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
}
```

其主要逻辑为BuildNameTable,获取nametable序列化后返回给客户端

#### BuildNameTable

BuildNameTable生成一个主机名及其关联的IP的表,然后代理可以使用该表来解析DNS.此逻辑始终处于活动状态.但是,只有在代理中启用DNS捕获后,本地DNS解析才会生效

```
func (configgen *ConfigGeneratorImpl) BuildNameTable(node *model.Proxy, push *model.PushContext) *nds.NameTable {
    // 只对sidecar类型的代理生效
    if node.Type != model.SidecarProxy {
        return nil
    }

    out := &nds.NameTable{
        Table: map[string]*nds.NameTable_NameInfo{},
    }

    for _, svc := range push.Services(node) {
        目前无法解析泛域名
        if svc.Hostname.IsWildCarded() {
            continue
        }

        svcAddress := svc.GetServiceAddressForProxy(node, push)
        var addressList []string

        //对于headless svc或者对于自动分配的serviceentry此处未指定IP,缺乏对有状态应用解析的支持
        if svcAddress == constants.UnspecifiedIP {
            // 用ep填充
            if svc.Attributes.ServiceRegist ry == string(serviceregistry.Kubernetes) &&
                svc.Resolution == model.Passthrough && len(svc.Ports) > 0 {
                for _, instance := range push.ServiceInstancesByPort(svc, svc.Ports[0].Port, nil) {
                    addressList = append(addressList, instance.Endpoint.Address)
                }
            }

            if len(addressList) == 0 {
                continue
            }
        } else {
            addressList = append(addressList, svcAddress)
        }

        nameInfo := &nds.NameTable_NameInfo{
            Ips:      addressList,
            Registry: svc.Attributes.ServiceRegistry,
        }
        if svc.Attributes.ServiceRegistry == string(serviceregistry.Kubernetes) {
            nameInfo.Namespace = svc.Attributes.Namespace
            nameInfo.Shortname = svc.Attributes.Name
        }
        out.Table[string(svc.Hostname)] = nameInfo
    }
    return out
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rocdu.gitbook.io/deep-understanding-of-istio/6/2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
