<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; CNI</title>
	<atom:link href="https://blog.gmem.cc/tag/cni/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 02:15:06 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Galaxy学习笔记</title>
		<link>https://blog.gmem.cc/galaxy-study-note</link>
		<comments>https://blog.gmem.cc/galaxy-study-note#comments</comments>
		<pubDate>Sat, 01 Aug 2020 11:28:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=36521</guid>
		<description><![CDATA[<p>简介 Galaxy是TKEStack的一个网络组件，支持为TKE集群提供Overlay/Underlay容器网络。Galaxy的一个特性是能够提供浮动IP（弹性IP） —— 即使Pod因为节点宕机而漂移到其它节点，其IP地址也能够保持不变。 网络类型 Galaxy提供四类网络，支持为每个工作负载单独配置网络模式。 Overlay网络 默认模式，基于Flannel的VXLAN或者Host Gateway（路由）方案。同节点容器通信不走网桥，报文直接利用主机路由转发，跨节点容器通信利用VXLAN协议封装或者直接路由，节点间路由记录在Etcd中。优点：简单可靠，性能不错，支持网络策略。 tke-installer默认安装的TKEStack会自动配置Galaxy为Overlay模式，CNI配置： [crayon-69d36c808766f556708978/] 在该模式下： Flannel在每个Kubelet节点上分配一个子网，并将其保存在etcd和本地路径[crayon-69d36c8087676141014147-i/] Kubelet调用Galaxy的CNI插件galaxy-sdn，该插件会通过UDS调用本机的Galaxy进程 Galaxy进程则又调用Flannel CNI，解析/run/flannel/subnet.env中的子网信息 Flannel CNI会调用： Bridge CNI或Veth CNI来为POD配置网络 调用host-lo <a class="read-more" href="https://blog.gmem.cc/galaxy-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/galaxy-study-note">Galaxy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Galaxy是TKEStack的一个网络组件，支持为TKE集群提供Overlay/Underlay容器网络。Galaxy的一个特性是能够提供浮动IP（弹性IP） —— 即使Pod因为节点宕机而漂移到其它节点，其IP地址也能够保持不变。</p>
<div class="blog_h2"><span class="graybg">网络类型</span></div>
<p>Galaxy提供四类网络，支持为每个工作负载单独配置网络模式。</p>
<div class="blog_h3"><span class="graybg">Overlay网络</span></div>
<p>默认模式，基于Flannel的VXLAN或者Host Gateway（路由）方案。<span style="background-color: #c0c0c0;">同节点容器通信不走网桥，报文直接利用主机路由转发</span>，<span style="background-color: #c0c0c0;">跨节点容器通信利用VXLAN协议封装或者直接路由</span>，<span style="background-color: #c0c0c0;">节点间路由记录在Etcd</span>中。优点：简单可靠，性能不错，支持网络策略。</p>
<p>tke-installer默认安装的TKEStack会自动配置Galaxy为Overlay模式，CNI配置：</p>
<pre class="crayon-plain-tag">{
  "type": "galaxy-sdn",
  "capabilities": {"portMappings": true},
  "cniVersion": "0.2.0"
} </pre>
<p>在该模式下：</p>
<ol>
<li>Flannel在每个Kubelet节点上分配一个子网，并将其保存在etcd和本地路径<pre class="crayon-plain-tag">/run/flannel/subnet.env</pre></li>
<li>Kubelet调用Galaxy的CNI插件galaxy-sdn，该插件会通过UDS调用本机的Galaxy进程</li>
<li>Galaxy进程则又调用Flannel CNI，解析/run/flannel/subnet.env中的子网信息</li>
<li>Flannel CNI会调用：
<ol>
<li>Bridge CNI或Veth CNI来为POD配置网络</li>
<li>调用host-lo</li>
</ol>
</li>
</ol>
<p>架构图：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/07/galaxy-overlay.png"><img class="wp-image-33845 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/07/galaxy-overlay.png" alt="galaxy-overlay" width="1013" height="455" /></a></p>
<div class="blog_h3"><span class="graybg">Underlay网络</span></div>
<p>该模式下，容器IP由宿主机网络提供，容器与宿主机可以直接路由，性能更好。<span style="background-color: #c0c0c0;">支持基于Linux Bridge/MacVlan/IPVlan和SRIOV的容器-宿主机二层联通</span>，可以根据业务场景和硬件环境，具体选择使用哪种网桥。</p>
<p>要使用Galaxy Underlay网络，需要启用Galaxy-ipam组件，该组件为Pod分配IP，浮动IP的能力也由它提供。</p>
<div class="blog_h3"><span class="graybg">NAT</span></div>
<p>利用K8S的hostPort配置，将容器端口映射到宿主机端口。如果不指定hostPort，Galaxy进行随机映射。</p>
<div class="blog_h3"><span class="graybg">Host</span></div>
<p>利用K8S的HostNetwork模式，直接使用物理网络。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Galaxy有三类组件构成。</p>
<div class="blog_h3"><span class="graybg">Galaxy</span></div>
<p>以DaemonSet方式运行在每个节点上，通过调用各种CNI插件来配置容器网络。</p>
<div class="blog_h3"><span class="graybg">CNI插件</span></div>
<p>Galaxy将实际的创建CNI的工作委托给其它CNI插件，也就是说它扮演一个装饰器的角色。Galaxy支持任何标准CNI插件，它也内置了若干CNI插件。</p>
<div class="blog_h3"><span class="graybg">Galaxy IPAM</span></div>
<p>是一个K8S Scheduler插件。kube-scheduler通过HTTP调用Galaxy-ipam，实现浮动IP的配置和管理。</p>
<p>因为仅仅在重新调度Pod的时候才会面临IP地址变化的问题，因此这个组件实现为Scheduler插件就很自然了。</p>
<div class="blog_h2"><span class="graybg">支持的CNI列表</span></div>
<p>Galaxy支持任何标准的CNI插件，你可以将其用作一个CNI框架，就像<a href="https://github.com/intel/multus-cni">multus-cni</a>那样。</p>
<p>Galaxy还包含了一系列内置的CNI插件：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SDN CNI</td>
<td>使用Overlay网络时，此插件是CNI的入口。该插件会通过UDS调用Galaxy守护进程，转发Kubelet提供的所有CNI参数</td>
</tr>
<tr>
<td>Veth CNI</td>
<td>用于Overlay网络中，创建一个VETH对来连接容器和宿主机命名空间。该插件从ipam插件得到Pod的IP</td>
</tr>
<tr>
<td>underlay-veth CNI</td>
<td>用于Underlay网络，创建VETH对来连接容器和宿主机命名空间，该插件不会创建网桥，而是使用宿主机路由规则来进行封包转发</td>
</tr>
<tr>
<td>Vlan CNI</td>
<td>
<p>用于Underlay网络，创建一个VETH对，连接容器和宿主机上的bridge/macvlan/ipvlan设备</p>
<p>此插件支持为Pod配置VLAN，它能够从CNI参数，例如ipinfos=[{"ip":"192.168.0.68/26","vlan":2,"gateway":"192.168.0.65"}] ，或者ipam CNI插件的结果中获得IP地址</p>
</td>
</tr>
<tr>
<td>SRIOV CNI</td>
<td>用于Underlay网络，利用以太网服务器适配器（Ethernet Server Adapter）的SR-IOV，能够创建VF设备并将其放入容器的网络命名空间</td>
</tr>
<tr>
<td>TKE route ENI CNI</td>
<td>用于腾讯云</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">构建Galaxy</span></div>
<div class="blog_h3"><span class="graybg">二进制文件</span></div>
<pre class="crayon-plain-tag">go get -d tkestack.io/galaxy
cd $GOPATH/src/tkestack.io/galaxy

# 构建所有二进制文件
make
# 构建指定二进制文件
make BINS="galxy galxy-ipam"
make BINS="galxy-ipam"</pre>
<div class="blog_h3"><span class="graybg">镜像</span></div>
<pre class="crayon-plain-tag"># 构建所有镜像
make image

# 构建指定镜像
make image BINS="galxy-ipam"

# 为指定体系结构构建
make image.multiarch PLATFORMS="linux_arm64" </pre>
<div class="blog_h2"><span class="graybg">使用Galaxy</span></div>
<p>清单文件可以<a href="https://raw.githubusercontent.com/tkestack/galaxy/master/yaml/galaxy.yaml">到GitHub下载</a>，默认内容如下：</p>
<pre class="crayon-plain-tag">---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: galaxy
rules:
- apiGroups: [""]
  resources:
  - pods
  - namespaces
  - nodes
  - pods/binding
  verbs: ["list", "watch", "get", "patch", "create", "update"]
- apiGroups: ["apps", "extensions"]
  resources:
  - statefulsets
  - deployments
  verbs: ["list", "watch"]
- apiGroups: [""]
  resources:
  - configmaps
  - endpoints
  - events
  verbs: ["get", "list", "watch", "update", "create", "patch"]
- apiGroups: ["galaxy.k8s.io"]
  resources:
  - pools
  - floatingips
  verbs: ["get", "list", "watch", "update", "create", "patch", "delete"]
- apiGroups: ["apiextensions.k8s.io"]
  resources:
  - customresourcedefinitions
  verbs:
  - "*"
- apiGroups: ["networking.k8s.io"]
  resources:
  - networkpolicies
  verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: galaxy
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: galaxy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: galaxy
subjects:
  - kind: ServiceAccount
    name: galaxy
    namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: galaxy
  name: galaxy
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: galaxy
  template:
    metadata:
      labels:
        app: galaxy
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: galaxy
      hostNetwork: true
      hostPID: true
      containers:
      - image: tkestack/galaxy:v1.0.7
        command: ["/bin/sh"]
# 入口点脚本：
#   拷贝来自ConfigMap的Galaxy配置到宿主机
#   cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; 
#   拷贝CNI插件二进制文件到宿主机
#   cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; 
#   启动Galaxy守护进程                        腾讯云中运行要这个参数
#   /usr/bin/galaxy --logtostderr=true --v=3 --route-eni
      # qcloud galaxy should run with --route-eni
        args: ["-c", "cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; /usr/bin/galaxy --logtostderr=true --v=3 --route-eni"]
      # private-cloud should run without --route-eni
      # args: ["-c", "cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; /usr/bin/galaxy --logtostderr=true --v=3"]
        imagePullPolicy: Always
        env:
          - name: MY_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: DOCKER_HOST
            value: unix:///host/run/docker.sock
        name: galaxy
        resources:
          requests:
            cpu: 100m
            memory: 200Mi
        securityContext:
          privileged: true
        volumeMounts:
        - name: galaxy-run
          mountPath: /var/run/galaxy/
        - name: flannel-run
          mountPath: /run/flannel
        - name: galaxy-etc
          mountPath: /etc/galaxy
        - name: cni-config
          mountPath: /etc/cni/net.d/
        - name: cni-bin
          mountPath: /opt/cni/bin
        - name: cni-etc
          mountPath: /etc/galaxy/cni
        - name: cni-state
          mountPath: /var/lib/cni
        - name: docker-sock
          mountPath: /host/run/
        - name: tz-config
          mountPath: /etc/localtime
      terminationGracePeriodSeconds: 30
      tolerations:
      - operator: Exists
      volumes:
      - name: galaxy-run
        hostPath:
          path: /var/run/galaxy
      - name: flannel-run
        hostPath:
          path: /run/flannel
      - configMap:
          defaultMode: 420
          name: galaxy-etc
        name: galaxy-etc
      - name: cni-config
        hostPath:
          path: /etc/cni/net.d/
      - name: cni-bin
        hostPath:
          path: /opt/cni/bin
      - name: cni-state
        hostPath:
          path: /var/lib/cni
      - configMap:
          defaultMode: 420
          name: cni-etc
        name: cni-etc
      - name: docker-sock
        # in case of docker restart, /run/docker.sock may change, we have to mount the /run directory
        hostPath:
          path: /run/
      - name: tz-config
        hostPath:
          path: /etc/localtime
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: galaxy-etc
  namespace: kube-system
data:
  # Galaxy配置文件
  # update network card name in "galaxy-k8s-vlan" and "galaxy-k8s-sriov" if necessary
  # update vf_num in "galaxy-k8s-sriov" according to demand
  # update ENIIPNetwork to tke-route-eni if running on qcloud
  galaxy.json: |
    {
      "NetworkConf":[
        {"name":"tke-route-eni","type":"tke-route-eni","eni":"eth1","routeTable":1},
        {"name":"galaxy-flannel","type":"galaxy-flannel", "delegate":{"type":"galaxy-veth"},"subnetFile":"/run/flannel/subnet.env"},
        {"name":"galaxy-k8s-vlan","type":"galaxy-k8s-vlan", "device":"eth1", "default_bridge_name": "br0"},
        {"name":"galaxy-k8s-sriov","type": "galaxy-k8s-sriov", "device": "eth1", "vf_num": 10}
      ],
      "DefaultNetworks": ["galaxy-flannel"],
      "ENIIPNetwork": "galaxy-k8s-vlan"
    }
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cni-etc
  namespace: kube-system
data:
  # CNI网络配置
  00-galaxy.conf: |
    {
      "name": "galaxy-sdn",
      "type": "galaxy-sdn",
      "capabilities": {"portMappings": true},
      "cniVersion": "0.2.0"
    }</pre>
<p>根据实际运行环境、使用的网络模式，需要进行调整。</p>
<div class="blog_h2"><span class="graybg">配置Kubelet</span></div>
<p>需要保证命令行标记：<pre class="crayon-plain-tag">--network-plugin=cni --cni-bin-dir=/opt/cni/bin/</pre>，并重启Kubelet。</p>
<div class="blog_h2"><span class="graybg">Overlay网络</span></div>
<div class="blog_h3"><span class="graybg">安装Flannel</span></div>
<p>这种网络模式依赖于Flannel，参考<a href="/flannel#install">Flannel学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">配置Galaxy</span></div>
<div class="blog_h3"><span class="graybg">全局配置</span></div>
<p>你需要修改上述Galaxy清单中的galaxy-etc，来配置默认网络（DefaultNetworks）：</p>
<pre class="crayon-plain-tag">{
  // 所有支持的网络模式的配置
  "NetworkConf":[
    {
      "name":"tke-route-eni", // 如果name为空，Galaxy假设它和type相同
      "type":"tke-route-eni",
      "eni":"eth1",
      "routeTable":1
    },
    {
      "name":"galaxy-flannel", // 这个插件就是flannel的CNI，只是被改了名字
      "type":"galaxy-flannel",
      "delegate":{
        "type":"galaxy-veth"
      },
      "subnetFile":"/run/flannel/subnet.env"
    },
    {
      "name":"galaxy-k8s-vlan",
      "type":"galaxy-k8s-vlan",
      "device":"eth1",
      "default_bridge_name":"br0"
    },
    {
      "name":"galaxy-k8s-sriov",
      "type":"galaxy-k8s-sriov",
      "device":"eth1",
      "vf_num":10
    },
    {
      "name":"galaxy-underlay-veth",
      "type":"galaxy-underlay-veth",
      "device":"eth1"
    }
  ],
  // Pod默认使用这个网络，注意可以加入多个网络
  "DefaultNetworks":[
    "galaxy-flannel" // 使用基于Flannel的Overlay网络
  ],
  // 如果Pod需要腾讯云弹性网卡（ENI）而且没有配置k8s.v1.cni.cncf.io/networks注解，则
  // 默认使用此网络。这个配置可以避免需要为所有需要Underlay网络的Pod添加注解
  "ENIIPNetwork":"galaxy-k8s-vlan"
}</pre>
<div class="blog_h3"><span class="graybg">Pod配置</span></div>
<p>可以为Pod指定注解： </p>
<pre class="crayon-plain-tag">k8s.v1.cni.cncf.io/networks: galaxy-flannel,galaxy-k8s-sriov</pre>
<p>来提示Galaxy应该为它配置哪些网络。 </p>
<div class="blog_h2"><span class="graybg">和其它CNI插件共存</span></div>
<p>Galaxy可以和其它CNI插件同时存在。需要注意的一点是，--network-conf-dir目录下其它插件的配置文件，其文件名的字典排序不应该比00-galaxy.conf更小，<span style="background-color: #c0c0c0;">否则Kubelet会在调用Galaxy CNI插件之前，调用其它CNI插件</span>，这可能不符合预期。</p>
<div class="blog_h2"><span class="graybg">Galaxy命令行标记</span></div>
<pre class="crayon-plain-tag">--alsologtostderr                   # 记录日志到文件，同时记录到标准错误
--bridge-nf-call-iptables           # 是否配置 bridge-nf-call-iptables，启用/禁用对网桥转发的封包被iptables规则过滤，默认true
--cni-paths stringSlice             # 除了从kubelet接收的，额外的CNI路径，默认/opt/cni/galaxy/bin
--flannel-allocated-ip-dir string   # Flannel CNI插件在何处存放分配的IP地址，默认/var/lib/cni/networks
--flannel-gc-interval duration      # 执行Flannel网络垃圾回收的间隔，默认10s
--gc-dirs string                    # 清理哪些目录，这些目录中的文件名包含容器ID
                                    # 默认 /var/lib/cni/flannel,/var/lib/cni/galaxy,/var/lib/cni/galaxy/port
--hostname-override string          # 覆盖kubelet hostname，如果指定该参数，则Galaxy使用它从API Server得到节点对象
--ip-forward                        # 是否启用IP转发
--json-config-path string           # Galaxy配置文件路径，默认/etc/galaxy/galaxy.json
--kubeconfig string                 # Kubelet配置文件
--log-backtrace-at traceLocation    # 如果日志在file:N打印，打印栈追踪。默认default :0
--log-dir string                    # 日志输出目录
--log-flush-frequency duration      # 日志刷出间隔，默认5s
--logtostderr                       # 输出到标准错误而非文件
--master string                     # API Server的地址和端口
--network-conf-dir string           # 额外的CNI网络配置文件。默认/etc/cni/net.d/
--network-policy                    # 启用网络策略支持
--route-eni                         # 是否启用腾讯云route-eni
--stderrthreshold severity          # 以上级别的日志打印到标准错误。默认2
-v, --v Level                       # 日志冗长级别 </pre>
<div class="blog_h1"><span class="graybg">Underlay网络</span></div>
<div class="blog_h2"><span class="graybg">工作原理</span></div>
<p>Underlay网络必须配合Galaxy-ipam使用，Galaxy-ipam负责为Pod分配或释放IP地址：</p>
<ol>
<li>你需要规划容器网络中使用的Underlay IP范围（注意和物理网路上其它节点的IP冲突），并且配置到ConfigMap floatingip-config中</li>
<li>调度Pod时，kube-scheduler会在filter/priority/bind方法中调用Galaxy-ipam</li>
<li>Galaxy-ipam检查Pod是否配置了Reserved IP，如果是，则Galaxy-ipam仅将此IP所在的可用子网的节点标记为有效节点，否则所有节点都将被标记为有效节点。在Pod绑定IP期间，Galaxy-ipam分配一个IP并将其写入到Pod annotations中</li>
<li>Galaxy从Pod的注解中获得IP，并将其作为参数传递给CNI，通过CNI配置Pod IP</li>
</ol>
<p>Galaxy的浮动IP能力由Galaxy-ipam提供，后者是一个<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/extend-cluster/#scheduler-extensions">Kubernetes Scheudler Extender</a>，K8S的调度器会调用Galaxy-ipam，影响其filtering/binding的过程。浮动IP必须配合Underlay网络使用。</p>
<p>整体工作流图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/07/galaxy-ipam.png"><img class="wp-image-33853 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/07/galaxy-ipam.png" alt="galaxy-ipam" width="1007" height="485" /></a></p>
<div class="blog_h2"><span class="graybg">安装Galaxy-ipam</span></div>
<p>清单文件可以到GitHub下载，默认内容如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: galaxy-ipam
  namespace: kube-system
  labels:
    app: galaxy-ipam
spec:
  type: NodePort
  ports:
  - name: scheduler-port
    port: 9040
    targetPort: 9040
    nodePort: 32760
    protocol: TCP
  - name: api-port
    port: 9041
    targetPort: 9041
    nodePort: 32761
    protocol: TCP
  selector:
    app: galaxy-ipam
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: galaxy-ipam
rules:
- apiGroups: [""]
  resources:
  - pods
  - namespaces
  - nodes
  - pods/binding
  verbs: ["list", "watch", "get", "patch", "create"]
- apiGroups: ["apps", "extensions"]
  resources:
  - statefulsets
  - deployments
  verbs: ["list", "watch"]
- apiGroups: [""]
  resources:
  - configmaps
  - endpoints
  - events
  verbs: ["get", "list", "watch", "update", "create", "patch"]
- apiGroups: ["galaxy.k8s.io"]
  resources:
  - pools
  - floatingips
  verbs: ["get", "list", "watch", "update", "create", "patch", "delete"]
- apiGroups: ["apiextensions.k8s.io"]
  resources:
  - customresourcedefinitions
  verbs:
  - "*"
- apiGroups: ["apps.tkestack.io"]
  resources:
  - tapps
  verbs: ["list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: galaxy-ipam
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: galaxy-ipam
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: galaxy-ipam
subjects:
  - kind: ServiceAccount
    name: galaxy-ipam
    namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: galaxy-ipam
  name: galaxy-ipam
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: galaxy-ipam
  template:
    metadata:
      labels:
        app: galaxy-ipam
    spec:
      priorityClassName: system-cluster-critical
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - galaxy-ipam
            topologyKey: "kubernetes.io/hostname"
      serviceAccountName: galaxy-ipam
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - image: tkestack/galaxy-ipam:v1.0.7
        args:
          - --logtostderr=true
          - --profiling
          - --v=3
          - --config=/etc/galaxy/galaxy-ipam.json
          # 这个服务暴露一个端口，供Scheduler调用
          - --port=9040
          - --api-port=9041
          - --leader-elect
        command:
          - /usr/bin/galaxy-ipam
        ports:
          - containerPort: 9040
          - containerPort: 9041
        imagePullPolicy: Always
        name: galaxy-ipam
        resources:
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: kube-config
          mountPath: /etc/kubernetes/
        - name: galaxy-ipam-log
          mountPath: /data/galaxy-ipam/logs
        - name: galaxy-ipam-etc
          mountPath: /etc/galaxy
        - name: tz-config
          mountPath: /etc/localtime
      terminationGracePeriodSeconds: 30
      tolerations:
        - effect: NoSchedule
          key: node-role.kubernetes.io/master
          operator: Exists
      volumes:
      - name: kube-config
        hostPath:
          path: /etc/kubernetes/
      - name: galaxy-ipam-log
        emptyDir: {}
      - configMap:
          defaultMode: 420
          name: galaxy-ipam-etc
        name: galaxy-ipam-etc
      - name: tz-config
        hostPath:
          path: /etc/localtime
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: galaxy-ipam-etc
  namespace: kube-system
data:
  # 配置文件
  galaxy-ipam.json: |
    {
      "schedule_plugin": {
        # 如果不是使用腾讯云的弹性网卡（ENI），去掉这一行
        "cloudProviderGrpcAddr": "127.0.0.2:80"
      }
    }</pre>
<div class="blog_h2"><span class="graybg">配置kube-scheduler</span></div>
<p>你需要为K8S调度器配置调度策略，此策略现在可以放在ConfigMap中：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: scheduler-policy
  namespace: kube-system
data:
  policy.cfg: |
    {
      "kind": "Policy",
      "apiVersion": "v1",
      "extenders": [
        {
          // 通过HTTP调用扩展
          "urlPrefix": "http://127.0.0.1:9040/v1",
          "httpTimeout": 10000000000,
          "filterVerb": "filter",
          "BindVerb": "bind",
          "weight": 1,
          "enableHttps": false,
          "managedResources": [
            {
              "name": "tke.cloud.tencent.com/eni-ip",
              "ignoredByScheduler": false
            }
          ]
        }
      ]
    }
EOF</pre>
<p>然后为调度器添加命令行标记：<pre class="crayon-plain-tag">--policy-configmap=scheduler-policy</pre>即可。</p>
<div class="blog_h2"><span class="graybg">配置浮动IP</span></div>
<div class="blog_h3"><span class="graybg">floatingip-config</span></div>
<p>运行在裸金属环境下时，可以使用如下配置：</p>
<pre class="crayon-plain-tag">kind: ConfigMap
apiVersion: v1
metadata:
 name: floatingip-config
 namespace: kube-system
data:
 floatingips: |
    [
        { 
            # 节点的CIDR，这个网络范围内的节点可以运行具有浮动IP的Pod
            "nodeSubnets": ["10.0.0.0/16"],
            # 可以分配给Pod的IP地址范围
            "ips": ["10.0.70.2~10.0.70.241"],
            # Pod IP子网信息
            "subnet":"10.0.70.0/24",
            # Pod IP网关信息
            "gateway":"10.0.70.1",
            # Pod IP所属VLAN，如果Pod IP和Node IP不在同一VLAN，需要设置该字段，同时
            # 确保节点所连接的交换机时一个trunk port
            "vlan": 1024
        }
    ]</pre>
<p>一个nodeSubnet可以对应多个Pod Subnet，例如：</p>
<pre class="crayon-plain-tag">// 如果Pod运行在10.49.28.0/26，那么它可以具有10.0.80.2~10.0.80.4或者10.0.81.2~10.0.81.4的IP地址
// 如果Pod运行在10.49.29.0/24，则只能具有10.0.80.0/24的IP地址
[{
	"nodeSubnets": ["10.49.28.0/26", "10.49.29.0/24"],
	"ips": ["10.0.80.2~10.0.80.4"],
	"subnet": "10.0.80.0/24",
	"gateway": "10.0.80.1"
}, {
	"nodeSubnets": ["10.49.28.0/26"],
	"ips": ["10.0.81.2~10.0.81.4"],
	"subnet": "10.0.81.0/24",
	"gateway": "10.0.81.1",
	"vlan": 3
}]</pre>
<p>只要可分配的IP地址范围不重叠，多个nodeSubnet可以共享同一个Pod Subnet：</p>
<pre class="crayon-plain-tag">[{
	"routableSubnet": "10.180.1.2/32",
	"ips": ["10.180.154.2~10.180.154.3"],
	"subnet": "10.180.154.0/24",
	"gateway": "10.180.154.1",
	"vlan": 3
}, {
	"routableSubnet": "10.180.1.3/32",
	"ips": ["10.180.154.7~10.180.154.8"],
	"subnet": "10.180.154.0/24",
	"gateway": "10.180.154.1",
	"vlan": 3
}]</pre>
<div class="blog_h3"><span class="graybg">保留IP</span></div>
<p>要保留一个IP不被分配，可以使用下面的FloatingIP资源： </p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: FloatingIP
metadata:
  # 名字是需要保留的IP
  name: 10.0.0.1
  labels:
    # 从1.0.8下面这个标签不再需要
    ipType: internalIP
    # 这个标签必须保留
    reserved: this-is-not-for-pods
spec:
  key: pool__reserved-for-node_
  policy: 2</pre>
<div class="blog_h3"><span class="graybg">Deployment配置</span></div>
<p>目前浮动IP仅仅支持Deployment、StatefulSet产生的工作负载。</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    metadata:
      annotations:
        # 如果默认网络不是galaxy-k8s-vlan则必须添加下面的注解
        k8s.v1.cni.cncf.io/networks: galaxy-k8s-vlan
        # IP释放策略：
        #    为空/不指定：Pod一旦停止，就释放IP
        #    immutable：仅仅在删除、缩容Deployment / StatefulSet的情况下才释放IP
        #    never：即使Deployment / StatefulSet被删除，也不会释放IP。后续的同名Deployment
        #           /StatefulSet会重用已分配的IP
        k8s.v1.cni.galaxy.io/release-policy: immutable
      creationTimestamp: null
      labels:
        k8s-app: nnn
        qcloud-app: nnn
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nnn
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
            # 扩展的资源限制，在某个容器中配置即可
            tke.cloud.tencent.com/eni-ip: "1"
          requests:
            cpu: 250m
            memory: 256Mi
            tke.cloud.tencent.com/eni-ip: "1"</pre>
<p>生成的Pod中，通过注解<pre class="crayon-plain-tag">k8s.v1.cni.galaxy.io/args</pre>指明了它的IP地址等信息。CNI插件也是基于这些信息为容器网络命名空间设置IP地址、路由的：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: nnn-7df5984746-58hjm
  annotations:
    k8s.v1.cni.cncf.io/networks: galaxy-k8s-vlan
    k8s.v1.cni.galaxy.io/args: '{"common":{"ipinfos":[{"ip":"192.168.64.202/24",
        "vlan":0,"gateway":"192.168.64.1","routable_subnet":"172.21.64.0/20"}]}}'
    k8s.v1.cni.galaxy.io/release-policy: immutable
...
spec:
...
status:
...
  hostIP: 172.21.64.15
  phase: Running
  podIP: 192.168.64.202
  podIPs:
  - ip: 192.168.64.202</pre>
<div class="blog_h2"><span class="graybg">Galaxy-ipam相关CRD</span></div>
<div class="blog_h3"><span class="graybg">Pool</span></div>
<p>为Deployment设置注解<pre class="crayon-plain-tag">tke.cloud.tencent.com/eni-ip-pool</pre>，可以让多个Deployment共享一个IP池。</p>
<p>使用IP池的情况下，默认的IP释放策略为never，除非通过注解<pre class="crayon-plain-tag">k8s.v1.cni.galaxy.io/release-policy</pre>指定其它策略。</p>
<p>默认情况下，IP池的大小随着Deployment/StatefulSet的副本数量的增加而增大，你可以设置固定尺寸的IP池：</p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: Pool
metadata:
  name: example-pool
size: 4</pre>
<p>通过HTTP接口创建IP池的时候，可以设置preAllocateIP=true来为池预先分配IP，通过kubectl创建CR时无法实现预分配。 </p>
<div class="blog_h3"><span class="graybg">FloatingIP</span></div>
<p>该CR保存了浮动IP，及其绑定的工作负载信息：</p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: FloatingIP
metadata:
  creationTimestamp: "2020-03-04T08:28:15Z"
  generation: 1
  labels:
    ipType: internalIP
  # IP地址
  name: 192.168.64.202
  resourceVersion: "2744910"
  selfLink: /apis/galaxy.k8s.io/v1alpha1/floatingips/192.168.64.202
  uid: b5d55f27-4548-44c7-b8ad-570814b55026
spec:
  attribute: '{"NodeName":"172.21.64.15"}'
  # 绑定的工作负载
  key: dp_default_nnn_nnn-7df5984746-58hjm
  policy: 1
  subnet: 172.21.64.0/20
  updateTime: "2020-03-04T08:28:15Z" </pre>
<div class="blog_h2"><span class="graybg">Galaxy-ipam HTTP接口</span></div>
<p>Galaxy-ipam提供了基于<a href="https://github.com/tkestack/galaxy/blob/master/doc/swagger.json">Swagger 2.0</a>的API，为galaxy-ipam提供命令行选项<pre class="crayon-plain-tag">--swagger</pre>，则它能够在下面的URL展示此API：</p>
<p style="padding-left: 30px;">http://${galaxy-ipam-ip}:9041/apidocs.json/v1</p>
<div class="blog_h3"><span class="graybg">查询分配的IP</span></div>
<pre class="crayon-plain-tag">// 查询分配给default命名空间中的StatefulSet sts的IP地址
// curl 'http://192.168.30.7:9041/v1/ip?appName=sts&amp;appType=statefulsets&amp;namespace=default'
{
 "last": true,
 "totalElements": 2,
 "totalPages": 1,
 "first": true,
 "numberOfElements": 2,
 "size": 10,
 "number": 0,
 "content": [
  {
   "ip": "10.0.0.112",
   "namespace": "default",
   "appName": "sts",
   "podName": "sts-0",
   "policy": 2,
   "appType": "statefulset",
   "updateTime": "2020-05-29T11:11:44.633383558Z",
   "status": "Deleted",
   "releasable": true
  },
  {
   "ip": "10.0.0.174",
   "namespace": "default",
   "appName": "sts",
   "podName": "sts-1",
   "policy": 2,
   "appType": "statefulset",
   "updateTime": "2020-05-29T11:11:45.132450117Z",
   "status": "Deleted",
   "releasable": true
  }
 ]
}</pre>
<div class="blog_h3"><span class="graybg">释放IP地址</span></div>
<pre class="crayon-plain-tag">// curl -X POST -H "Content-type: application/json" -d '
//   { "ips": [
//     { "ip":"10.0.0.112", "appName":"sts", "appType":"statefulset",  "podName":"sts-0","namespace":"default"},
//     {"ip":"10.0.0.174", "appName":"sts", "appType":"statefulset", "podName":"sts-1", "namespace":"default"}
//   ]}
// '
// http://192.168.30.7:9041/v1/ip
{
 "code": 200,
 "message": ""
}</pre>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">关于滚动更新</span></div>
<p>Deployment的默认更新策略是StrategyType=RollingUpdate，25% max unavailable, 25% max surge。这意味着这在滚动更新过程中，可能存在超过副本数限制25%的Pod。这可能导致死锁状态：</p>
<ol>
<li>假设Deployment副本数为3，那么有一个副本不可用，就超过25%的限制。Deployment的控制器只能先创建新Pod，然后等它就绪后再删除一个旧Pod</li>
<li>如果浮动IP的释放策略被设置为immutable/never，这就意味着新的Pod需要从一个旧Pod那复用IP，然而旧Pod还尚未删除</li>
</ol>
<p>这会导致新Pod卡斯在调度阶段。</p>
<div class="blog_h1"><span class="graybg">端口映射</span></div>
<p>使用K8S的NodePort服务，可以将一组Pod通过宿主机端口暴露到集群外部。但是如果希望访问StatefulSet的每个特定Pod的端口，使用K8S NodePort服务无法实现。</p>
<p>使用K8S的HostPort，则会存在两个Pod调度到同一节点，进而产生端口冲突的问题：</p>
<pre class="crayon-plain-tag">spec:
      containers:
      - image: ...
        ports:
          - containerPort: 9040
            hostPort: 9040</pre>
<p>Galaxy提供了一个功能，可以进行随机的端口映射： </p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: hello
  name: hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
      annotations:
        # 为Pod设置此注解即可
        tkestack.io/portmapping: ""
    spec:
      containers:
      - image: ...
        ports:
          - containerPort: 9040</pre>
<p>这样Galaxy会利用iptables，随机的映射宿主机的端口到Pod的每个容器端口，并且将此随机端口回填到tkestack.io/portmapping注解中。</p>
<div class="blog_h1"><span class="graybg">源码解读</span></div>
<div class="blog_h2"><span class="graybg">CNI</span></div>
<div class="blog_h3"><span class="graybg">galaxy-sdn</span></div>
<p>在TKEStack的缺省配置（基于Flannel的VXLAN模式的Overlay网络）下，每个节点只有一个CNI配置：</p>
<pre class="crayon-plain-tag">{
  "type": "galaxy-sdn",
  "capabilities": {"portMappings": true},
  "cniVersion": "0.2.0"
}</pre>
<p>我们先看看这个CNI插件做的什么事情，又是如何和Flannel交互的。Galaxy所有自带的CNI插件都位于github.com/tkestack/galaxy/cni目录下，其中galaxy-sdn插件入口点如下：</p>
<pre class="crayon-plain-tag">package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"

	"github.com/containernetworking/cni/pkg/skel"
	// 使用0.20版本的CNI
	t020 "github.com/containernetworking/cni/pkg/types/020"
	"github.com/containernetworking/cni/pkg/version"
	galaxyapi "tkestack.io/galaxy/pkg/api/galaxy"
	"tkestack.io/galaxy/pkg/api/galaxy/private"
)

type cniPlugin struct {
	socketPath string
}

func NewCNIPlugin(socketPath string) *cniPlugin {
	return &amp;cniPlugin{socketPath: socketPath}
}

// Create and fill a CNIRequest with this plugin's environment and stdin which
// contain the CNI variables and configuration
func newCNIRequest(args *skel.CmdArgs) *galaxyapi.CNIRequest {
	envMap := make(map[string]string)
	for _, item := range os.Environ() {
		idx := strings.Index(item, "=")
		if idx &gt; 0 {
			envMap[strings.TrimSpace(item[:idx])] = item[idx+1:]
		}
	}

	return &amp;galaxyapi.CNIRequest{
		Env:    envMap,
		Config: args.StdinData,
	}
}

// 调用Galaxy守护进程
func (p *cniPlugin) doCNI(url string, req *galaxyapi.CNIRequest) ([]byte, error) {
	data, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal CNI request %v: %v", req, err)
	}
	// 通过UDS连接
	client := &amp;http.Client{
		Transport: &amp;http.Transport{
			Dial: func(proto, addr string) (net.Conn, error) {
				return net.Dial("unix", p.socketPath)
			},
		},
	}

	resp, err := client.Post(url, "application/json", bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("failed to send CNI request: %v", err)
	}
	defer resp.Body.Close() // nolint: errcheck

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read CNI result: %v", err)
	}

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("galaxy returns: %s", string(body))
	}

	return body, nil
}

// 就是将CNI请求转发给Galaxy守护进程处理
func (p *cniPlugin) CmdAdd(args *skel.CmdArgs) (*t020.Result, error) {
	// 由于通过UDS通信，URL的hostname部分随便写了一个dummy，就是简单的调用/cni这个Endpoint
	body, err := p.doCNI("http://dummy/cni", newCNIRequest(args))
	if err != nil {
		return nil, err
	}

	result := &amp;t020.Result{}
	if err := json.Unmarshal(body, result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response '%s': %v", string(body), err)
	}

	return result, nil
}

// Send the ADD command environment and config to the CNI server, printing
// the IPAM result to stdout when called as a CNI plugin
func (p *cniPlugin) skelCmdAdd(args *skel.CmdArgs) error {
	result, err := p.CmdAdd(args)
	if err != nil {
		return err
	}
	return result.Print()
}

// Send the DEL command environment and config to the CNI server
func (p *cniPlugin) CmdDel(args *skel.CmdArgs) error {
	_, err := p.doCNI("http://dummy/cni", newCNIRequest(args))
	return err
}

// 使用skel框架开发
func main() {
	// 传入UDS套接字的路径
	p := NewCNIPlugin(private.GalaxySocketPath)
	skel.PluginMain(p.skelCmdAdd, p.CmdDel, version.Legacy)
}</pre>
<p>可以看到galaxy-sdn就是个空壳子，所有工作都是委托给Galaxy守护进程处理的。 </p>
<div class="blog_h3"><span class="graybg">galaxy-k8s-vlan</span></div>
<p>整体流程：</p>
<pre class="crayon-plain-tag">func init() {
	// this ensures that main runs only on main thread (thread group leader).
	// since namespace ops (unshare, setns) are done for a single thread, we
	// must ensure that the goroutine does not jump from OS thread to thread
	runtime.LockOSThread()
	_, pANet, _ = net.ParseCIDR("10.0.0.0/8")
	_, pBNet, _ = net.ParseCIDR("172.16.0.0/12")
	_, pCNet, _ = net.ParseCIDR("192.168.0.0/16")
}

func main() {
	d = &amp;vlan.VlanDriver{}
	skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

func cmdAdd(args *skel.CmdArgs) error {
	// 加载CNI配置文件
	conf, err := d.LoadConf(args.StdinData)
	if err != nil {
		return err
	}
	// 首先尝试从args获得IPInfo，如果失败则调用第三方IPAM插件分配IP
	vlanIds, results, err := ipam.Allocate(conf.IPAM.Type, args)
	if err != nil {
		return err
	}
	// 判断是否创建默认网桥
	if d.DisableDefaultBridge == nil {
		defaultTrue := true
		d.DisableDefaultBridge = &amp;defaultTrue
		for i := range vlanIds {
			if vlanIds[i] == 0 {
				*d.DisableDefaultBridge = false
			}
		}
	}
	// 初始化VLAN驱动，主要是在必要的情况下对宿主机上的网桥进行配置，或者进行针对MACVlan/IPVlan的配置
	if err := d.Init(); err != nil {
		return fmt.Errorf("failed to setup bridge %v", err)
	}
	result020s, err := resultConvert(results)
	if err != nil {
		return err
	}
	// 配置宿主机网络
	if err := setupNetwork(result020s, vlanIds, args); err != nil {
		return err
	}
	result020s[0].DNS = conf.DNS
	return result020s[0].Print()
}

func cmdDel(args *skel.CmdArgs) error {
	// 删除所有VETH对
	if err := utils.DeleteAllVeth(args.Netns); err != nil {
		return err
	}
	conf, err := d.LoadConf(args.StdinData)
	if err != nil {
		return err
	}
	// 是否IP地址
	return ipam.Release(conf.IPAM.Type, args)
}</pre>
<p>VlanDriver是此CNI的核心。 </p>
<pre class="crayon-plain-tag">type VlanDriver struct {
	//FIXME add a file lock cause we are running multiple processes?
	*NetConf
	// 不管是物理接口，还是VLAN子接口，都位于宿主机命名空间
	// 所有VLAN接口的物理接口（父接口）的设备索引，例如eth1
	vlanParentIndex int
	// 如果启用VLAN，则是当前所属VLAN的子接口的索引。否则是物理接口的索引
	DeviceIndex int
}</pre>
<p>ADD/DEL命令都需要调用下面的方法加载CNI配置：</p>
<pre class="crayon-plain-tag">const (
	VlanPrefix        = "vlan"
	BridgePrefix      = "docker"
	DefaultBridge     = "docker"
	// IPVLAN有两种模式L2/L3
	DefaultIPVlanMode = "l3"
)

func (d *VlanDriver) LoadConf(bytes []byte) (*NetConf, error) {
	conf := &amp;NetConf{}
	if err := json.Unmarshal(bytes, conf); err != nil {
		return nil, fmt.Errorf("failed to load netconf: %v", err)
	}
	if conf.DefaultBridgeName == "" {
		conf.DefaultBridgeName = DefaultBridge
	}
	if conf.BridgeNamePrefix == "" {
		conf.BridgeNamePrefix = BridgePrefix
	}
	if conf.VlanNamePrefix == "" {
		conf.VlanNamePrefix = VlanPrefix
	}
	if conf.IpVlanMode == "" {
		conf.IpVlanMode = DefaultIPVlanMode
	}

	d.NetConf = conf
	return conf, nil
}</pre>
<p>CNI配置定义如下：</p>
<pre class="crayon-plain-tag">type NetConf struct {
	// CNI标准网络配置
	types.NetConf
	// 持有IDC IP地址的设备名，例如eth1或eth1.12（VLAN子接口）
	Device string `json:"device"`
	// 上述Device为空的时候，候选设备列表。取第一个存在的设备
	Devices []string `json:"devices"`
	// 交换机实现方式 macvlan, bridge（默认）, pure（避免创建不必要的网桥）
	Switch string `json:"switch"`
	// IPVLAN模式，可选l2, l3（默认）, l3s
	IpVlanMode string `json:"ipvlan_mode"`

	// 不去创建默认网桥
	DisableDefaultBridge *bool `json:"disable_default_bridge"`
	// 默认网桥的名字
	DefaultBridgeName string `json:"default_bridge_name"`
	// 网桥名字前缀
	BridgeNamePrefix string `json:"bridge_name_prefix"`
	// VLAN接口名字前缀
	VlanNamePrefix string `json:"vlan_name_prefix"`
	// 是否启用免费ARP
	GratuitousArpRequest bool `json:"gratuitous_arp_request"`
	// 设置MTU
	MTU int `json:"mtu"`
}</pre>
<p>ADD命令，加载完CNI配置后，首先是调用IPAM进行IP地址分配：</p>
<pre class="crayon-plain-tag">func Allocate(ipamType string, args *skel.CmdArgs) ([]uint16, []types.Result, error) {
	var (
		vlanId uint16
		err    error
	)
	// 解析key1=val1;key2=val2格式的CNI参数。这些参数来自Pod的k8s.v1.cni.galaxy.io/args注解，
	// 如果使用浮动IP，则Galaxy IPAM会设置该注解，将IP地址填写在ipinfos字段
	kvMap, err := cniutil.ParseCNIArgs(args.Args)
	if err != nil {
		return nil, nil, err
	}
	var results []types.Result
	var vlanIDs []uint16
	// 读取ipinfos字段
	if ipInfoStr := kvMap[constant.IPInfosKey]; ipInfoStr != "" {
		// 解析IP信息
		var ipInfos []constant.IPInfo
		if err := json.Unmarshal([]byte(ipInfoStr), &amp;ipInfos); err != nil {
			return nil, nil, fmt.Errorf("failed to unmarshal ipInfo from args %q: %v", args.Args, err)
		}
		if len(ipInfos) == 0 {
			return nil, nil, fmt.Errorf("empty ipInfos")
		}
		for j := range ipInfos {
			results = append(results, cniutil.IPInfoToResult(&amp;ipInfos[j]))
			vlanIDs = append(vlanIDs, ipInfos[j].Vlan)
		}
		// 直接“分配”Pod注解里写的IP地址
		return vlanIDs, results, nil
	}
	// 如果Pod注解里面没有IP地址信息，则需要调用谋者IPAM插件，因此断言ipamType不为空：
	if ipamType == "" {
		return nil, nil, fmt.Errorf("neither ipInfo from cni args nor ipam type from netconf")
	}
	// 调用IPAM插件
	generalResult, err := ipam.ExecAdd(ipamType, args.StdinData)
	if err != nil {
		return nil, nil, err
	}
	result, err := t020.GetResult(generalResult)
	if err != nil {
		return nil, nil, err
	}
	if result.IP4 == nil {
		return nil, nil, fmt.Errorf("IPAM plugin returned missing IPv4 config")
	}
	return append(vlanIDs, vlanId), append(results, generalResult), err
}</pre>
<p>如果使用Galaxy IPAM提供的浮动IP功能，则上述代码会自动获取已经分配的IP信息并返回。</p>
<p>分配/获取IP地址之后，就执行VlanDriver的初始化：</p>
<pre class="crayon-plain-tag">func (d *VlanDriver) Init() error {
	var (
		device netlink.Link
		err    error
	)
	// 这个驱动需要指定宿主机上的父接口
	if d.Device != "" {
		device, err = netlink.LinkByName(d.Device)
	} else {
		if len(d.Devices) == 0 {
			return fmt.Errorf("a device is needed to use vlan plugin")
		}
		for _, devName := range d.Devices {
			device, err = netlink.LinkByName(devName)
			if err == nil {
				break
			}
		}
	}
	if err != nil {
		return fmt.Errorf("Error getting device %s: %v", d.Device, err)
	}
	// 默认MTU从父接口上查找
	if d.MTU == 0 {
		d.MTU = device.Attrs().MTU
	}
	d.DeviceIndex = device.Attrs().Index
	d.vlanParentIndex = device.Attrs().Index
	// 如果指定的设备是VLAN子接口，则使用其父接口的索引
	if device.Type() == "vlan" {
		//A vlan device
		d.vlanParentIndex = device.Attrs().ParentIndex
		//glog.Infof("root device %s is a vlan device, parent index %d", d.Device, d.vlanParentIndex)
	}
	if d.IPVlanMode() {
		switch d.GetIPVlanMode() {
		case netlink.IPVLAN_MODE_L3S, netlink.IPVLAN_MODE_L3:
			// 允许绑定到非本地地址，什么作用
			return utils.EnableNonlocalBind()
		default:
			return nil
		}
	} else if d.MacVlanMode() {
		return nil
	} else if d.PureMode() {
		if err := d.initPureModeArgs(); err != nil {
			return err
		}
		return utils.EnableNonlocalBind()
	}
	if d.DisableDefaultBridge != nil &amp;&amp; *d.DisableDefaultBridge {
		return nil
	}
	// 需要创建默认网桥
	// 获得父接口的IP地址
	v4Addr, err := netlink.AddrList(device, netlink.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("Errror getting ipv4 address %v", err)
	}
	filteredAddr := network.FilterLoopbackAddr(v4Addr)
	if len(filteredAddr) == 0 {
		// 父接口没有IP地址，那么地址应当转给网桥了。检查确保网桥设备存在
		bri, err := netlink.LinkByName(d.DefaultBridgeName)
		if err != nil {
			return fmt.Errorf("Error getting bri device %s: %v", d.DefaultBridgeName, err)
		}
		if bri.Attrs().Index != device.Attrs().MasterIndex {
			return fmt.Errorf("No available address found on device %s", d.Device)
		}
	} else {
		// 否则，意味着网桥应该还不存在，初始化之
		if err := d.initVlanBridgeDevice(device, filteredAddr); err != nil {
			return err
		}
	}
	return nil
}

// 不使用MacVlan/IPVlan，则需要依赖网桥
func (d *VlanDriver) initVlanBridgeDevice(device netlink.Link, filteredAddr []netlink.Addr) error {
	// 创建网桥
	bri, err := getOrCreateBridge(d.DefaultBridgeName, device.Attrs().HardwareAddr)
	if err != nil {
		return err
	}
	// 启动网桥
	if err := netlink.LinkSetUp(bri); err != nil {
		return fmt.Errorf("failed to set up bridge device %s: %v", d.DefaultBridgeName, err)
	}
	// 获取父接口路由列表
	rs, err := netlink.RouteList(device, nl.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("failed to list route of device %s", device.Attrs().Name)
	}
	defer func() {
		if err != nil {
			// 如果出现错误，则路由需要加回去
			for i := range rs {
				_ = netlink.RouteAdd(&amp;rs[i])
			}
		}
	}()
	// 将父接口的IP地址、路由，转移到网桥上
	err = d.moveAddrAndRoute(device, bri, filteredAddr, rs)
	if err != nil {
		return err
	}
	return nil
}</pre>
<p>可以看到，如果使用MacVlan或者IPVlan模式，则不会去管理宿主机上的网桥（这也是MacVlan/IPVlan的价值之一，不需要网桥）。否则，会创建网桥并转移走父接口的IP地址、路由。</p>
<p>最后，下面的函数负责配置好网络：  </p>
<pre class="crayon-plain-tag">func setupNetwork(result020s []*t020.Result, vlanIds []uint16, args *skel.CmdArgs) error {
	if d.MacVlanMode() {
		// 处理MACVlan模式
		if err := setupMacvlan(result020s[0], vlanIds[0], args); err != nil {
			return err
		}
	} else if d.IPVlanMode() {
		// 处理IPVlan模式
		if err := setupIPVlan(result020s[0], vlanIds[0], args); err != nil {
			return err
		}
	} else {
		// 处理网桥模式
		ifName := args.IfName
		if err := setupVlanDevice(result020s, vlanIds, args); err != nil {
			return err
		}
		args.IfName = ifName
	}
	// 发送一个免费ARP，让交换机知道浮动IP漂移到当前节点上了
	if d.PureMode() {
		_ = utils.SendGratuitousARP(d.Device, result020s[0].IP4.IP.IP.String(), "", d.GratuitousArpRequest)
	}
	return nil
}</pre>
<p>如果使用MACVlan，则配置过程如下：</p>
<pre class="crayon-plain-tag">func setupMacvlan(result *t020.Result, vlanId uint16, args *skel.CmdArgs) error {
	if err := d.MaybeCreateVlanDevice(vlanId); err != nil {
		return err
	}
	// 通过MACVlan连接宿主机和容器
	if err := utils.MacVlanConnectsHostWithContainer(result, args, d.DeviceIndex, d.MTU); err != nil {
		return err
	}
	// 在命名空间内进行宣告
	_ = utils.SendGratuitousARP(args.IfName, result.IP4.IP.IP.String(), args.Netns, d.GratuitousArpRequest)
	return nil
}

func MacVlanConnectsHostWithContainer(result *t020.Result, args *skel.CmdArgs, parent int, mtu int) error {
	var err error
	// 创建MACVlan设备
	macVlan := &amp;netlink.Macvlan{
		Mode: netlink.MACVLAN_MODE_BRIDGE,
		LinkAttrs: netlink.LinkAttrs{
			Name:        HostMacVlanName(args.ContainerID),
			MTU:         mtu,
			ParentIndex: parent, // 父设备可能是物理接口或者VLAN子接口
		}}
	// 添加接口
	if err := netlink.LinkAdd(macVlan); err != nil {
		return err
	}
	// 出错时必须接口被删除
	defer func() {
		if err != nil {
			netlink.LinkDel(macVlan)
		}
	}()
	// 配置沙盒，细节参考下文
	if err = configSboxDevice(result, args, macVlan); err != nil {
		return err
	}
	return nil
}</pre>
<p>如果使用IPVlan，则配置过程如下： </p>
<pre class="crayon-plain-tag">func setupIPVlan(result *t020.Result, vlanId uint16, args *skel.CmdArgs) error {
	// 如有必要，创建VLAN设备
	if err := d.MaybeCreateVlanDevice(vlanId); err != nil {
		return err
	}
	// 连接宿主机和容器网络                                         IPVLAN的父设备可能是
	//                                                            VLAN子接口（位于宿主机命名空间）
	if err := utils.IPVlanConnectsHostWithContainer(result, args, d.DeviceIndex, d.GetIPVlanMode(), d.MTU); err != nil {
		return err
	}

	// l3模式下，所有连接在一起的容器之间的相互通信，都要通过宿主机上的IPVlan父接口进行代理
	// 在我们的环境中（部署在OpenStack的虚拟机上），出现Pod无法ping二层网关的情况，需要
	//     arping -c 2 -U -I eth0 &lt;pod-ip&gt;
	// 才可以解决，等价于下面的代码
	// Gratuitous ARP不能保证ARP缓存永久有效，底层交换机（OpenStack虚拟的）需要进行适当的配置，
	// 将Pod IP和VM的MAC地址关联
	if d.IpVlanMode == "l3" || d.IpVlanMode == "l3s" {
		_ = utils.SendGratuitousARP(d.Device, result.IP4.IP.IP.String(), "", d.GratuitousArpRequest)
		return nil
	}
	// 在网络命名空间中进行宣告
	_ = utils.SendGratuitousARP(args.IfName, result.IP4.IP.IP.String(), args.Netns, d.GratuitousArpRequest)
	return nil
}


// 创建VLAN设备的细节
func (d *VlanDriver) MaybeCreateVlanDevice(vlanId uint16) error {
	// 如果不启用VLAN支持，则不做任何事情
	if vlanId == 0 {
		return nil
	}
	_, err := d.getOrCreateVlanDevice(vlanId)
	return err
}
func (d *VlanDriver) getOrCreateVlanDevice(vlanId uint16) (netlink.Link, error) {
	// 根据父接口和VLAN ID查找已存在的VLAN设备
	link, err := d.getVlanIfExist(vlanId)
	if err != nil || link != nil {
		if link != nil {
			// 设置VLAN设备索引
			d.DeviceIndex = link.Attrs().Index
		}
		return link, err
	}
	vlanIfName := fmt.Sprintf("%s%d", d.VlanNamePrefix, vlanId)
	// 获取VLAN设备，如果获取不到则通过后面的回调函数创建
	vlan, err := getOrCreateDevice(vlanIfName, func(name string) error {
		// 创建设备                                                 名字 vlan100 父接口索引
		vlanIf := &amp;netlink.Vlan{LinkAttrs: netlink.LinkAttrs{Name: vlanIfName, ParentIndex: d.vlanParentIndex},
			// VLAN号
			VlanId: (int)(vlanId)}
		// 添加设备
		if err := netlink.LinkAdd(vlanIf); err != nil {
			return fmt.Errorf("Failed to add vlan device %s: %v", vlanIfName, err)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	// 设置为UP状态
	if err := netlink.LinkSetUp(vlan); err != nil {
		return nil, fmt.Errorf("Failed to set up vlan device %s: %v", vlanIfName, err)
	}
	// 更新VLAN子接口索引号
	d.DeviceIndex = vlan.Attrs().Index
	return vlan, nil
}
func (d *VlanDriver) getVlanIfExist(vlanId uint16) (netlink.Link, error) {
	links, err := netlink.LinkList()
	if err != nil {
		return nil, err
	}
	for _, link := range links {
		// 遍历网络接口列表，查找VLAN类型的、VLAN ID匹配的、父接口匹配的
		// 隐含意味着对于每个VLAN，只需要一个子接口，所有连接到此VLAN的容器使用它
		if link.Type() == "vlan" {
			if vlan, ok := link.(*netlink.Vlan); !ok {
				return nil, fmt.Errorf("vlan device type case error: %T", link)
			} else {
				if vlan.VlanId == int(vlanId) &amp;&amp; vlan.ParentIndex == d.vlanParentIndex {
					return link, nil
				}
			}
		}
	}
	return nil, nil
}


// 通过IPVLAN两宿主机和容器连接起来的细节
// 配置网络沙盒
func configSboxDevice(result *t020.Result, args *skel.CmdArgs, sbox netlink.Link) error {
	// 配置MAC地址之前，应该将接口DOWN掉
	if err := netlink.LinkSetDown(sbox); err != nil {
		return fmt.Errorf("could not set link down for container interface %q: %v", sbox.Attrs().Name, err)
	}
	if sbox.Type() != "ipvlan" {
		// IPVlan不需要设置MAC地址，因为必须和父接口一致
		if err := netlink.LinkSetHardwareAddr(sbox, GenerateMACFromIP(result.IP4.IP.IP)); err != nil {
			return fmt.Errorf("could not set mac address for container interface %q: %v", sbox.Attrs().Name, err)
		}
	}
	// 获得容器网络命名空间
	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	// 总是要关闭命名空间
	defer netns.Close() // nolint: errcheck
	// 移动到网络命名空间中
	if err = netlink.LinkSetNsFd(sbox, int(netns.Fd())); err != nil {
		return fmt.Errorf("failed to move sbox device %q to netns: %v", sbox.Attrs().Name, err)
	}
	// 在网络命名空间中配置
	return netns.Do(func(_ ns.NetNS) error {
		// 改名，IPVLAN接口直接变为容器的eth0，就像VETH对的一端那样
		if err := netlink.LinkSetName(sbox, args.IfName); err != nil {
			return fmt.Errorf("failed to rename sbox device %q to %q: %v", sbox.Attrs().Name, args.IfName, err)
		}
		// 如果容器、宿主机上有多个网络设备，则需要禁用rp_filter（反向路径过滤）
		// 因为从宿主机来的ARP请求（广播报文），可能使用不确定的源IP，如果启用反向路径过滤，封包可能被丢弃
		if err := DisableRpFilter(args.IfName); err != nil {
			return fmt.Errorf("failed disable rp_filter to dev %s: %v", args.IfName, err)
		}
		// 禁用所有接口的rp_filter
		if err := DisableRpFilter("all"); err != nil {
			return fmt.Errorf("failed disable rp_filter to all: %v", err)
		}
		// 配置容器网络接口：将IP地址、路由添加到容器网络接口
		return cniutil.ConfigureIface(args.IfName, result)
	})
}
func ConfigureIface(ifName string, res *t020.Result) error {
	// 网络接口应该已经存在
	link, err := netlink.LinkByName(ifName)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", ifName, err)
	}
	// 设置为UP状态
	if err := netlink.LinkSetUp(link); err != nil {
		return fmt.Errorf("failed to set %q UP: %v", ifName, err)
	}

	// TODO(eyakubovich): IPv6
	addr := &amp;netlink.Addr{IPNet: &amp;res.IP4.IP, Label: ""}
	// 添加IP地址
	if err = netlink.AddrAdd(link, addr); err != nil {
		return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err)
	}

	// 遍历并应用IPV4路由
	for _, r := range res.IP4.Routes {
		gw := r.GW
		if gw == nil {
			gw = res.IP4.Gateway
		}
		if err = ip.AddRoute(&amp;r.Dst, gw, link); err != nil {
			// we skip over duplicate routes as we assume the first one wins
			if !os.IsExist(err) {
				return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
			}
		}
	}
	return nil
}</pre>
<p>DEL命令比较简单：</p>
<ol>
<li>删除容器网络命名空间中所有VETH对</li>
<li>释放IP，就是调用IPAM插件，如果没有插件则什么都不做。Galaxy IPAM不会被调用，它总是自己负责IP地址的回收</li>
</ol>
<div class="blog_h3"><span class="graybg">galaxy-veth-host</span></div>
<p><a href="https://github.com/lyft/cni-ipvlan-vpc-k8s/blob/master/plugin/unnumbered-ptp/unnumbered-ptp.go">该插件</a>用于解决使用IPVlan L2模式下Pod访问不了宿主机IP、Service IP的问题。使用该插件，需要配置Galaxy：</p>
<pre class="crayon-plain-tag">{
  "NetworkConf":[
    {"name":"tke-route-eni","type":"tke-route-eni","eni":"eth1","routeTable":1},
    {"name":"galaxy-flannel","type":"galaxy-flannel", "delegate":{"type":"galaxy-veth"},"subnetFile":"/run/flannel/subnet.env"},
    {"name":"galaxy-k8s-vlan","type":"galaxy-k8s-vlan", "device":"eth0", "switch":"ipvlan", "ipvlan_mode":"l2", "mtu": 1500},
    {"name":"galaxy-k8s-sriov","type": "galaxy-k8s-sriov", "device": "eth0", "vf_num": 10},
    // 新增
    {
        "name":"galaxy-veth-host","type": "galaxy-veth-host", 
        // 这里配置K8S的服务CIDR         指明宿主机网卡
        "serviceCidr": "11.1.252.0/22", "hostInterface": "eth0", 
        // 容器中此插件创建的网卡名称     是否进行SNAT
        "containerInterface": "veth0", "ipMasq": true
    }
  ],
  "DefaultNetworks": ["galaxy-flannel"]
}</pre>
<p>Pod注解需要使用两个网络：</p>
<pre class="crayon-plain-tag">annotations:
  k8s.v1.cni.cncf.io/networks: "galaxy-k8s-vlan,galaxy-veth-host"</pre>
<p>代码解读： </p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"fmt"
	"math"
	"math/rand"
	"net"
	"os"
	"runtime"
	"sort"
	"strconv"
	"time"

	"github.com/containernetworking/cni/pkg/skel"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/types/current"
	"github.com/containernetworking/cni/pkg/version"
	"github.com/containernetworking/plugins/pkg/ip"
	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containernetworking/plugins/pkg/utils"
	"github.com/containernetworking/plugins/pkg/utils/sysctl"
	"github.com/coreos/go-iptables/iptables"
	"github.com/j-keck/arping"
	"github.com/vishvananda/netlink"
)

// constants for full jitter backoff in milliseconds, and for nodeport marks
const (
	maxSleep             = 10000 // 10.00s
	baseSleep            = 20    //  0.02
	RPFilterTemplate     = "net.ipv4.conf.%s.rp_filter"
	podRulePriority      = 1024
	nodePortRulePriority = 512
)

func init() {
	// this ensures that main runs only on main thread (thread group leader).
	// since namespace ops (unshare, setns) are done for a single thread, we
	// must ensure that the goroutine does not jump from OS thread to thread
	runtime.LockOSThread()
}

// PluginConf is whatever you expect your configuration json to be. This is whatever
// is passed in on stdin. Your plugin may wish to expose its functionality via
// runtime args, see CONVENTIONS.md in the CNI spec.
type PluginConf struct {
	types.NetConf

	// This is the previous result, when called in the context of a chained
	// plugin. Because this plugin supports multiple versions, we'll have to
	// parse this in two passes. If your plugin is not chained, this can be
	// removed (though you may wish to error if a non-chainable plugin is
	// chained.
	// If you need to modify the result before returning it, you will need
	// to actually convert it to a concrete versioned struct.
	RawPrevResult *map[string]interface{} `json:"prevResult"`
	PrevResult    *current.Result         `json:"-"`

	IPMasq             bool   `json:"ipMasq"`
	HostInterface      string `json:"hostInterface"`
	ServiceCidr        string `json:"serviceCidr"`
	ContainerInterface string `json:"containerInterface"`
	MTU                int    `json:"mtu"`
	TableStart         int    `json:"routeTableStart"`
	NodePortMark       int    `json:"nodePortMark"`
	NodePorts          string `json:"nodePorts"`
}

// parseConfig parses the supplied configuration (and prevResult) from stdin.
func parseConfig(stdin []byte) (*PluginConf, error) {
	conf := PluginConf{}

	if err := json.Unmarshal(stdin, &amp;conf); err != nil {
		return nil, fmt.Errorf("failed to parse network configuration: %v", err)
	}

	// Parse previous result.
	if conf.RawPrevResult != nil {
		resultBytes, err := json.Marshal(conf.RawPrevResult)
		if err != nil {
			return nil, fmt.Errorf("could not serialize prevResult: %v", err)
		}
		res, err := version.NewResult(conf.CNIVersion, resultBytes)
		if err != nil {
			return nil, fmt.Errorf("could not parse prevResult: %v", err)
		}
		conf.RawPrevResult = nil
		conf.PrevResult, err = current.NewResultFromResult(res)
		if err != nil {
			return nil, fmt.Errorf("could not convert result to current version: %v", err)
		}
	}
	// End previous result parsing

	if conf.HostInterface == "" {
		return nil, fmt.Errorf("hostInterface must be specified")
	}

	if conf.ContainerInterface == "" {
		return nil, fmt.Errorf("containerInterface must be specified")
	}

	if conf.NodePorts == "" {
		conf.NodePorts = "30000:32767"
	}

	if conf.NodePortMark == 0 {
		conf.NodePortMark = 0x2000
	}

	// start using tables by default at 256
	if conf.TableStart == 0 {
		conf.TableStart = 256
	}

	return &amp;conf, nil
}

func cmdAdd(args *skel.CmdArgs) error {
	conf, err := parseConfig(args.StdinData)
	if err != nil {
		return err
	}

	// 必须作为插件链调用，并且有前置插件
	if conf.PrevResult == nil {
		return fmt.Errorf("must be called as chained plugin")
	}

	// 从前序插件的结果中得到容器IP列表
	containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs))
	if conf.CNIVersion != "0.3.0" &amp;&amp; conf.CNIVersion != "0.3.1" {
		for _, ip := range conf.PrevResult.IPs {
			containerIPs = append(containerIPs, ip.Address.IP)
		}
	} else {
		for _, ip := range conf.PrevResult.IPs {
			if ip.Interface == nil {
				continue
			}
			intIdx := *ip.Interface
			if intIdx &gt;= 0 &amp;&amp; intIdx &lt; len(conf.PrevResult.Interfaces) &amp;&amp; conf.PrevResult.Interfaces[intIdx].Name != args.IfName {
				continue
			}
			containerIPs = append(containerIPs, ip.Address.IP)
		}
	}
	if len(containerIPs) == 0 {
		return fmt.Errorf("got no container IPs")
	}
	// 得到宿主机网络接口
	iface, err := netlink.LinkByName(conf.HostInterface)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", conf.HostInterface, err)
	}
	// 得到宿主机网络接口的地址列表
	hostAddrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL)
	if err != nil || len(hostAddrs) == 0 {
		return fmt.Errorf("failed to get host IP addresses for %q: %v", iface, err)
	}

	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	defer netns.Close()

	containerIPV4 := false
	containerIPV6 := false
	for _, ipc := range containerIPs {
		if ipc.To4() != nil {
			containerIPV4 = true
		} else {
			containerIPV6 = true
		}
	}
	// 创建VETH对，并且在容器端进行配置：
	// 1. 如果启用了MASQ，则对出口是args.IfName的流量进行SNAT
	// 2. 为宿主机所有IP添加路由，走veth
	// 3. 为K8S服务IP添加旅游，则veth，网关设置为第一个宿主机IP
	hostInterface, _, err := setupContainerVeth(netns, conf.ServiceCidr, conf.ContainerInterface, conf.MTU,
		hostAddrs, conf.IPMasq, containerIPV4, containerIPV6, args.IfName, conf.PrevResult)
	if err != nil {
		return err
	}
	// 配置容器的宿主端：
	if err = setupHostVeth(hostInterface.Name, hostAddrs, conf.IPMasq, conf.TableStart, conf.PrevResult); err != nil {
		return err
	}

	if conf.IPMasq {
		// 在宿主端启用IP转发
		err := enableForwarding(containerIPV4, containerIPV6)
		if err != nil {
			return err
		}

		chain := utils.FormatChainName(conf.Name, args.ContainerID)
		comment := utils.FormatComment(conf.Name, args.ContainerID)
		for _, ipc := range containerIPs {
			addrBits := 128
			if ipc.To4() != nil {
				addrBits = 32
			}
			// 对来自容器IP的流量进行SNAT
			if err = ip.SetupIPMasq(&amp;net.IPNet{IP: ipc, Mask: net.CIDRMask(addrBits, addrBits)}, chain, comment); err != nil {
				return err
			}
		}
	}

	// 配置NodePort相关的iptables规则
	if err = setupNodePortRule(conf.HostInterface, conf.NodePorts, conf.NodePortMark); err != nil {
		return err
	}

	// Pass through the result for the next plugin
	return types.PrintResult(conf.PrevResult, conf.CNIVersion)
}

func setupContainerVeth(netns ns.NetNS, serviceCidr string, ifName string, mtu int,
	hostAddrs []netlink.Addr, masq, containerIPV4, containerIPV6 bool, k8sIfName string,
	pr *current.Result) (*current.Interface, *current.Interface, error) {
	hostInterface := &amp;current.Interface{}
	containerInterface := &amp;current.Interface{}
	// 在容器网络命名空间（netns）中执行
	err := netns.Do(func(hostNS ns.NetNS) error {
		// 创建VETH对，一端自动放入hostNS（这个变量netns.Do函数自动提供，为初始网络命名空间）
		hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
		if err != nil {
			return err
		}
		hostInterface.Name = hostVeth.Name
		hostInterface.Mac = hostVeth.HardwareAddr.String()
		containerInterface.Name = contVeth0.Name
		// ip.SetupVeth函数不会获取VETH对的peer（第二个接口）的MAC地址，因此这里需要执行查询
		containerNetlinkIface, _ := netlink.LinkByName(contVeth0.Name)
		containerInterface.Mac = containerNetlinkIface.Attrs().HardwareAddr.String()
		containerInterface.Sandbox = netns.Path()

		// 本次CNI调用产生的两个网络接口，纳入到Result
		pr.Interfaces = append(pr.Interfaces, hostInterface, containerInterface)

		// 后面只是用到index属性，用上面Link对象不行么？
		contVeth, err := net.InterfaceByName(ifName)
		if err != nil {
			return fmt.Errorf("failed to look up %q: %v", ifName, err)
		}

		if masq {
			// 支持IPv4/IPv6的IP转发
			err := enableForwarding(containerIPV4, containerIPV6)
			if err != nil {
				return err
			}

			// 如果出口网卡是k8sIfName（容器主接口，通常是eth0，kubelet调用galaxy-sdn时提供）
			// ，则进行源地址转换
			err = setupSNAT(k8sIfName, "kube-proxy SNAT")
			if err != nil {
				return fmt.Errorf("failed to enable SNAT on %q: %v", k8sIfName, err)
			}
		}

		// 为宿主机的每个网络接口的地址添加路由条目
		for _, ipc := range hostAddrs {
			addrBits := 128
			if ipc.IP.To4() != nil {
				addrBits = 32
			}
			// 这些地址的出口网卡都设置为
			err := netlink.RouteAdd(&amp;netlink.Route{
				LinkIndex: contVeth.Index, // 新添加的VETH
				Scope:     netlink.SCOPE_LINK,
				Dst: &amp;net.IPNet{
					IP:   ipc.IP,
					Mask: net.CIDRMask(addrBits, addrBits),
				},
			})

			if err != nil {
				return fmt.Errorf("failed to add host route dst %v: %v", ipc.IP, err)
			}
		}

		_, serviceNet, err := net.ParseCIDR(serviceCidr)
		if err != nil {
			return fmt.Errorf("failed to parse service cidr :%v", err)
		}

		// 将K8S服务网段的路由设置为：出口网卡新添加的VETH，网关为宿主机第一个IP
		err = netlink.RouteAdd(&amp;netlink.Route{
			LinkIndex: contVeth.Index,
			Scope:     netlink.SCOPE_UNIVERSE,
			Dst:       serviceNet,
			// 封包发给宿主机第一个IP/网络接口处理。VETH的宿主端没有连接到什么网络接口
			// 这些网络接口可以作为下一跳，因为和VETH宿主端属于同一网络命名空间
			Gw: hostAddrs[0].IP,
		})
		if err != nil {
			return fmt.Errorf("failed to add service cidr route %v: %v", hostAddrs[0].IP, err)
		}

		// 为所有IPv4地址发送免费ARP
		for _, ipc := range pr.IPs {
			if ipc.Version == "4" {
				_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
			}
		}

		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	return hostInterface, containerInterface, nil
}

func enableForwarding(ipv4 bool, ipv6 bool) error {
	if ipv4 {
		err := ip.EnableIP4Forward()
		if err != nil {
			return fmt.Errorf("Could not enable IPv6 forwarding: %v", err)
		}
	}
	if ipv6 {
		err := ip.EnableIP6Forward()
		if err != nil {
			return fmt.Errorf("Could not enable IPv6 forwarding: %v", err)
		}
	}
	return nil
}

func setupSNAT(ifName string, comment string) error {
	ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
	if err != nil {
		return fmt.Errorf("failed to locate iptables: %v", err)
	}
	rulespec := []string{"-o", ifName, "-j", "MASQUERADE"}
	//if ipt.HasRandomFully() {
	//	rulespec = append(rulespec, "--random-fully")
	//}
	rulespec = append(rulespec, "-m", "comment", "--comment", comment)
	return ipt.AppendUnique("nat", "POSTROUTING", rulespec...)
}

func setupHostVeth(vethName string, hostAddrs []netlink.Addr, masq bool, tableStart int, result *current.Result) error {
	// no IPs to route
	if len(result.IPs) == 0 {
		return nil
	}

	// lookup by name as interface ids might have changed
	veth, err := net.InterfaceByName(vethName)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", vethName, err)
	}

	// 对于所有容器IP，出口设置为VETH
	for _, ipc := range result.IPs {
		addrBits := 128
		if ipc.Address.IP.To4() != nil {
			addrBits = 32
		}

		err := netlink.RouteAdd(&amp;netlink.Route{
			LinkIndex: veth.Index,
			Scope:     netlink.SCOPE_LINK,
			Dst: &amp;net.IPNet{
				IP:   ipc.Address.IP,
				Mask: net.CIDRMask(addrBits, addrBits),
			},
		})

		if err != nil {
			return fmt.Errorf("failed to add host route dst %v: %v", ipc.Address.IP, err)
		}
	}

	// 为来自Pod的，目的地址是VPC的配置策略路由
	err = addPolicyRules(veth, result.IPs[0], result.Routes, tableStart)
	if err != nil {
		return fmt.Errorf("failed to add policy rules: %v", err)
	}

	// 为所有宿主机端的IP发送免费ARP
	for _, ipc := range hostAddrs {
		if ipc.IP.To4() != nil {
			_ = arping.GratuitousArpOverIface(ipc.IP, *veth)
		}
	}

	return nil
}

func addPolicyRules(veth *net.Interface, ipc *current.IPConfig, routes []*types.Route, tableStart int) error {
	table := -1

	// 对路由，来自先前插件调用的结果，进行排序
	sort.Slice(routes, func(i, j int) bool {
		return routes[i].Dst.String() &lt; routes[j].Dst.String()
	})

	// 尝试最多10次，向空表（table slot）写入路由
	for i := 0; i &lt; 10 &amp;&amp; table == -1; i++ {
		var err error
		// 寻找空白路由表
		table, err = findFreeTable(tableStart + rand.Intn(1000))
		if err != nil {
			return err
		}

		// 将所有路由添加到路由表
		for _, route := range routes {
			err := netlink.RouteAdd(&amp;netlink.Route{
				LinkIndex: veth.Index,     // 出口设置为宿主机VETH
				Dst:       &amp;route.Dst,     // 目标是先前CNI调用发现的路由，这些路由可能来自云提供商（VPC）
				Gw:        ipc.Address.IP, // 将Result的第一个IP作为网关
				Table:     table,          // 写在这个表中
			})
			if err != nil {
				table = -1
				break
			}
		}

		if table == -1 {
			// failed to add routes so sleep and try again on a different table
			wait := time.Duration(rand.Intn(int(math.Min(maxSleep,
				baseSleep*math.Pow(2, float64(i)))))) * time.Millisecond
			fmt.Fprintf(os.Stderr, "route table collision, retrying in %v\n", wait)
			time.Sleep(wait)
		}
	}

	// ensure we have a route table selected
	if table == -1 {
		return fmt.Errorf("failed to add routes to a free table")
	}

	// 创建路由策略
	rule := netlink.NewRule()
	// 如果流量来自VETH宿主端，也就是来自Pod
	rule.IifName = veth.Name
	rule.Table = table
	rule.Priority = podRulePriority

	err := netlink.RuleAdd(rule)
	if err != nil {
		return fmt.Errorf("failed to add policy rule %v: %v", rule, err)
	}

	return nil
}

func findFreeTable(start int) (int, error) {
	allocatedTableIDs := make(map[int]bool)
	// 遍历所有IPv4/IPv6的路由规则
	for _, family := range []int{netlink.FAMILY_V4, netlink.FAMILY_V6} {
		rules, err := netlink.RuleList(family)
		if err != nil {
			return -1, err
		}
		for _, rule := range rules {
			// 收集所有已经占用的table slot
			allocatedTableIDs[rule.Table] = true
		}
	}
	// 寻找第一个空白的slot
	for i := start; i &lt; math.MaxUint32; i++ {
		if !allocatedTableIDs[i] {
			return i, nil
		}
	}
	return -1, fmt.Errorf("failed to find free route table")
}

func setupNodePortRule(ifName string, nodePorts string, nodePortMark int) error {
	ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
	if err != nil {
		return fmt.Errorf("failed to locate iptables: %v", err)
	}

	// 确保NodePort流量被正确的标记
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", ifName, "-p", "tcp",
		"--dport", nodePorts, "-j", "CONNMARK", "--set-mark", strconv.Itoa(nodePortMark),
		"-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", ifName, "-p", "udp",
		"--dport", nodePorts, "-j", "CONNMARK", "--set-mark", strconv.Itoa(nodePortMark),
		"-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", "veth+", "-j", "CONNMARK",
		"--restore-mark", "-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}

	// 在宿主机网络接口上启用非严格的RP filter
	_, err = sysctl.Sysctl(fmt.Sprintf(RPFilterTemplate, ifName), "2")
	if err != nil {
		return fmt.Errorf("failed to set RP filter to loose for interface %q: %v", ifName, err)
	}

	// 对于标记为NodePort的流量，添加策略路由
	rule := netlink.NewRule()
	rule.Mark = nodePortMark
	rule.Table = 254 // main table
	rule.Priority = nodePortRulePriority

	exists := false
	rules, err := netlink.RuleList(netlink.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("Unable to retrive IP rules %v", err)
	}

	for _, r := range rules {
		if r.Table == rule.Table &amp;&amp; r.Mark == rule.Mark &amp;&amp; r.Priority == rule.Priority {
			exists = true
			break
		}
	}
	if !exists {
		err := netlink.RuleAdd(rule)
		if err != nil {
			return fmt.Errorf("failed to add policy rule %v: %v", rule, err)
		}
	}

	return nil
}

// cmdDel is called for DELETE requests
func cmdDel(args *skel.CmdArgs) error {
	conf, err := parseConfig(args.StdinData)
	if err != nil {
		return err
	}

	if args.Netns == "" {
		return nil
	}

	// 网络命名空间存在，进行清理
	var ipnets []netlink.Addr
	vethPeerIndex := -1
	_ = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
		var err error

		if conf.IPMasq {
			iface, err := netlink.LinkByName(args.IfName)
			if err != nil {
				if err.Error() == "Link not found" {
					return ip.ErrLinkNotFound
				}
				return fmt.Errorf("failed to lookup %q: %v", args.IfName, err)
			}
			// 从args.IfName（通常是eth0）获取IP地址列表
			ipnets, err = netlink.AddrList(iface, netlink.FAMILY_ALL)
			if err != nil || len(ipnets) == 0 {
				return fmt.Errorf("failed to get IP addresses for %q: %v", args.IfName, err)
			}
		}

		vethIface, err := netlink.LinkByName(conf.ContainerInterface)
		if err != nil &amp;&amp; err != ip.ErrLinkNotFound {
			return err
		}
		vethPeerIndex, _ = netlink.VethPeerIndex(&amp;netlink.Veth{LinkAttrs: *vethIface.Attrs()})
		return nil
	})

	if conf.IPMasq {
		chain := utils.FormatChainName(conf.Name, args.ContainerID)
		comment := utils.FormatComment(conf.Name, args.ContainerID)
		for _, ipn := range ipnets {
			addrBits := 128
			if ipn.IP.To4() != nil {
				addrBits = 32
			}

			_ = ip.TeardownIPMasq(&amp;net.IPNet{IP: ipn.IP, Mask: net.CIDRMask(addrBits, addrBits)}, chain, comment)
		}

		if vethPeerIndex != -1 {
			link, err := netlink.LinkByIndex(vethPeerIndex)
			if err != nil {
				return nil
			}

			rule := netlink.NewRule()
			rule.IifName = link.Attrs().Name
			// ignore errors as we might be called multiple times
			_ = netlink.RuleDel(rule)
			_ = netlink.LinkDel(link)
		}
	}

	return nil
}

func main() {
	rand.Seed(time.Now().UnixNano())
	skel.PluginMain(cmdAdd, cmdDel, version.All)
}&amp;nbsp;</pre>
<div class="blog_h2"><span class="graybg">Galaxy守护进程</span></div>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>这个守护进程由DaemonSet galaxy提供，在所有K8S节点上运行，入口点如下：</p>
<pre class="crayon-plain-tag">package main

import (
	"math/rand"
	"time"

	"github.com/spf13/pflag"
	"k8s.io/component-base/cli/flag"
	"k8s.io/component-base/logs"
	glog "k8s.io/klog"
	"tkestack.io/galaxy/pkg/galaxy"
	"tkestack.io/galaxy/pkg/signal"
	"tkestack.io/galaxy/pkg/utils/ldflags"
)

func main() {
	// 随机化
	rand.Seed(time.Now().UTC().UnixNano())
	// 创建Galaxy结构
	galaxy := galaxy.NewGalaxy()
	galaxy.AddFlags(pflag.CommandLine)
	flag.InitFlags()
	logs.InitLogs()
	defer logs.FlushLogs()

	ldflags.PrintAndExitIfRequested()
	// 启动Galaxy
	if err := galaxy.Start(); err != nil {
		glog.Fatalf("Error start galaxy: %v", err)
	}
	// 等待信号，并停止Galaxy
	signal.BlockSignalHandler(func() {
		if err := galaxy.Stop(); err != nil {
			glog.Errorf("Error stop galaxy: %v", err)
		}
	})
}</pre>
<p>Galaxy结构的规格如下：</p>
<pre class="crayon-plain-tag">type Galaxy struct {
	// 对应Galaxy主配置文件
	JsonConf
	// 对应命令行选项
	*options.ServerRunOptions
	quitChan  chan struct{}
	dockerCli *docker.DockerInterface
	netConf   map[string]map[string]interface{}
	// 负责处理端口映射
	pmhandler *portmapping.PortMappingHandler
	client    kubernetes.Interface
	pm        *policy.PolicyManager
}

type JsonConf struct {
	NetworkConf     []map[string]interface{}
	DefaultNetworks []string
	ENIIPNetwork string
}</pre>
<p>初始化过程：</p>
<pre class="crayon-plain-tag">func NewGalaxy() *Galaxy {
	g := &amp;Galaxy{
		ServerRunOptions: options.NewServerRunOptions(),
		quitChan:         make(chan struct{}),
		netConf:          map[string]map[string]interface{}{},
	}
	return g
}

func (g *Galaxy) Init() error {
	if g.JsonConfigPath == "" {
		return fmt.Errorf("json config is required")
	}
	data, err := ioutil.ReadFile(g.JsonConfigPath)
	if err != nil {
		return fmt.Errorf("read json config: %v", err)
	}
	// 解析主配置文件
	if err := json.Unmarshal(data, &amp;g.JsonConf); err != nil {
		return fmt.Errorf("bad config %s: %v", string(data), err)
	}
	glog.Infof("Json Config: %s", string(data))
	// 配置合法性的简单校验
	if err := g.checkNetworkConf(); err != nil {
		return err
	}
	// 需要访问本机Docker
	dockerClient, err := docker.NewDockerInterface()
	if err != nil {
		return err
	}
	g.dockerCli = dockerClient
	g.pmhandler = portmapping.New("")
	return nil
}</pre>
<p>启动过程：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) Start() error {
	// 初始化Galaxy配置
	if err := g.Init(); err != nil {
		return err
	}
	// 创建K8S客户端
	g.initk8sClient()
	// 启动Flannel垃圾回收器
	gc.NewFlannelGC(g.dockerCli, g.quitChan, g.cleanIPtables).Run()
	// 启用或禁用bridge-nf-call-iptables，此参数可以控制网桥转发的封包被不被iptables过滤
	kernel.BridgeNFCallIptables(g.quitChan, g.BridgeNFCallIptables)
	// 配置iptables转发
	kernel.IPForward(g.quitChan, g.IPForward)
	// 启动时，遍历节点上已有的Pod，对于需要进行端口映射的，执行端口映射
	if err := g.setupIPtables(); err != nil {
		return err
	}
	if g.NetworkPolicy {
		g.pm = policy.New(g.client, g.quitChan)
		go wait.Until(g.pm.Run, 3*time.Minute, g.quitChan)
	}
	if g.RouteENI {
		// 使用腾讯云ENI需要关闭反向路径过滤
		kernel.DisableRPFilter(g.quitChan)
		eni.SetupENIs(g.quitChan)
	}
	// 在UDS上监听、启动HTTP服务器、注册路由
	return g.StartServer()
}</pre>
<div class="blog_h3"><span class="graybg">CNI请求处理</span></div>
<p>上述代码结尾的g.StartServer()会调用下面的方法注册路由：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) installHandlers() {
	ws := new(restful.WebService)
	ws.Route(ws.GET("/cni").To(g.cni))
	ws.Route(ws.POST("/cni").To(g.cni))
	restful.Add(ws)
}

// 处理CNI插件转发来的CNI请求
func (g *Galaxy) cni(r *restful.Request, w *restful.Response) {
	data, err := ioutil.ReadAll(r.Request.Body)
	if err != nil {
		glog.Warningf("bad request %v", err)
		http.Error(w, fmt.Sprintf("err read body %v", err), http.StatusBadRequest)
		return
	}
	defer r.Request.Body.Close() // nolint: errcheck
	// 将请求转换为CNIRequest，然后转换为PodRequest
	req, err := galaxyapi.CniRequestToPodRequest(data)
	if err != nil {
		glog.Warningf("bad request %v", err)
		http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
		return
	}
	req.Path = strings.TrimRight(fmt.Sprintf("%s:%s", req.Path, strings.Join(g.CNIPaths, ":")), ":")
	// 处理CNI请求
	result, err := g.requestFunc(req)
	if err != nil {
		http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError)
	} else {
		// Empty response JSON means success with no body
		w.Header().Set("Content-Type", "application/json")
		if _, err := w.Write(result); err != nil {
			glog.Warningf("Error writing %s HTTP response: %v", req.Command, err)
		}
	}
}</pre>
<p>当CNI插件galaxy-sdn调用Galaxy守护进程的/cni端点时，Galaxy会将请求从：</p>
<pre class="crayon-plain-tag">// Request sent to the Galaxy by the Galaxy SDN CNI plugin
type CNIRequest struct {
    // CNI environment variables, like CNI_COMMAND and CNI_NETNS
    Env map[string]string `json:"env,omitempty"`
    // CNI configuration passed via stdin to the CNI plugin
    Config []byte `json:"config,omitempty"`
}</pre>
<p>转换为PodRequest：</p>
<pre class="crayon-plain-tag">type PodRequest struct {
    // 需要执行的CNI命令
    Command string
    // 来自环境变量CNI_ARGS
    PodNamespace string
    // 来自环境变量CNI_ARGS
    PodName string
    // kubernetes pod ports
    Ports []k8s.Port
    // 存放操作结果的通道
    Result chan *PodResult
    // 通过环境变量传来的CNI_IFNAME、CNI_COMMAND、CNI_ARGS...
    *skel.CmdArgs
    // Galaxy需要委托其它CNI插件完成工作，这是传递给那些插件的参数
    //                 插件类型    参数名  参数值
    ExtendedCNIArgs map[string]map[string]json.RawMessage
}

// 请求处理结果
type PodResult struct {
    Response []byte
    Err error
}</pre>
<p>然后调用requestFunc方法处理转换后的CNI请求：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) requestFunc(req *galaxyapi.PodRequest) (data []byte, err error) {
	start := time.Now()
	glog.Infof("%v, %s+", req, start.Format(time.StampMicro))
	// 处理ADD命令
	if req.Command == cniutil.COMMAND_ADD {
		defer func() {
			glog.Infof("%v, data %s, err %v, %s-", req, string(data), err, start.Format(time.StampMicro))
		}()
		var pod *corev1.Pod
		// 查找Pod
		pod, err = g.getPod(req.PodName, req.PodNamespace)
		if err != nil {
			return
		}
		result, err1 := g.cmdAdd(req, pod)
		if err1 != nil {
			err = err1
			return
		} else {
			// 转换结果格式
			result020, err2 := convertResult(result)
			if err2 != nil {
				err = err2
			} else {
				data, err = json.Marshal(result)
				if err != nil {
					return
				}
				// 如果处理成功，则执行端口映射，回顾一下启动的时候会对所有本节点现存的Pod进行端口映射
				err = g.setupPortMapping(req, req.ContainerID, result020, pod)
				if err != nil {
					g.cleanupPortMapping(req)
					return
				}
				pod.Status.PodIP = result020.IP4.IP.IP.String()
				// 处理网络策略
				if g.pm != nil {
					if err := g.pm.SyncPodChains(pod); err != nil {
						glog.Warning(err)
					}
					g.pm.SyncPodIPInIPSet(pod, true)
				}
			}
		}
	} else if req.Command == cniutil.COMMAND_DEL {
		defer glog.Infof("%v err %v, %s-", req, err, start.Format(time.StampMicro))
		err = cniutil.CmdDel(req.CmdArgs, -1)
		if err == nil {
			err = g.cleanupPortMapping(req)
		}
	} else {
		err = fmt.Errorf("unknown command %s", req.Command)
	}
	return
}</pre>
<p>requestFunc方法就是查询出Pod，将其作为参数的一部分，再调用 cmdAdd方法。需要注意，由于CNI版本很低，仅仅支持ADD/DEL两个命令。</p>
<p>ADD命令的处理逻辑如下：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) cmdAdd(req *galaxyapi.PodRequest, pod *corev1.Pod) (types.Result, error) {
	// 通过解析Pod的注解、spec特殊字段，来构造出Pod的网络需求
	networkInfos, err := g.resolveNetworks(req, pod)
	if err != nil {
		return nil, err
	}
	return cniutil.CmdAdd(req.CmdArgs, networkInfos)
}

func CmdAdd(cmdArgs *skel.CmdArgs, networkInfos []*NetworkInfo) (types.Result, error) {
	// 每个NetworkInfo代表Pod需要加入的一个网络
	if len(networkInfos) == 0 {
		return nil, fmt.Errorf("No network info returned")
	}
	// 将网络需求的JSON格式保存在/var/lib/cni/galaxy/$ContainerID
	if err := saveNetworkInfo(cmdArgs.ContainerID, networkInfos); err != nil {
		return nil, fmt.Errorf("Error save network info %v for %s: %v", networkInfos, cmdArgs.ContainerID, err)
	}
	var (
		err    error
		// 前一个网络的结果
		result types.Result
	)
	// 处理并满足每一个网络需求
	for idx, networkInfo := range networkInfos {
		// 将来自k8s.v1.cni.galaxy.io/args注解的CNI参数添加到参数数组
		cmdArgs.Args = strings.TrimRight(fmt.Sprintf("%s;%s", cmdArgs.Args, BuildCNIArgs(networkInfo.Args)), ";")
		if result != nil {
			networkInfo.Conf["prevResult"] = result
		}
		// 委托给具体的CNI插件的ADD命令
		result, err = DelegateAdd(networkInfo.Conf, cmdArgs, networkInfo.IfName)
		if err != nil {
			// 如果失败，删除所有已经创建的CNI
			glog.Errorf("fail to add network %s: %v, begin to rollback and delete it", networkInfo.Args, err)
			// 从idx开始，倒过来依次为每种网络需求调用DEL命令
			delErr := CmdDel(cmdArgs, idx)
			glog.Warningf("fail to delete cni in rollback %v", delErr)
			return nil, fmt.Errorf("fail to establish network %s:%v", networkInfo.Args, err)
		}
	}
	if err != nil {
		return nil, err
	}
	return result, nil
}</pre>
<p>处理每一个网络需求时，会委托给相应的CNI插件：</p>
<pre class="crayon-plain-tag">// 每个插件的NetConf是在前面resolveNetworks时，从confdir=/etc/cni/net.d/读取出来的
func DelegateAdd(netconf map[string]interface{}, args *skel.CmdArgs, ifName string) (types.Result, error) {
	netconfBytes, err := json.Marshal(netconf)
	if err != nil {
		return nil, fmt.Errorf("error serializing delegate netconf: %v", err)
	}
	typ, err := getNetworkType(netconf)
	if err != nil {
		return nil, err
	}
	pluginPath, err := invoke.FindInPath(typ, strings.Split(args.Path, ":"))
	if err != nil {
		return nil, err
	}
	// 通过命令行调用
	glog.Infof("delegate add %s args %s conf %s", args.ContainerID, args.Args, string(netconfBytes))
	return invoke.ExecPluginWithResult(pluginPath, netconfBytes, &amp;invoke.Args{
		Command:       "ADD",
		ContainerID:   args.ContainerID,
		NetNS:         args.Netns,
		PluginArgsStr: args.Args,
		IfName:        ifName,
		Path:          args.Path,
	})
}</pre>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>这里做个小结：</p>
<ol>
<li>工作负载的网络需求（需要加入到哪些CNI网络），可以通过注解、Spec配置</li>
<li>Galaxy通过读取Pod注解、Spec，构造出网络列表</li>
<li>Galaxy会遍历CNI网络列表，依次通过命令行调用对应的CNI插件的ADD命令</li>
<li>如果遍历过程中出错，逆序的调用已经ADD的CNI插件的DEL命令</li>
</ol>
<p>第3、4其实就是新版本的CNI中NetworkList提供的能力。</p>
<div class="blog_h2"><span class="graybg">Galaxy IPAM</span></div>
<p>和Galaxy守护进程一样，Galaxy IPAM也是一个HTTP服务器。只是前者仅仅供本机的CNI插件galaxy-sdn调用，因此使用UDS，而Galaxy IPAM是供Scheduler调用，因此使用TCP。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>入口点如下：</p>
<pre class="crayon-plain-tag">func main() {
	// initialize rand seed
	rand.Seed(time.Now().UTC().UnixNano())

	s := server.NewServer()
	// add command line args
	s.AddFlags(pflag.CommandLine)

	flag.InitFlags()
	logs.InitLogs()
	defer logs.FlushLogs()

	// if checking version, print it and exit
	ldflags.PrintAndExitIfRequested()

	if err := s.Start(); err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err) // nolint: errcheck
		os.Exit(1)
	}
}</pre>
<p>启动Galaxy IPAM服务器： </p>
<pre class="crayon-plain-tag">func (s *Server) Start() error {
	if err := s.init(); err != nil {
		return fmt.Errorf("init server: %v", err)
	}
	// 启用InformerFactory监控K8S资源变化
	s.StartInformers(s.stopChan)
	// 多实例部署的Leader选举
	if s.LeaderElection.LeaderElect &amp;&amp; s.leaderElectionConfig != nil {
		leaderelection.RunOrDie(context.Background(), *s.leaderElectionConfig)
		return nil
	}
	return s.Run()
}

// Galaxy IPAM初始化
func (s *Server) init() error {
	// 读取配置文件
	if options.JsonConfigPath == "" {
		return fmt.Errorf("json config is required")
	}
	data, err := ioutil.ReadFile(options.JsonConfigPath)
	if err != nil {
		return fmt.Errorf("read json config: %v", err)
	}
	if err := json.Unmarshal(data, &amp;s.JsonConf); err != nil {
		return fmt.Errorf("bad config %s: %v", string(data), err)
	}
	// 初始化K8S客户端、IPAMContext
	s.initk8sClient()
	// 此浮动IP插件，实现了PodWatcher接口，能够监听Pod的生命周期事件
	s.plugin, err = schedulerplugin.NewFloatingIPPlugin(s.SchedulePluginConf, s.IPAMContext)
	if err != nil {
		return err
	}
	// 当有Pod事件后，调用浮动IP插件
	s.PodInformer.Informer().AddEventHandler(eventhandler.NewPodEventHandler(s.plugin))
	return nil
}

func (s *Server) Run() error {
	// 初始化浮动IP插件
	if err := s.plugin.Init(); err != nil {
		return err
	}
	// 运行浮动IP插件
	s.plugin.Run(s.stopChan)
	// 注册API路由
	go s.startAPIServer()
	// 注册供Scheduler调度的路由 /v1/filter、/v1/priority、/v1/bind
	s.startServer()
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">FloatingIPPlugin</span></div>
<p>在上面的初始化流程中我们看到，在运行Galaxy IPAM服务器时，会初始化并运行s.plugin。类型为FloatingIPPlugin。</p>
<p>它是一个Pod事件监听器，实现接口：</p>
<pre class="crayon-plain-tag">type PodWatcher interface {
	AddPod(pod *corev1.Pod) error
	UpdatePod(oldPod, newPod *corev1.Pod) error
	DeletePod(pod *corev1.Pod) error
}</pre>
<p>当Pod事件发生后，上述方法会被调用。</p>
<p>它的初始化逻辑如下：</p>
<pre class="crayon-plain-tag">//                                  这个上下文包含各种客户端、Lister、Informer
func NewFloatingIPPlugin(conf Conf, ctx *context.IPAMContext) (*FloatingIPPlugin, error) {
	conf.validate()
	glog.Infof("floating ip config: %v", conf)
	plugin := &amp;FloatingIPPlugin{
		nodeSubnet:  make(map[string]*net.IPNet),
		IPAMContext: ctx,
		conf:        &amp;conf,
		unreleased:  make(chan *releaseEvent, 50000),
		// 这是一个哈希，每个key都可以被请求加锁
		dpLockPool:  keymutex.NewHashed(500000),
		podLockPool: keymutex.NewHashed(500000),
		crdKey:      NewCrdKey(ctx.ExtensionLister),
		crdCache:    crd.NewCrdCache(ctx.DynamicClient, ctx.ExtensionLister, 0),
	}
	// 初始化IPAM
	plugin.ipam = floatingip.NewCrdIPAM(ctx.GalaxyClient, plugin.FIPInformer)
	if conf.CloudProviderGRPCAddr != "" {
		plugin.cloudProvider = cloudprovider.NewGRPCCloudProvider(conf.CloudProviderGRPCAddr)
	}
	return plugin, nil
}

func (p *FloatingIPPlugin) Init() error {
	if len(p.conf.FloatingIPs) &gt; 0 {
		// 初始化IP池
		if err := p.ipam.ConfigurePool(p.conf.FloatingIPs); err != nil {
			return err
		}
	} else {
		// 配置文件中没有浮动IP，从ConfigMap中拉取
		glog.Infof("empty floatingips from config, fetching from configmap")
		if err := wait.PollInfinite(time.Second, func() (done bool, err error) {
			updated, err := p.updateConfigMap()
			if err != nil {
				glog.Warning(err)
			}
			return updated, nil
		}); err != nil {
			return fmt.Errorf("failed to get floatingip config from configmap: %v", err)
		}
	}
	glog.Infof("plugin init done")
	return nil
}</pre>
<p>它的run方法的逻辑，主要包括三部分：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) Run(stop chan struct{}) {
	if len(p.conf.FloatingIPs) == 0 {
		go wait.Until(func() {
// 1. 定期刷新ConfigMap floatingip-config，获取最新的浮动IP配置，并调用ipam.ConfigurePool配置IP池
			if _, err := p.updateConfigMap(); err != nil {
				glog.Warning(err)
			}
		}, time.Minute, stop)
	}
	go wait.Until(func() {
// 2. 定期同步Pod状态，必要的情况下进行IP释放
		if err := p.resyncPod(); err != nil {
			glog.Warningf("resync pod: %v", err)
		}
		p.syncPodIPsIntoDB()
	}, time.Duration(p.conf.ResyncInterval)*time.Minute, stop)
	for i := 0; i &lt; 5; i++ {
// 3. 从通道中读取IP释放事件，解除IP和Pod的绑定关系
		go p.loop(stop)
	}
}

func (p *FloatingIPPlugin) updateConfigMap() (bool, error) {
	// 拉取ConfigMap floatingip-config
	cm, err := p.Client.CoreV1().ConfigMaps(p.conf.ConfigMapNamespace).Get(p.conf.ConfigMapName, v1.GetOptions{})
	if err != nil {
		return false, fmt.Errorf("failed to get floatingip configmap %s_%s: %v", p.conf.ConfigMapName,
			p.conf.ConfigMapNamespace, err)
	}
	val, ok := cm.Data[p.conf.FloatingIPKey]
	if !ok {
		return false, fmt.Errorf("configmap %s_%s doesn't have a key floatingips", p.conf.ConfigMapName,
			p.conf.ConfigMapNamespace)
	}
	var updated bool
	// 调用IPAM，更新IP池
	if updated, err = p.ensureIPAMConf(&amp;p.lastIPConf, val); err != nil {
		return false, err
	}
	defer func() {
		if !updated {
			return
		}
		// 浮动IP配置更新，则节点允许的子网信息被清空
		p.nodeSubnetLock.Lock()
		defer p.nodeSubnetLock.Unlock()
		p.nodeSubnet = map[string]*net.IPNet{}
	}()
	return true, nil
}

// 释放以下Pod的IP
// 1. 所有者TAPP不存在的、已被删除的Pod
// 2. 所有者StatefulSet/Deployment存在、已被删除的Pod，但是没有配置IP为不变的
// 3. 所有者Deployment不需要这么多IP的、已被删除的的Pod
// 4. 所有者StatefulSet的副本数超过小于被删除Pod索引的
// 5. 未被删除，但是被驱逐的Pod
func (p *FloatingIPPlugin) resyncPod() error {
	glog.V(4).Infof("resync pods+")
	defer glog.V(4).Infof("resync pods-")
	resyncMeta := &amp;resyncMeta{}
	if err := p.fetchChecklist(resyncMeta); err != nil {
		return err
	}
	p.resyncAllocatedIPs(resyncMeta)
	return nil
}

// 拉取并处理待释放事件
func (p *FloatingIPPlugin) loop(stop chan struct{}) {
	for {
		select {
		case &lt;-stop:
			return
		case event := &lt;-p.unreleased:
			go func(event *releaseEvent) {
				// 解除Pod和IP地址的绑定关系
				if err := p.unbind(event.pod); err != nil {
					event.retryTimes++
					if event.retryTimes &gt; 3 {
						// leave it to resync to protect chan from explosion
						glog.Errorf("abort unbind for pod %s, retried %d times: %v", util.PodName(event.pod),
							event.retryTimes, err)
					} else {
						glog.Warningf("unbind pod %s failed for %d times: %v", util.PodName(event.pod),
							event.retryTimes, err)
						// backoff time if required
						time.Sleep(100 * time.Millisecond * time.Duration(event.retryTimes))
						p.unreleased &lt;- event
					}
				}
			}(event)
		}
	}
}</pre>
<p>当监听到Pod事件时，可能需要和IPAM同步Pod IP，或者产生一个待释放事件：</p>
<pre class="crayon-plain-tag">// AddPod does nothing
func (p *FloatingIPPlugin) AddPod(pod *corev1.Pod) error {
	return nil
}

// Pod更新时，和IPAM同步Pod IP
func (p *FloatingIPPlugin) UpdatePod(oldPod, newPod *corev1.Pod) error {
	if !p.hasResourceName(&amp;newPod.Spec) {
		return nil
	}
	// 先前的pod.Status.Phase不是终点状态（Failed/Succeeded），这一次是
	// 意味着Pod运行完毕，需要释放IP。通常是Job类Pod
	if !finished(oldPod) &amp;&amp; finished(newPod) {
		// Deployments will leave evicted pods
		// If it's a evicted one, release its ip
		glog.Infof("release ip from %s_%s, phase %s", newPod.Name, newPod.Namespace, string(newPod.Status.Phase))
		p.unreleased &lt;- &amp;releaseEvent{pod: newPod}
		return nil
	}
	// 同步Pod IP
	if err := p.syncPodIP(newPod); err != nil {
		glog.Warningf("failed to sync pod ip: %v", err)
	}
	return nil
}

func (p *FloatingIPPlugin) DeletePod(pod *corev1.Pod) error {
	if !p.hasResourceName(&amp;pod.Spec) {
		return nil
	}
	glog.Infof("handle pod delete event: %s_%s", pod.Name, pod.Namespace)
	// Pod删除后，产生一个待释放IP事件
	p.unreleased &lt;- &amp;releaseEvent{pod: pod}
	return nil
}</pre>
<p>和IPAM同步Pod IP的逻辑：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) syncPodIP(pod *corev1.Pod) error {
	// 只有到达Running状态的，才同步
	if pod.Status.Phase != corev1.PodRunning {
		return nil
	}
	// 同步干的事情是，如果Pod就有ipinfos注解，并且IP地址没有在池中分配，那么在池中将IP分配给Pod
	if pod.Annotations == nil {
		return nil
	}
	// 这个有点意思，lockpod会立即调用，并且返回一个unlock函数，此函数会在syncPodIP退出时执行。代码简洁
	// 防止有其它线程再操作此Pod
	defer p.lockPod(pod.Name, pod.Namespace)()
	keyObj, err := util.FormatKey(pod)
	if err != nil {
		glog.V(5).Infof("sync pod %s/%s ip formatKey with error %v", pod.Namespace, pod.Name, err)
		return nil
	}
	cniArgs, err := constant.UnmarshalCniArgs(pod.Annotations[constant.ExtendedCNIArgsAnnotation])
	if err != nil {
		return err
	}
	ipInfos := cniArgs.Common.IPInfos
	for i := range ipInfos {
		if ipInfos[i].IP == nil || ipInfos[i].IP.IP == nil {
			continue
		}
		// 遍历所有注解中的IP地址，调用IPAM的AllocateSpecificIP方法分配IP
		if err := p.syncIP(keyObj.KeyInDB, ipInfos[i].IP.IP, pod); err != nil {
			glog.Warningf("sync pod %s ip %s: %v", keyObj.KeyInDB, ipInfos[i].IP.IP.String(), err)
		}
	}
	return nil
}</pre>
<p>小结一下FloatingIPPlugin的职责。它主要负责监控ConfigMap、Pod的变化。当ConfigMap变化后更新IPAM的配置，当Pod发生变化时，更新IPAM池中的IP分配信息。</p>
<p>此外FloatingIPPlugin还提供了调度的核心算法，包括Filter、Prioritize、Bind这几个方法，我们在后面在探讨。</p>
<div class="blog_h3"><span class="graybg">IPAM</span></div>
<p>FloatingIPPlugin.ipam字段，代表了IP地址分配管理的核心，它的接口：</p>
<pre class="crayon-plain-tag">type IPAM interface {
	// 初始化IP池
	ConfigurePool([]*FloatingIPPool) error
	// 释放映射中键匹配的IP，返回已分配、未分配IP地址的映射
	ReleaseIPs(map[string]string) (map[string]string, map[string]string, error)
	// 为Pod分配指定的IP
	AllocateSpecificIP(string, net.IP, Attr) error
	// 在子网中分配IP
	AllocateInSubnet(string, *net.IPNet, Attr) (net.IP, error)
	// 在给定的多个IP范围中，都分配一个IP
	AllocateInSubnetsAndIPRange(string, *net.IPNet, [][]nets.IPRange, Attr) ([]net.IP, error)
	// 在指定的子网中，以指定的key分配IP
	AllocateInSubnetWithKey(oldK, newK, subnet string, attr Attr) error
	// 保留IP
	ReserveIP(oldK, newK string, attr Attr) (bool, error)
	// 释放IP
	Release(string, net.IP) error
	// 根据Key返回的一个匹配
	First(string) (*FloatingIPInfo, error)
	// 将IP地址转换为浮动IP对象
	ByIP(net.IP) (FloatingIP, error)
	// 根据前缀查询
	ByPrefix(string) ([]*FloatingIPInfo, error)
	// 根据关键字查询
	ByKeyword(string) ([]FloatingIP, error)
	// 返回节点的子网信息
	NodeSubnet(net.IP) *net.IPNet
} </pre>
<div class="blog_h3"><span class="graybg">调度器扩展</span></div>
<p>Galaxy IPAM对接K8S调度器，使用的是Scheduler Extender Webhook，也就是它提供若干HTTP接口供调度器使用。</p>
<p>这些接口实现为Galaxy IPAM Server的方法，在startServer()的时候注册：</p>
<pre class="crayon-plain-tag">// 筛选匹配的节点
func (s *Server) filter(request *restful.Request, response *restful.Response) {
	// K8S标准API，ExtenderArgs包含了当前正被调度的Pod，以及可用的（已经被筛选过的）节点列表
	args := new(schedulerapi.ExtenderArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST filter %v", *args)
	start := time.Now()
	glog.V(3).Infof("filtering %s_%s, start at %d+", args.Pod.Name, args.Pod.Namespace, start.UnixNano())
	// 调用FloatingIPPlugin
	filteredNodes, failedNodesMap, err := s.plugin.Filter(&amp;args.Pod, args.Nodes.Items)
	glog.V(3).Infof("filtering %s_%s, start at %d-", args.Pod.Name, args.Pod.Namespace, start.UnixNano())
	args.Nodes.Items = filteredNodes
	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	_ = response.WriteEntity(schedulerapi.ExtenderFilterResult{
		// 可以调度的节点
		Nodes:       args.Nodes,
		// 不可调度的节点，以及不可调度的原因
		FailedNodes: failedNodesMap,
		// 错误信息
		Error:       errStr,
	})
}

// 节点优先级判定（评分）
func (s *Server) priority(request *restful.Request, response *restful.Response) {
	args := new(schedulerapi.ExtenderArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST priority %v", *args)
	// 调用FloatingIPPlugin
	hostPriorityList, err := s.plugin.Prioritize(&amp;args.Pod, args.Nodes.Items)
	if err != nil {
		glog.Warningf("prioritize err: %v", err)
	}
	_ = response.WriteEntity(*hostPriorityList)
}


// 绑定Pod到节点
func (s *Server) bind(request *restful.Request, response *restful.Response) {
	args := new(schedulerapi.ExtenderBindingArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST bind %v", *args)
	start := time.Now()
	glog.V(3).Infof("binding %s_%s to %s, start at %d+", args.PodName, args.PodNamespace, args.Node, start.UnixNano())
	// 调用FloatingIPPlugin
	err := s.plugin.Bind(args)
	glog.V(3).Infof("binding %s_%s to %s, start at %d-", args.PodName, args.PodNamespace, args.Node, start.UnixNano())
	var result schedulerapi.ExtenderBindingResult
	if err != nil {
		glog.Warningf("bind err: %v", err)
		result.Error = err.Error()
	}
	_ = response.WriteEntity(result)
}</pre>
<p>这些方法仅仅是负责HTTP相关处理，核心是上文我们提到的FloatingIPPlugin的几个方法：</p>
<pre class="crayon-plain-tag">// 没有可用IP的节点被标记为failedNodes
// 如果Pod不需要浮动IP，则failedNodes为空
func (p *FloatingIPPlugin) Filter(pod *corev1.Pod, nodes []corev1.Node) (
    []corev1.Node, schedulerapi.FailedNodesMap, error) {
	start := time.Now()
	failedNodesMap := schedulerapi.FailedNodesMap{}
	// 没有指定自定义资源tke.cloud.tencent.com/eni-ip的request，认为不需要浮动IP
	if !p.hasResourceName(&amp;pod.Spec) {
		return nodes, failedNodesMap, nil
	}
	filteredNodes := []corev1.Node{}
	// 开始过滤，期间锁定针对此Pod的操作
	defer p.lockPod(pod.Name, pod.Namespace)()
	// 读取注解k8s.v1.cni.galaxy.io/args，获取Pod要加入的子网信息
	subnetSet, err := p.getSubnet(pod)
	if err != nil {
		return filteredNodes, failedNodesMap, err
	}
	// 遍历所有节点
	for i := range nodes {
		nodeName := nodes[i].Name
		// 得到节点所属的子网
		subnet, err := p.getNodeSubnet(&amp;nodes[i])
		if err != nil {
			failedNodesMap[nodes[i].Name] = err.Error()
			continue
		}
		// 如果节点可以分配Pod要加入的子网，则保留节点，否则，丢弃节点
		if subnetSet.Has(subnet.String()) {
			filteredNodes = append(filteredNodes, nodes[i])
		} else {
			failedNodesMap[nodeName] = "FloatingIPPlugin:NoFIPLeft"
		}
	}
	if glog.V(5) {
		nodeNames := make([]string, len(filteredNodes))
		for i := range filteredNodes {
			nodeNames[i] = filteredNodes[i].Name
		}
		glog.V(5).Infof("filtered nodes %v failed nodes %v for %s_%s", nodeNames, failedNodesMap,
			pod.Namespace, pod.Name)
	}
	metrics.ScheduleLatency.WithLabelValues("filter").Observe(time.Since(start).Seconds())
	return filteredNodes, failedNodesMap, nil
}

// 打分目前是空实现
func (p *FloatingIPPlugin) Prioritize(pod *corev1.Pod, nodes []corev1.Node) (*schedulerapi.HostPriorityList, error) {
	list := &amp;schedulerapi.HostPriorityList{}
	if !p.hasResourceName(&amp;pod.Spec) {
		return list, nil
	}
	//TODO
	return list, nil
}

// 将新的浮动IP绑定给Pod，或者重用以有的IP
func (p *FloatingIPPlugin) Bind(args *schedulerapi.ExtenderBindingArgs) error {
	start := time.Now()
	pod, err := p.PodLister.Pods(args.PodNamespace).Get(args.PodName)
	if err != nil {
		return fmt.Errorf("failed to find pod %s: %w", util.Join(args.PodName, args.PodNamespace), err)
	}
	if !p.hasResourceName(&amp;pod.Spec) {
		return fmt.Errorf("pod which doesn't want floatingip have been sent to plugin")
	}
	defer p.lockPod(pod.Name, pod.Namespace)()
	// 为Pod生成键，键由要加入的池、Pod命名空间、Pod名字、所属StatefulSet/Deployment的名字构成
	keyObj, err := util.FormatKey(pod)
	if err != nil {
		return err
	}
	// 分配IP给Pod
	cniArgs, err := p.allocateIP(keyObj.KeyInDB, args.Node, pod)
	if err != nil {
		return err
	}
	data, err := json.Marshal(cniArgs)
	if err != nil {
		return fmt.Errorf("marshal cni args %v: %v", *cniArgs, err)
	}
	// 添加k8s.v1.cni.galaxy.io/args注解
	bindAnnotation := map[string]string{constant.ExtendedCNIArgsAnnotation: string(data)}
	var err1 error
	if err := wait.PollImmediate(time.Millisecond*500, 3*time.Second, func() (bool, error) {
		// 执行绑定操作，为Pod添加binding子资源
		if err := p.Client.CoreV1().Pods(args.PodNamespace).Bind(&amp;corev1.Binding{
			ObjectMeta: v1.ObjectMeta{Namespace: args.PodNamespace, Name: args.PodName, UID: args.PodUID,
				Annotations: bindAnnotation},
			Target: corev1.ObjectReference{
				Kind: "Node",
				Name: args.Node,
			},
		}); err != nil {
			err1 = err
			if apierrors.IsNotFound(err) {
				// Pod已经不存在了，终止轮询
				return false, err
			}
			return false, nil
		}
		glog.Infof("bind pod %s to %s with %s", keyObj.KeyInDB, args.Node, string(data))
		return true, nil
	}); err != nil {
		if apierrors.IsNotFound(err1) {
			// 绑定过程中发现Pod消失，说明有人将其删除了。已经分配的IP需要回收
			glog.Infof("binding returns not found for pod %s, putting it into unreleased chan", keyObj.KeyInDB)
			// attach ip annotation
			p.unreleased &lt;- &amp;releaseEvent{pod: pod}
		}
		// If fails to update, depending on resync to update
		return fmt.Errorf("update pod %s: %w", keyObj.KeyInDB, err1)
	}
	metrics.ScheduleLatency.WithLabelValues("bind").Observe(time.Since(start).Seconds())
	return nil
}</pre>
<p>我们再来看一下在Bind阶段，分配IP的细节：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) allocateIP(key string, nodeName string, pod *corev1.Pod) (*constant.CniArgs, error) {
	cniArgs, err := getPodCniArgs(pod)
	if err != nil {
		return nil, err
	}
	ipranges := cniArgs.RequestIPRange</pre>
<p>getPodCniArgs读取k8s.v1.cni.galaxy.io/args注解为结构： </p>
<pre class="crayon-plain-tag">type CniArgs struct {
	// 用户可以指定从什么地址范围分配IP
	RequestIPRange [][]nets.IPRange `json:"request_ip_range,omitempty"`
	// 这些参数最终会传递给底层CNI插件，作为key1=val1;key2=val2格式的参数
	Common CommonCniArgs `json:"common"`
}</pre>
<p>allocateIP会使用key去查询已经分配给key的、每个iprange中的IP信息：</p>
<pre class="crayon-plain-tag">ipInfos, err := p.ipam.ByKeyAndIPRanges(key, ipranges)
	if err != nil {
		return nil, fmt.Errorf("failed to query floating ip by key %s: %v", key, err)
	}
	if len(ipranges) == 0 &amp;&amp; len(ipInfos) &gt; 0 {
		// reuse only one if requesting only one ip
		ipInfos = ipInfos[:1]
	}
	// 那些尚未分配IP地址的ipranges
	var unallocatedIPRange [][]nets.IPRange // those does not have allocated ips
	reservedIPs := sets.NewString()
	for i := range ipInfos {
		if ipInfos[i] == nil {
			// 未分配
			unallocatedIPRange = append(unallocatedIPRange, ipranges[i])
		} else {
			// 已经分配给当前key，需要保留
			reservedIPs.Insert(ipInfos[i].IP.String())
		}
	}
	policy := parseReleasePolicy(&amp;pod.ObjectMeta)
	attr := floatingip.Attr{Policy: policy, NodeName: nodeName, Uid: string(pod.UID)}</pre>
<p>检查从IPAM获取的ipinfos，如果PodUid和当前Pod.GetUID()，说明可能先前版本的Pod尚未删除，等待删除（bind函数返回错误）：</p>
<pre class="crayon-plain-tag">for _, ipInfo := range ipInfos {
		// check if uid missmatch, if we delete a statfulset/tapp and creates a same name statfulset/tapp immediately,
		// galaxy-ipam may receive bind event for new pod early than deleting event for old pod
		if ipInfo != nil &amp;&amp; ipInfo.PodUid != "" &amp;&amp; ipInfo.PodUid != string(pod.GetUID()) {
			return nil, fmt.Errorf("waiting for delete event of %s before reuse this ip", key)
		}
	}</pre>
<p>如果有需要新分配的IP，调用IPAM进行分配：</p>
<pre class="crayon-plain-tag">if len(unallocatedIPRange) &gt; 0 || len(ipInfos) == 0 {
		// 节点所属的子网
		subnet, err := p.queryNodeSubnet(nodeName)
		if err != nil {
			return nil, err
		}
		// 执行分配
		if _, err := p.ipam.AllocateInSubnetsAndIPRange(key, subnet, unallocatedIPRange, attr); err != nil {
			return nil, err
		}
		// 更新IP信息
		ipInfos, err = p.ipam.ByKeyAndIPRanges(key, ipranges)
		if err != nil {
			return nil, fmt.Errorf("failed to query floating ip by key %s: %v", key, err)
		}
	}</pre>
<p>检查已分配IP重用的情况，并更新IPAM属性： </p>
<pre class="crayon-plain-tag">for _, ipInfo := range ipInfos {
		//...
		if reservedIPs.Has(ipInfo.IP.String()) {
			glog.Infof("%s reused %s, updating attr to %v", key, ipInfo.IPInfo.IP.String(), attr)
			if err := p.ipam.UpdateAttr(key, ipInfo.IPInfo.IP.IP, attr); err != nil {
				return nil, fmt.Errorf("failed to update floating ip release policy: %v", err)
			}
		}
	}</pre>
<p>回填Pod注解：</p>
<pre class="crayon-plain-tag">// 新分配的IP地址
	var allocatedIPs []string
	// 所有IP地址
	var ret []constant.IPInfo
	for _, ipInfo := range ipInfos {
		if !reservedIPs.Has(ipInfo.IP.String()) {
			allocatedIPs = append(allocatedIPs, ipInfo.IP.String())
		}
		ret = append(ret, ipInfo.IPInfo)
	}
	glog.Infof("%s reused ips %v, allocated ips %v, attr %v", key, reservedIPs.List(), allocatedIPs, attr)
	// 回填
	cniArgs.Common.IPInfos = ret
	return &amp;cniArgs, nil</pre>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>当一个Pod创建时，如果它的Spec上具有自定义的资源请求tke.cloud.tencent.com/eni-ip，则调度器认为，需要为它分配浮动IP。</p>
<p>由于在floatingip-config中，可以配置多组浮动IP子网。Pod应该通过注解：</p>
<ol>
<li>k8s.v1.cni.galaxy.io/args 来声明自己需要加入什么子网、需要请求什么范围的IP地址</li>
<li>tke.cloud.tencent.com/eni-ip-pool 来声明需要使用哪个IP池</li>
</ol>
<p>Pod创建后，首先进入调度阶段。作为K8S调度器扩展的Galaxy IPAM会按需进行IP分配，并将结果写入到Pod的注解中。</p>
<p>当Kubelet在本地启动Pod时， 会调用CNI插件galaxy-sdn，galaxy-sdn则会调用Galaxy守护进程。后者则<span style="background-color: #c0c0c0;">通过命令行调用galaxy-k8s-vlan等底层CNI插件，并且将Pod注解k8s.v1.cni.galaxy.io/args中的键值转换为key1=val1;key2=val2CNI参数传递</span>。</p>
<p>galaxy-k8s-vlan插件会调用ipam.Allocate，获取CNI参数中的IP地址信息，并转换为v0.20格式的Result，然后调用setupNetwork将IP地址设置到容器的网络接口上。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/galaxy-study-note">Galaxy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/galaxy-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Cilium学习笔记</title>
		<link>https://blog.gmem.cc/cilium</link>
		<comments>https://blog.gmem.cc/cilium#comments</comments>
		<pubDate>Mon, 22 Jun 2020 10:56:30 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37551</guid>
		<description><![CDATA[<p>简介 Cilium Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。 Hubble Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。 由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。 Hubble可以回答以下问题： 服务依赖和通信关系图： 两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？ 进行了哪些HTTP调用？ 服务消费了哪些Kafka主题，发布了哪些Kafka主题 网络监控和报警： 网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因 最近5分钟有哪些服务存在DNS解析问题 哪些服务出现连接超时、中断的问题 无应答SYN请求的频率是多高 应用程序监控： 特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高 HTTP请求延迟的95th/99th位数是多少 <a class="read-more" href="https://blog.gmem.cc/cilium">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">Cilium</span></div>
<p>Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。</p>
<div class="blog_h2"><span class="graybg">Hubble</span></div>
<p>Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。</p>
<p>由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。</p>
<p>Hubble可以回答以下问题：</p>
<ol>
<li>服务依赖和通信关系图：
<ol>
<li>两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？</li>
<li>进行了哪些HTTP调用？</li>
<li>服务消费了哪些Kafka主题，发布了哪些Kafka主题</li>
</ol>
</li>
<li>网络监控和报警：
<ol>
<li>网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因</li>
<li>最近5分钟有哪些服务存在DNS解析问题</li>
<li>哪些服务出现连接超时、中断的问题</li>
<li>无应答SYN请求的频率是多高</li>
</ol>
</li>
<li>应用程序监控：
<ol>
<li>特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高</li>
<li>HTTP请求延迟的95th/99th位数是多少</li>
<li>哪两个服务之间的延迟最高</li>
</ol>
</li>
<li>安全可观察性：
<ol>
<li>哪些服务因为网络策略而出现连接被阻止</li>
<li>哪些服务被从集群外部访问</li>
<li>哪些服务尝试解析了特定的域名</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">优势</span></div>
<p>在监控（系统和应用）领域，从来没有一个技术能像eBPF一样做到如此的高性能、细粒度、透明化，以及动态性。</p>
<p>现代数据中心中运行的应用程序，通常基于微服务架构设计，应用程序被拆分为大量独立的小服务，这些服务基于轻量级的协议（例如HTTP）进行通信。这些微服务通常容器化部署，可以动态按需创建、销毁、扩缩容。</p>
<p>这种容器化的微服务架构，在连接安全性方面引入了挑战。传统的Linux网络安全机制，例如iptables，基于IP地址、TCP/UDP端口进行过滤。在容器化架构下IP地址会很快变化，这会导致ACL规则、LB表需要不断的、加速（随着业务规模扩大）更新。由于IP地址不稳定，给实现精准可观察性也带来了挑战。</p>
<p>依赖于eBPF，Cilium能够基于服务/Pod/容器的标识（而非IP地址），实现安全策略的更新。能够在L7/L4/L3进行过滤。</p>
<div class="blog_h2"><span class="graybg">能力</span></div>
<div class="blog_h3"><span class="graybg">透明的保护API</span></div>
<p>能够在L7进行过滤，支持REST/HTTP、gRPC、Kafka等协议。从而实现：</p>
<ol>
<li>允许对/public/.*的GET请求，禁止其它任何请求</li>
<li>允许service1在Kafka的主题topic1上发布消息，service2在topic1上消费消息，禁止其它Kafka消息</li>
<li>要求所有HTTP请求具有头X-Token: [0-9]+</li>
</ol>
<div class="blog_h3"><span class="graybg">基于身份标识的安全访问</span></div>
<p>经典的容器防火墙，基于IP地址/端口来进行封包过滤，每当有新的容器启动，都要求所有服务器更新防火墙规则。</p>
<p>Cilium支持为一组应用程序分配身份标帜，共享同一安全策略。身份标识将关联到容器发出的所有网络封包，在接收封包的节点，可以校验身份信息。</p>
<div class="blog_h3"><span class="graybg">外部服务安全访问</span></div>
<p>上一段提到了，Cilium支持基于身份标识的内部服务之间的安全访问机制。对于外部服务，Cilium支持经典的基于CIDR的ingress/egress安全策略。</p>
<div class="blog_h3"><span class="graybg">简单的容器网络</span></div>
<p>Cilium支持一个简单的、扁平的L3网络，能够跨越多个集群，连接所有容器。通过使用host scope的IP分配器，IP分配被保持简单，每个主机可以独立进行分配分配，不需要相互协作。</p>
<p>支持以下多节点网络模型：</p>
<ol>
<li>Overlay：目前内置支持VxLAN和Geneve，所有Linux支持的封装格式都可以启用</li>
<li>Native Routing：也叫Direct Routing，使用Linux宿主机的路由表，底层网络必须具有路由容器IP的能力。支持原生的IP6网络，能够和云网络路由器协作</li>
</ol>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>Cilium实现了分布式的负载均衡，可以完全代替kube-proxy。LB基于eBPF实现，使用高效的、可无限扩容的哈希表来存储信息。</p>
<p>对于南北向负载均衡，Cilium作了最大化性能的优化。支持XDP、DSR（Direct Server Return，LB仅仅修改转发封包的目标MAC地址）。</p>
<p>对于东西向负载均衡，Cilium在内核套接字层（TCP连接时）执行高效的service-to-backend转换（通过eBPF直接修改封包），避免了更低层次（IP）中per-packet的NAT（依赖conntrack，在高并发或大量连接的情况下有若干问题）操作成本。</p>
<div class="blog_h3"><span class="graybg">带宽管理</span></div>
<p>Cilium利用eBPF实现高效的基于EDT（Earliest Departure Time）的egress限速，能够很大程度上避免HTB/TBF等经典qdisc的缺点，包括传输尾延迟，多队列NIC下的锁问题。</p>
<div class="blog_h3"><span class="graybg">监控和诊断</span></div>
<p>对于任何分布式系统，可观察性对于监控和故障诊断都非常重要。Cilium提供了更好的诊断工具：</p>
<ol>
<li>携带元数据的事件监控：当封包被丢弃时，不但报告源地址，而能提供 完整的发送者/接收者元数据</li>
<li>策略决策跟踪：支持跟踪并发现是什么策略导致封包丢弃或请求拒绝</li>
<li>支持通过Prometheus暴露指标</li>
<li>Hubble：一个专门为Cilium设计的可观察性平台，能够提供服务依赖图、监控和报警</li>
</ol>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">整体架构</span></div>
<p>整体上的组件架构如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch.png"><img class="size-large wp-image-37593 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch-1024x912.png" alt="cilium-arch" width="710" height="632" /></a></p>
<div class="blog_h2"><span class="graybg">Cilium组件</span></div>
<div class="blog_h3"><span class="graybg">Agent</span></div>
<p>cilium-agent在集群的每个节点上运行，它通过K8S或API接收描述网络、服务负载均衡、网络策略、可观察性的配置信息。</p>
<p>cilium-agent监听来自容器编排系统的事件，从而知晓哪些容器被启动/停止，它管理所有eBPF程序，后者控制所有网络访问。</p>
<p>Cilium利用eBPF实现datapath的过滤、修改、监控、重定向，需要<span style="background-color: #c0c0c0;">Linux 4.8+才能运行，推荐使用4.9.17+内核</span>（因为4.8已经EOL）。Cilium会自动探测内核版本，识别可用特性。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>cilium和cilium-agent一起安装，它和cilium-agent的REST API交互，从而探测本地agent的状态。CLI也提供了直接访问eBPF map的工具。</p>
<div class="blog_h3"><span class="graybg">Operator</span></div>
<p>负责集群范围的工作，它不在任何封包转发/网络策略决策的关键路径上，即使operator暂时不可用，集群仍然能正常运作。</p>
<p>根据配置，operator持续不可用一段时间后，可能出现问题：</p>
<ol>
<li>如果需要operator来分配IP地址，则会出现IPAM延迟，因此导致新的工作负载的调度延迟</li>
<li>由于没有operator来更新kvstore的心跳，会导致agent认为kvstore不健康，并重启</li>
</ol>
<div class="blog_h3"><span class="graybg">CNI插件</span></div>
<p>cilium-cni和当前节点上的Cilium API交互，触发必要的datapath配置，以提供容器网络、LB、网络策略。</p>
<div class="blog_h2"><span class="graybg">Hubble组件</span></div>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>在所有节点上运行，从cilium中抓去eBPF的可观察性数据。它被嵌入在cilium-agent中，以实现高性能和低overhead。它提供了一个gRPC服务，用于抓取flow和Prometheus指标。</p>
<div class="blog_h3"><span class="graybg">Relay</span></div>
<p>hubble-relay是一个独立组件，能够连接到所有Server，通过Server的gRPC API，获取全集群的可观察性数据。这些数据又通过一个API来暴露出去。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>hubble是一个命令行工具，能够连接到gRPC API、hubble-relay、本地server，来获取flow events。</p>
<div class="blog_h3"><span class="graybg">GUI</span></div>
<p>hubble-ui能够利用hubble-relay的可观测性数据，提供图形化的服务依赖、连接图。</p>
<div class="blog_h2"><span class="graybg">数据存储</span></div>
<p>Cilium需要一个数据存储，用来在Agent之间传播状态。</p>
<div class="blog_h3"><span class="graybg">K8S CRD</span></div>
<p>默认数据存储。</p>
<div class="blog_h3"><span class="graybg">KV Store</span></div>
<p>外部键值存储，可以提供更好的性能。支持etcd和consul。</p>
<div class="blog_h1"><span class="graybg">概念</span></div>
<div class="blog_h2"><span class="graybg">术语</span></div>
<div class="blog_h3"><span class="graybg">Label</span></div>
<p>标签是定位大的资源集合的一种通用、灵活的方法。每当需要定位、选择、描述某些实体时，Cilium使用标签：</p>
<ol>
<li>Endpoint：从容器运行时、编排系统或者其它资源得到标签</li>
<li>Network Policy：根据标签来选择可以相互通信的一组Endpoint，网络策略自身也基于标签来识别</li>
</ol>
<p>标签就是键值对，值部分可以省略。键具有唯一性，一个实体上不会有两个相同键的标签。键通常仅仅包含字符<code class="docutils literal notranslate"><span class="pre">[a-z0-9-.]</span></code>。</p>
<p>标签从源提取，为了防止潜在的键冲突，Cilium为所有导入的键添加前缀。例如</p>
<ol>
<li><pre class="crayon-plain-tag">k8s:role=frontend</pre>：具有role=frontend的K8S Pod，对应的Cilium端点具有此标签</li>
<li><pre class="crayon-plain-tag">container:user=alex</pre>：通过docker run -l user=alex运行的容器，对应的Cilium端点具有此标签</li>
</ol>
<p>不同前缀含义如下：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">container</pre>：从本地容器运行时得到的标签<br /><pre class="crayon-plain-tag">k8s</pre>：从Kubernetes得到的标签<br /><pre class="crayon-plain-tag">mesos</pre>：从Mesos得到的标签<br /><pre class="crayon-plain-tag">reserved</pre>：专用于特殊的保留标签<br /><pre class="crayon-plain-tag">unspec</pre>：未指定来源的标签</p>
<p>当通过标签来匹配资源时，使用前缀可以限定资源来源。如果不指定前缀，默认为<pre class="crayon-plain-tag">any:</pre>，标识匹配任何来源的资源。</p>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>通过为容器分配IP，Cilium让它在网络上可见。多个应容器可能具有相同IP，典型的例子是Pod中的容器。任何享有同一IP的容器，在Cilium的术语里面，叫做端点。</p>
<p>Cilium的默认行为是，同时分配IPv4/IPv6地址给每个端点，你可以使用--enable-ipv4=false这样的选项来禁用某个IP版本。</p>
<p>在内部，Cilium为每个端点，在节点范围内，分配为唯一性的ID。</p>
<p>端点会从关联的容器自动提取端点元数据（Endpoint Metadata）。这些元数据用来安全、策略、负载均衡、路由上识别端点。端点元数据可能来自K8S的Pod标签、Mesos的标签、Docker的容器标签。</p>
<div class="blog_h3"><span class="graybg">Identity</span></div>
<p>任何端点都被分配身份标识（Identity），<span style="background-color: #c0c0c0;">身份标识通过端点的标签确定</span>，并且具有集群范围内的标识符（数字ID）。<span style="background-color: #c0c0c0;">端点被分配的身份标识，和它的安全相关标签匹配</span>，也就是说，具有相同安全相关标签的所有端点，共享同一身份标识。</p>
<p>仅仅安全相关标签用于确定端点的身份标识。<span style="background-color: #c0c0c0;">安全相关标签具有特定前缀</span>，默认情况下安全相关标签以<pre class="crayon-plain-tag">id.</pre>开头。启动cilium-agent时可以<span style="background-color: #c0c0c0;">指定自定义的前缀</span>。</p>
<p>特殊身份标识用于那些不被Cilium管理的端点，这些特殊标识以<pre class="crayon-plain-tag">reserved:</pre>作为前缀：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 60px; text-align: center;">数字ID</td>
<td style="width: 200px; text-align: center;">身份标识</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>reserved:unknown</td>
<td>无法提取身份标识的任何端点</td>
</tr>
<tr>
<td>1</td>
<td>reserved:host</td>
<td>localhost，任何来自/发往本机IP的流量，都牵涉到该端点</td>
</tr>
<tr>
<td>2</td>
<td>reserved:world</td>
<td>任何集群外的端点</td>
</tr>
<tr>
<td>3</td>
<td>reserved:unmanaged</td>
<td>不被Cilium管理的网络端点，例如在Cilium安装前就存在的Pod</td>
</tr>
<tr>
<td>4</td>
<td>reserved:health</td>
<td>由cilium-agent发起健康检查流量而产生的端点</td>
</tr>
<tr>
<td>5</td>
<td>reserved:init</td>
<td>身份标识尚未提取的端点</td>
</tr>
<tr>
<td>6</td>
<td>reserved:remote-node</td>
<td>集群中所有其它节点的集合</td>
</tr>
</tbody>
</table>
<p>Cilium能够识别一些知名标签，包括k8s-app=kube-dns，并自动分配安全标识。这个特性的目的是，让Cilium顺利的在启用Policy的情况下自举并获得网络连接性。</p>
<p>Cilium利用分布式的KV存储，为身份标识产生数字ID。cilium-agent为会使用身份标识去查询，如果KV存储已经没有对应的数字ID，就会新创建一个。</p>
<div class="blog_h3"><span class="graybg">Node</span></div>
<p>集群的单个成员，每个节点都必须运行cilium-agent。</p>
<div class="blog_h2"><span class="graybg">网络安全</span></div>
<p>Cilium提供多个层次的安全特性，这些特性可以单独或者联合使用。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>容器编排系统中倾向于产生大量的Pod，这些Pod具有独立IP。传统的基于IP的网络策略，在容器场景下需要大量的、频繁变动的规则。</p>
<p>Cilium则完全将网络地址和安全策略分开。作为代替，它总是基于Pod的身份标识（通过它的标签提取）应用安全策略。它能够允许任何具有role=frontend标签的Pod访问role=backend的Pod，不管Pod的数量多少。</p>
<div class="blog_h3"><span class="graybg">安全策略</span></div>
<p>如果运行从A到B发起通信，则自动意味着允许B到A的报文传输，但是不意味着B能够发起到A的通信。</p>
<p>安全策略可以在ingress/egress端应用。</p>
<p>如果不提供任何策略，默认行为是允许任何通信。一旦提供一个安全策略规则，则所有不在白名单中的流量都被丢弃。</p>
<div class="blog_h3"><span class="graybg">代理注入</span></div>
<p>Cilium能够透明的为任何网络连接注入L4代理，这是L7网络策略的基础。目前支持的代理实现是<a href="https://docs.cilium.io/en/v1.10/concepts/security/proxy/envoy/">Envoy</a>。</p>
<p>你可以用Go语言编写少量的、用于解析新协议的代码。这种Go代码能够完全利用Cilium提供的高性能的转发到/自Envoy代理的能力、丰富的L7感知策略定义语言、访问日志、基于kTLS的加密流量可观察性。总而言之，作为开发者你只需要使用Go语言编写协议解析代码，其它的事情Cilium+Envoy+eBPF会做好。</p>
<div class="blog_h2"><span class="graybg">网络数据路径概要</span></div>
<div class="blog_h3"><span class="graybg">L1-2</span></div>
<ol>
<li>当网卡接收到封包后，它可以通过PCI桥，将其存放到内存（ring buffer）中</li>
<li>内核中一般化的轮询机制NAPI Poll（epoll、驱动程序都会使用该机制），会拉取到ring buffer中的数据，开始处理</li>
</ol>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png"><img class="aligncenter wp-image-37985" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png" alt="datapath-l12" width="788" height="444" /></a></p>
<div class="blog_h3"><span class="graybg">L2</span></div>
<p>几乎所有驱动程序都会实现的<pre class="crayon-plain-tag">drvr_poll</pre>，它会调用第一个BPF程序，即XDP。</p>
<p>如果此程序返回<pre class="crayon-plain-tag">pass</pre>，内核会：</p>
<ol>
<li>调用clean_rx，在此Linux分配skb</li>
<li>如果启用GRO（Generic receive offload），则调用gro_rx，在此封包会被聚合，以一点延迟来换取吞吐量的提升。如果tcpdump时发现不可理解的巨大封包，可能是因为启用了GRO，你看到的是内核给的fake封包</li>
<li>调用receive_skb，开始L2接收处理</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png"><img class="wp-image-37983 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png" alt="datapath-l2" width="952" height="430" /></a></p>
<div class="blog_h3"><span class="graybg">L2-3</span></div>
<p>当调用receive_skb后：</p>
<ol>
<li>如果驱动没有实现XDP支持，则在此调用XDP BPF程序，这里的效率比较低</li>
<li>轮询所有的 socket tap，将包放到正确的（如果存在） tap 设备的缓冲区</li>
<li>调用tc BPF程序。这是Cilium最依赖的挂钩点，实现了修改封包（例如打标记）、重新路由、丢弃封包等操作。这里的BPF程序可能会影响qdisc统计信息，从而影响流量塑形。如果tc BPF程序返回OK，则进入netfilter</li>
<li>netfilter 也会对入向的包进行处理，它是<span style="background-color: #c0c0c0;">网络栈的下半部分</span>，iptables规则越多，对网络栈下半部分造成的瓶颈也就越大</li>
<li>取决于L3协议的类型（几乎都是IP），调用相应L3接收函数并进入网络栈第三层</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png"><img class="wp-image-37981 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png" alt="datapath-l23" width="967" height="435" /></a></p>
<div class="blog_h3"><span class="graybg">L3-4</span></div>
<p>当调用ip_rcv后：</p>
<ol>
<li>首先是netfilter钩子pre_routing，这里会从L4视角处理封包，会执行netfilter中的任何四层规则</li>
<li>netfilter处理完毕后，回调ip_rcv_finish</li>
<li>ip_rcv_finish会立即调用ip_routing对封包进行路由判断：是否位于lookback上，是否能够路由出去。如果Cilium没有使用隧道模式，则会使用到这里的路由功能</li>
<li>如果路由目的地是本机，则会调用ip_local_deliver。进而调用xfrm4_policy</li>
<li>xfrm4_policy负责完成包的封装、解封装、加解密。IPSec就是在此完成</li>
<li>根据L4协议的不同，调用相应的L4接收函数</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png"><img class="wp-image-37979 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png" alt="datapath-l34" width="964" height="353" /></a></p>
<div class="blog_h3"><span class="graybg">L4</span></div>
<p>这里以UDP为例，L4入口函数为udp_rcv：</p>
<ol>
<li>该函数会对封包的合法性进行验证，检查UDP的checksum</li>
<li>封包再次送到xfrm4_policy进行处理。这是因为某些transform policy能够指定L4协议，而此时L4协议才明确</li>
<li>根据端口，查找对应的套接字，然后将skb存放到一个链表s.rcv_q中</li>
<li>最后，调用sk_data_ready，标记套接字有数据待收取</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png"><img class="wp-image-37977 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png" alt="datapath-l4" width="788" height="428" /></a></p>
<div class="blog_h3"><span class="graybg">L4-userspace</span></div>
<ol>
<li>上节提到了，套接字（的等待队列）会被标记为有数据待收取。用户空间程序，通过epoll在等待队列上监听，而因获得通知</li>
<li>用户空间调用udp_recv_msg函数，后者会调用cgroup BPF程序。这种程序用来实现透明的客户端egressing负载均衡</li>
<li>最后是sock_ops BPF程序。用于socket level的细粒度流量塑形。对于某些功能来说这很重要，例如客户端限速</li>
</ol>
<p> <a href="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png"><img class="size-full wp-image-37989 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png" alt="l4-us" width="664" height="654" /></a></p>
<div class="blog_h2"><span class="graybg">BPF挂钩点和对象</span></div>
<p>Linux内核在网络栈中支持一系列的BPF挂钩点，用于挂接BPF程序。Cilium利用这些挂钩点来实现高层次的网络功能。</p>
<div class="blog_h3"><span class="graybg">挂钩点</span></div>
<p>Cilium用到的钩子包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">钩子</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>XDP</td>
<td>
<p>网络路径上最早的、可以软件介入的点，在驱动接收到封包之后，具有最好的封包处理性能</p>
<p>能够快速过滤恶意/非预期的流量，例如DDoS</p>
</td>
</tr>
<tr>
<td>tc ingress/egress</td>
<td>
<p>在封包已经开始最初的处理之后的挂钩点，此时内核L3处理尚未开始，但是已经能够访问大部分的封包元数据</p>
<p>适合进行本节点相关的处理，例如应用L3/L4端点策略，重定向流量到特定端点</p>
</td>
</tr>
<tr>
<td>socket operations</td>
<td>socket operation hook挂钩到特定的cgroup，并且当TCP事件发生时执行。Cilium挂钩到根cgroup，依此实现TCP状态转换的监控，特别是ESTABLISHED状态转换。当一个TCP套接字进入ESTABLISHED状态，并且它具有一个节点本地的对端（可能是一个本地的proxy），则自动执行socket send/recv钩子来进行加速</td>
</tr>
<tr>
<td>socket send/recv</td>
<td>
<p>每当TCP套接字执行send操作时触发，钩子可以探查消息，然后或者丢弃、或者将消息发送到TCP层，或者重定向给另外一个套接字</p>
<p>Cilium使用这种钩子来加速数据路径的重定向</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络对象</span></div>
<p>利用上面这些挂钩点，以及虚拟接口（cilium_host, cilium_net）、一个可选的Overlay接口（cilium_vxlan）、内核的crypto支持、以及用户空间代理Envoy，Cilium创建以下类型的网络对象：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">网络对象</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Prefilter</td>
<td>
<p>这类对象运行XDP程序，提供一系列的预过滤规则，获得最大性能的封包过滤</p>
<p>通过Cillium Agent提供的CIDR map，被用<span style="background-color: #c0c0c0;">于快速查找，判定一个封包是否应该被丢弃</span>。例如，假设目的地址不是有效的端点，则应该快速丢弃</p>
</td>
</tr>
<tr>
<td>Endpoint Policy</td>
<td>
<p>这类对象实现Cilium端点策略，它使用一个Map来查询当前封包关联的身份标识，当端点数量很大时，性能不会变差</p>
<p>根据策略，在这一层可能<span style="background-color: #c0c0c0;">丢弃封包、转发给本地端点、转发给Service对象、转发给L7策略对象</span></p>
<p>在Cilium中，这是<span style="background-color: #c0c0c0;">映射封包到身份标识、以及<span style="background-color: #c0c0c0;">应用</span>L3/L4策略</span>的主要对象</p>
</td>
</tr>
<tr>
<td>Service</td>
<td>
<p>这类对象根据每个封包的目的地址来进行Map查找，寻找对应的Service，如果找到了，则封包被转发给Service的某个L3/L4端点</p>
<p>可以和Endpoint Policy对象集成；也可以实现独立的LB</p>
</td>
</tr>
<tr>
<td>L3 Encryption</td>
<td>
<p>在ingress端，L3 Encryption对象标记封包为待解密，随后封包被传递给内核的xfrm（transform）层进行解密，随后解密后的封包传回，并交给网络栈中的其它对象进行后续处理</p>
<p>在egress端，首先根据目的地址进行Map查找，判断是否需要加密，如果是，目标节点上哪些key可用。同时在两端可用的、最近的key被用来加密。封包随后被标记为待解密，传递给内核的xfrm层。加密后的封包，传递给下一层处理，可能是传递给Linux网络栈进行路由，使用overlay的情况下可能直接发起一个尾调用</p>
</td>
</tr>
<tr>
<td>Socket Layer Enforcement</td>
<td>
<p>使用两类钩子：socket operations、socket send/recv，来<span style="background-color: #c0c0c0;">监、控所有Cilium管理的端点（包括L7代理）的TCP连接</span></p>
<p><span style="background-color: #c0c0c0;">socket operations钩子</span>否则<span style="background-color: #c0c0c0;">识别候选的、可加速的套接字</span>。这些<span style="background-color: #c0c0c0;">可加速套接字包括所有本地端点之间的连接、任何发往Cilium代理的连接</span>。可加速套接字的所有封包都会被socket send/recv钩子处理 —— 通过BPF sockmap进行快速重定向</p>
</td>
</tr>
<tr>
<td>L7 Policy</td>
<td>该对象将代理流量重定向给Cilium的用户空间代理，也就是Envoy。Envoy随后要么转发流量，要么根据配置的L7策略生成适当的reject消息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">BPF Maps</span></div>
<p>Cilium使用了大量的BPF Maps，这些Map创建的时候都进行了容量限制。超过限制后，无法插入数据，因此限制了数据路径的扩容能力。下表显示了默认容量：</p>
<table class="docutils full-width" border="1">
<tbody valign="top">
<tr class="row-even">
<td style="width: 150px; text-align: center;"><strong>Map类别</strong></td>
<td style="text-align: center;"><strong>作用域</strong></td>
<td style="width: 80px; text-align: center;"><strong>默认限制</strong></td>
<td style="text-align: center;"><strong>扩容影响</strong></td>
</tr>
<tr>
<td>Connection Tracking</td>
<td>node<br />endpoint</td>
<td>1M TCP<br />256k UDP</td>
<td>Max 1M concurrent TCP connections, max 256k expected UDP answers</td>
</tr>
<tr class="row-odd">
<td>NAT</td>
<td>node</td>
<td>512k</td>
<td>Max 512k NAT entries</td>
</tr>
<tr class="row-even">
<td>Neighbor Table</td>
<td>node</td>
<td>512k</td>
<td>Max 512k neighbor entries</td>
</tr>
<tr class="row-odd">
<td>Endpoints</td>
<td>node</td>
<td>64k</td>
<td>Max 64k local endpoints + host IPs per node</td>
</tr>
<tr class="row-even">
<td>IP cache</td>
<td>node</td>
<td>512k</td>
<td>Max 256k endpoints (IPv4+IPv6), max 512k endpoints (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>Load Balancer</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative backends across all services across all clusters</td>
</tr>
<tr class="row-even">
<td>Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k allowed identity + port + protocol pairs for specific endpoint</td>
</tr>
<tr class="row-odd">
<td>Proxy Map</td>
<td>node</td>
<td>512k</td>
<td>Max 512k concurrent redirected TCP connections to proxy</td>
</tr>
<tr class="row-even">
<td>Tunnel</td>
<td>node</td>
<td>64k</td>
<td>Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>IPv4 Fragmentation</td>
<td>node</td>
<td>8k</td>
<td>Max 8k fragmented datagrams in flight simultaneously on the node</td>
</tr>
<tr class="row-even">
<td>Session Affinity</td>
<td>node</td>
<td>64k</td>
<td>Max 64k affinities from different clients</td>
</tr>
<tr class="row-odd">
<td>IP Masq</td>
<td>node</td>
<td>16k</td>
<td>Max 16k IPv4 cidrs used by BPF-based ip-masq-agent</td>
</tr>
<tr class="row-even">
<td>Service Source Ranges</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative LB source ranges across all services</td>
</tr>
<tr class="row-odd">
<td>Egress Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k endpoints across all destination CIDRs across all clusters</td>
</tr>
</tbody>
</table>
<p>部分BPF Map的容量上限可以通过cilium-agent的命令行选项覆盖：</p>
<p style="padding-left: 30px;">--bpf-ct-global-tcp-max<br />--bpf-ct-global-any-max<br />--bpf-nat-global-max<br />--bpf-neigh-global-max<br />--bpf-policy-map-max<br />--bpf-fragments-map-max<br />--bpf-lb-map-max</p>
<p>如果指定了--bpf-ct-global-tcp-max或/和--bpf-ct-global-any-max，则NAT表（<pre class="crayon-plain-tag">--bpf-nat-global-max</pre>）的大小不能超过前面两个表合计大小的2/3。</p>
<p>使用<pre class="crayon-plain-tag">--bpf-map-dynamic-size-ratio=0.0025</pre>，则cilium-agent在启动时能够动态根据总计内存来调整Map的容量。该选项取值0.0025则0.25%的系统内存用于BPF Map。 该标记会影响消耗大部分内存的Map，包括：</p>
<p style="padding-left: 30px;">cilium_ct_{4,6}_global<br />cilium_ct_{4,6}_any<br />cilium_nodeport_neigh{4,6}<br />cilium_snat_v{4,6}_external<br />cilium_lb{4,6}_reverse_sk</p>
<p>Cilium使用自己的，基于BPF Map实现的连接跟踪表，--bpf-map-dynamic-size-ratio影响容量，但是不会小于131072。</p>
<div class="blog_h2"><span class="graybg">封包生命周期</span></div>
<div class="blog_h3"><span class="graybg">从端点到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png"><img class="alignnone wp-image-37657 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png" alt="cilium_bpf_endpoint" width="1070" height="1124" /></a></p>
<p>上图包含两个部分：</p>
<ol>
<li>上半部分：本地端点到端点数据流图，显示了Cilium如何配合L7代理进行封包重定向的细节</li>
<li>下半部分：启用了Socket Layer Enforcement后的数据流图。这种情况下，TCP连接的握手阶段，需要遍历Endpoint Policy，直到ESTABLISHED，之后仅仅需要L7 Policy</li>
</ol>
<p>如果启用了L7规则，则流量会被转发给用户空间代理，代理处理完后，转发给目的端点的代理，后者再转发给目的端点的Pod。转发都是由bpf_redir负责，直接修改封包。</p>
<div class="blog_h3"><span class="graybg">端点到Egress</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png"><img class="alignnone wp-image-37661 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png" alt="cilium_bpf_egres" width="1079" height="724" /></a></p>
<p>跨节点的封包流，可能牵涉到overlay，默认情况下overlay接口的名字是cilium_vxlan。</p>
<p>如果需要L3 Encryption，则Endpoint端的tc钩子会将其流量传递给L3 Encryption处理。需要注意tc BPF程序的<span id="crayon-60d3fea497f3d798084325" class="crayon-syntax crayon-syntax-inline  crayon-theme-gmem-github crayon-theme-gmem-github-inline crayon-font-consolas" style="font-size: 15px !important; line-height: 20px !important;"><span class="crayon-pre crayon-code" style="font-size: 15px !important; line-height: 20px !important; -moz-tab-size: 4; -o-tab-size: 4; -webkit-tab-size: 4; tab-size: 4;"><span class="crayon-v">da</span></span></span>模式，能够直接对封包进行修改、转发，而不需要外部的tc action模块。</p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<div class="blog_h3"><span class="graybg">Ingress到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg" alt="" width="1068" height="507" /></a></p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<p>这种封包流可以被Prefilter快速处理，决定是否需要丢弃封包、是否需要进行负载均衡处理。</p>
<div class="blog_h2"><span class="graybg">iptables</span></div>
<p>依赖于实际使用的Linux内核版本，Cilium能够利用eBPF datapath全部或部分特性。如果Linux内核版本较低，某些功能可能基于iptables实现。</p>
<p>下图显示了Cilium和kube-proxy安装的iptables规则以及相互关系：<a href="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg" alt="" width="1078" height="962" /></a></p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<div class="blog_h3"><span class="graybg">内核</span></div>
<p>要求内核版本4.9.17或者更高。</p>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<p>必须启用CNI作为网络插件。</p>
<div class="blog_h2"><span class="graybg">下载命令行工具</span></div>
<pre class="crayon-plain-tag">curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz{,.sha256sum}</pre>
<div class="blog_h2"><span class="graybg">安装到K8S</span></div>
<div class="blog_h3"><span class="graybg">通过命令行工具</span></div>
<p>参考下面的命令进行安装：</p>
<pre class="crayon-plain-tag"># 安装到当前Kubernetes context
cilium install

cilium install
  --agent-image string              # Cilium Agent镜像
  --operator-image                  # Cilium Operator镜像
  --cluster-id int                  # 多集群模式下唯一ID
  --cluster-name string             # 多集群模式下此集群的名字
  --config strings                  # 添加Cilium配置条目，对应ConfigMap中的一个键值
  --context string                  # 使用的K8S Context
  --datapath-mode                   # 使用的datapath模式
  --disable-check strings           # 禁用指定的校验
  --encryption string               # 所有工作负载流量的加密：disabled（默认） | ipsec | wireguard
  --inherit-ca string               # 从另外一个集群继承/导入CA
  --ipam string                     # IPAM模式
  --kube-proxy-replacement string   # kube-proxy replacement工作模式：disabled（默认） | probe | strict
  -n, --namespace                   # Cilium安装到什么命名空间，默认kube-system
  --native-routing-cidr string      # 直接路由的CIDR，和PodCIDR一致
  --node-encryption                 # 加密所有节点到节点流量
  --restart-unmanaged-pods          # 重启所有没有被Cilium管理的Pod，默认true，保证所有Pod获得Cilium提供的容器网络
  --wait                            # 等待安装完毕，默认true


cilium install  \
  --agent-image=docker.gmem.cc/cilium/cilium:v1.10.1 \
  --operator-image=docker.gmem.cc/cilium/operator-generic:v1.10.1</pre>
<p>如果安装失败，可以通过命令<pre class="crayon-plain-tag">cilium status</pre> 查看整体部署状态，查看日志。</p>
<p>安装完毕后，使用下面的命令来检查状态、进行连通性测试：</p>
<pre class="crayon-plain-tag">cilium status --wait

cilium connectivity test</pre>
<div class="blog_h3"><span class="graybg">通过Helm</span></div>
<pre class="crayon-plain-tag">helm repo add cilium https://helm.cilium.io/



helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system

  # 是否启用调试日志
  --set debug.enabled=true

  # 集群ID，整数，范围1-255，网格中所有集群都必须有唯一ID
  --set cluster.id=1
  # 集群的名字，仅对于集群网格需要
  --set cluster.name=gmem

  # 容器网络路径选项，veth或者ipvlan
  --set datapathMode=veth

  # 禁用隧道，使用直接路由
  --set tunnel=disabled
  # 如果所有节点位于L2网络中，下面的选项用于自动在工作节点之间同步PodCIDR的路由
  # 如果不指定该选项，节点之间的路由不会同步，会出现节点A无法访问B上Pod IP的问题
  --set autoDirectNodeRoutes=true
  # 指定可以直接进行路由（访问时不需要进行IP遮掩）的CIDR，对应K8S配置的cluster-cidr（PodCIDR）
  # 禁用隧道后必须手工设置，否则报错
  --set nativeRoutingCIDR=172.27.0.0/16

  # 当ConfigMap改变时，滚动更新cilium-agent
  --set rollOutCiliumPods=false
  # 为cilium-config这个ConfigMap配置额外的键值
  --set extraConfig={}

  # 传递cilium-agent的额外命令行选项
  --set extraArgs=[]

  # 传递cilium-agent的额外环境变量
  --set extraEnv={}
  
  # 是否在Cilium中启用内置BGP支持
  --set bgp.enabled=false
  # 是否分配、宣告LoadBalancer服务的IP地址
  --set bgp.announce.loadbalancerIP=false

  # 强制cilium-agent在init容器中等待eBPF文件系统已挂载
  --set bpf.waitForMount=false
  # 是否预先分配eBPF Map的键值，会增加内存消耗并降低延迟
  --set bpf.preallocateMaps=false
  # TCP连接跟踪表的最大条目数量
  --set bpf.ctTcpMax=524288
  # 非TCP连接跟踪表的最大条目数量
  --set bpf.ctAnyMax=262144
  # 负载均衡表中最大服务条目
  --set bpf.lbMapMax=65536
  # NAT表最大条目数量
  --set bpf.natMax=524288
  # neighbor表最大条目数量
  --set bpf.neighMax=524288
  # 端点策略映射最大条目数量
  --set bpf.policyMapMax=16384
  # 配置所有BPF Map的自动sizing，根据可用内存
  --set bpf.mapDynamicSizeRatio=0.0025
  # 监控通知（monitor notifications）的聚合级别 none, low, medium, maximum
  --set bpf.monitorAggregation=medium
  # 活动连接的监控通知的间隔
  --set bpf.monitorInterval=5s
  # 哪些TCP flag第一次出现在某个连接中，会触发通知
  --set bpf.monitorFlags=all
  # 允许从外部访问集群的ClusterIP
  --set bpf.lbExternalClusterIP=false
  # 即用基于eBPF的IP遮掩支持
  --set bpf.masquerade=true
  # 直接路由模式，是通过宿主机网络栈进行（true），还是（如果内核支持）使用更直接的、高效的eBPF（false）
  # 后者的副作用是，跳过宿主机的netfilter
  --set bpf.hostRouting=true
  # 是否启用基于eBPF的TPROXY，以便在实现L7策略时减少对iptables的依赖
  --set bpf.tproxy=true
  # NodePort反向NAT处理时，是否跳过FIB查找
  --set bpf.lbBypassFIBLookup=true

  # 每当cilium-agnet重启时，清空BPF状态
  --set cleanBpfState=false
  # 每当cilium-agnet重启时，清空所有状态
  --set cleanState=false

  # 和其它CNI插件组成链，可选值none generic-veth portmap
  --set cni.chainingMode=none
  # 让Cilium管理/etc/cni/net.d目录，将其它CNI插件的配置改为*.cilium_bak
  --set cni.exclusive=true
  # 如果你希望通过外部机制将CNI配置写入，则设置为true
  --set cni.customConf=false
  --set cni.confPath: /etc/cni/net.d
  --set cni.binPath: /opt/cni/bin

  # 配置容器运行时集成  containerd crio docker none auto
  --set containerRuntime.integration=none

  # 支持对自定义BPF程序的尾调用
  --set customCalls.enabled=false

  # IPAM模式
  --set ipam.mode=cluster-pool
  # IPv4 CIDR
  --set ipam.operator.clusterPoolIPv4PodCIDR=0.0.0.0/8
  --set ipam.operator.clusterPoolIPv4MaskSize=24
  # IPv6 CIDR
  --set ipam.operator.clusterPoolIPv6PodCIDR=fd00::/104
  --set ipam.operator.clusterPoolIPv6MaskSize=120

  # 配置基于eBPF的ip-masq-agent
  --set ipMasqAgent.enabled=false

  # IP协议版本支持
  --set ipv4.enabled=true
  --set ipv6.enabled=false

  # 如果启用，这重定向、SNAT离开集群的流量
  --set egressGateway.enabled=false

  # 启用监控sidecar
  --set monitor.enabled=false

  # 配置Service负载均衡
  # 是否启用独立的、不连接到kube-apiserver的L4负载均衡器
  --set loadBalancer.standalone=false
  # 负载均衡算法 random或者maglev
  --set loadBalancer.algorithm=random
  # 对于远程后端，LB操作模式 snat, dsr, hybrid
  --set loadBalancer.mode=snat
  # 是否基于XDP来加速服务处理
  --set loadBalancer.acceleration=disabled
  # 是否利用IP选项/IPIP封装，来将Service的IP/端口信息传递到远程后端
  --set loadBalancer.dsrDispatch=opt

  # 是否对从端点离开节点的流量进行IP遮掩
  --set enableIPv4Masquerade=true
  --set enableIPv6Masquerade=true

  # 支持L7网络策略
  --set l7Proxy=true

  # 镜像
  --set image.repository=docker.gmem.cc/cilium/cilium
  --set image.useDigest=false
  --set operator.image.repository=docker.gmem.cc/cilium/operator
  --set operator.image.useDigest=false


# 重启所有被有被cilium管理的Pod
kubectl get pods --all-namespaces -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,HOSTNETWORK:.spec.hostNetwork --no-headers=true | grep '&lt;none&gt;' | awk '{print "-n "$1" "$2}' | xargs -L 1 -r kubectl delete pod</pre>
<div class="blog_h2"><span class="graybg">高级安装</span></div>
<div class="blog_h3"><span class="graybg">使用外部Etcd</span></div>
<p>使用独立的外部Etcd（而非K8S自带的）可以提供更好的性能适用于更大的部署环境。</p>
<p>选用外部Etcd的时机可能是：</p>
<ol>
<li>超过250节点，5000个Pod。或者，在通过Kubernetes evnets进行状态传播时，出现了很高的overhead</li>
<li>你不希望利用CRD来存储Cilium状态</li>
</ol>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set "etcd.endpoints[0]=http://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=http://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=http://etcd-endpoint3:2379" \
  # 不使用CRD来存储状态
  --set identityAllocationMode=kvstore


# 使用SSL
kubectl create secret generic -n kube-system cilium-etcd-secrets \
    --from-file=etcd-client-ca.crt=ca.crt \
    --from-file=etcd-client.key=client.key \
    --from-file=etcd-client.crt=client.crt

helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set etcd.ssl=true \
  --set "etcd.endpoints[0]=https://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=https://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=https://etcd-endpoint3:2379"</pre>
<div class="blog_h3"><span class="graybg">CNI Chaining</span></div>
<p>CNI Chaining允许联用Cilium和其它CNI插件。联用时某些Cilium高级特性不可用，包括：</p>
<ol>
<li><a href="https://github.com/cilium/cilium/issues/12454">L7策略</a></li>
<li><a href="https://github.com/cilium/cilium/issues/15596">IPsec透明加密</a></li>
</ol>
<p>你需要创建一个CNI配置，使用plugin list：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cni-configuration
  namespace: kube-system
data:
  cni-config: |-
    {
      "name": "generic-veth",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "calico",
          "log_level": "info",
          "datastore_type": "kubernetes",
          "mtu": 1440,
          "ipam": {
              "type": "calico-ipam"
          },
          "policy": {
              "type": "k8s"
          },
          "kubernetes": {
              "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
          }
        },
        {
          "type": "portmap",
          "snat": true,
          "capabilities": {"portMappings": true}
        },
        {
          "type": "cilium-cni"
        }
      ]
    }</pre><br />
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace=kube-system \
  --set cni.chainingMode=generic-veth \
  --set cni.customConf=true \
  --set cni.configMap=cni-configuration \
  --set tunnel=disabled \
  --set enableIPv4Masquerade=false \
  --set enableIdentityMark=false</pre>
<div class="blog_h1"><span class="graybg">K8S集成</span></div>
<p>Cilium能够为K8S带来：</p>
<ol>
<li>基于CNI的容器网络支持</li>
<li>基于身份标识实现的NetworkPolicy，用于隔离L3/L4连接性</li>
<li>CRD形式的NetworkPolicy扩展，支持：
<ol>
<li>L7策略，目前支持HTTP、Kafka等协议</li>
<li>Egress策略支持CIDR</li>
</ol>
</li>
<li>ClusterIP实现，提供分布式的负载均衡。完全兼容kube-proxy模型</li>
</ol>
<p>支持的K8S版本为1.16+，内核版本4.9+。</p>
<p>K8S能够自动分配per-node的CIDR，通过kube-controller-manager的命令行选项<pre class="crayon-plain-tag">--allocate-node-cidrs</pre>启用此特性。Cilium会自动使用分配的CIDR。</p>
<div class="blog_h2"><span class="graybg">ConfigMap</span></div>
<p>Cilium使用名为cilium-config的ConfigMap：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  # The kvstore configuration is used to enable use of a kvstore for state
  # storage.
  kvstore: etcd
  kvstore-opt: '{"etcd.config": "/var/lib/etcd-config/etcd.config"}'

  # Etcd配置
  etcd-config: |-
    ---
    endpoints:
      - https://node-1:31079
      - https://node-2:31079
    trusted-ca-file: '/var/lib/etcd-secrets/etcd-client-ca.crt'
    key-file: '/var/lib/etcd-secrets/etcd-client.key'
    cert-file: '/var/lib/etcd-secrets/etcd-client.crt'

  # 是否让Cilium运行在调试模式下
  debug: "false"
  # 是否启用IPv4地址支持
  enable-ipv4: "true"
  # 是否启用IPv6地址支持
  enable-ipv6: "true"
  # 在启动cilium-agent时，从文件系统中移除所有eBPF状态。这会导致进行中的连接中断、负载均衡决策丢失
  # 所有的eBPF状态将从源（例如K8S或kvstore）重新构造
  # 该选项用于缓和严重的eBPF maps有关的问题，并且在打开、重启cilium-agent后，立即关闭
  clean-cilium-bpf-state: "false"
  # 清除所有Cilium状态，包括钉在文件系统中的eBPF状态、CNI配置文件、端点状态
  # 当前被Cilium管理的Pod可能继续正常工作，但是可能在没有警告的情况下不再工作
  clean-cilium-state: "false"
  # 该选项启用在cilium monitor中的追踪事件的聚合
  monitor-aggregation: none, low, medium, maximum
  # 启用Map条目的预分配，这样可以降低per-packet的延迟，代价是提前的为Map中条目分配内存
  # 如果此选项改变，则cilium-agnet下次重启会导致具有活动连接的端点临时性中断
  preallocate-bpf-maps: "true"</pre>
<p>修改此ConfigMap后，你需要重新启动cilium-agent才能生效。需要注意，K8S的ConfigMap变更可能需要2分钟才能传播到所有节点。</p>
<div class="blog_h2"><span class="graybg">NetworkPolicy</span></div>
<p>K8S标准的NetworkPolicy，可以用来指定L3/L4 ingress策略，以及受限的egress策略。详细参考<a href="/kubernetes-study-note#network-policy">Kubernetes学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">CiliumNetworkPolicy</span></div>
<p>功能类似于标准的NetworkPolicy，但是提供丰富的多的特性，能够配置L3/L4/L7策略。</p>
<div class="blog_h2"><span class="graybg">L3策略</span></div>
<p>L3策略用于提供端点之间基本的连接性。支持通过以下方式来指定：</p>
<ol>
<li>基于标签：当通信双方端点都被Cilium管理（因而被提取了标签）时，使用此方式。此方式的优点是IP地址之类的易变信息不会编码在策略中</li>
<li>基于服务：自动提取、维护编排系统的服务的后端IP列表（对于K8S就是Service的Endpoint的IP地址列表）。即使端点不会Cilium管理，这种方式也可以避免硬编码IP到策略中</li>
<li>基于实体：实体用于描述那些被归类的、不需要知道其IP地址的端点。例如具有reserved:身份标识的那些端点</li>
<li>基于IP/CIDR：当外部服务不是一个端点时使用</li>
<li>基于DNS：先进行DNS查找，然后转换为IP</li>
</ol>
<div class="blog_h3"><span class="graybg">基于标签</span></div>
<pre class="crayon-plain-tag">## ingress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend

# 允许所有端点访问victim
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-to-victim"
spec:
  endpointSelector:
    matchLabels:
      role: victim
  ingress:
  - fromEndpoints:
    - {}



## egress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-egress-rule"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - matchLabels:
        role: backend

# 允许frontend访问所有端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-from-frontend"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - {}

# 禁止restricted访问任何端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "deny-all-egress"
spec:
  endpointSelector:
    matchLabels:
      role: restricted
  egress:
  - {}</pre>
<p>在设计策略时，通常遵循关注点分离原则。CiliumNetworkPolicy支持设置任何连接性发生所需要的“前提条件”。字段fromRequires用于为任何fromEndpoints指定前提条件。类似的还有toRequires。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "requires-rule"
specs:
  # 对于生产环境下的Pod，允许访问它的端点，必须也在生产环境中（前提条件）
  - description: "For endpoints with env=prod, only allow if source also has label env=prod"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromRequires:
      - matchLabels:
          env: prod</pre>
<p>上面这个 fromRequires规则本身不会允许任何流量，它必须和fromEndpoints规则进行“与”才能允许特定流量：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
specs:
  # 配合上面的规则，效果就是，生产环境下的前端组件，可以访问生产环境下的端点
  - description: "For endpoints with env=prod, allow if source also has label role=frontend"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromEndpoints:
      - matchLabels:
          role: frontend</pre>
<div class="blog_h3"><span class="graybg">基于服务</span></div>
<p>运行在集群中的服务，可以在Egress规则的白名单中列出：</p>
<pre class="crayon-plain-tag"># 使用服务名
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-rule"
spec:
  # 策略控制的目标端点，总是通过标签选择
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  # 允许访问特定服务
  - toServices:
    - k8sService:
        serviceName: myservice
        namespace: default


# 使用服务选择器
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-labels-rule"
spec:
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  - toServices:
    - k8sServiceSelector:
        selector:
          matchLabels:
            head: none</pre>
<p>fromEntities用于描述哪些实体可以访问选择的端点； toEntities则用于描述选择的端点能够访问哪些实体。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>支持的实体参考前文描述的具有reserved:前缀的特殊身份标识。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "dev-to-host"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许开发环境端点访问其本机上的实体
  egress:
    - toEntities:
      - host


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-dev-from-nodes-in-cluster"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许本机、集群远程机器访问开发环境端点
  # 注意，K8S默认允许从宿主机访问任何本地端点，cilium-agnet选项 --allow-localhost=policy可以禁用这默认行为
  ingress:
    - fromEntities:
      - host
      - remote-node


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "from-world-to-role-public"
spec:
  endpointSelector:
    matchLabels:
      role: public
  # 允许集群外部访问role:public的端点
  ingress:
    - fromEntities:
      - world</pre>
<div class="blog_h3"><span class="graybg">基于CIDR</span></div>
<p>不被Cilium管理的实体，没有标签，不属于端点。这些实体通常是运行在特定子网中的外部服务、VM、裸金属机器。这类实体在策略中，可以用CIDR规则来描述。</p>
<p>CIDR规则不能用在通信两端都是以下之一的场景：</p>
<ol>
<li>被Cilium管理的端点</li>
<li>使用属于集群节点的IP的实体，包括使用host networking的Pod</li>
</ol>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许访问外部CIDR
  egress:
  - toCIDR:
    - 20.1.1.1/32
  - toCIDRSet:
    - cidr: 10.0.0.0/8
      except:
      - 10.96.0.0/12</pre>
<div class="blog_h3"><span class="graybg">基于DNS</span></div>
<p>使用DNS名称来指定不被Cilium管理的实体也是支持的，由matchName/matchPattern规则给出的DNS信息，会被cilium-agent收集为IP地址。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-fqdn"
spec:
  endpointSelector:
    matchLabels:
      app: test-app
  egress:
    # 通过DNS Proxy拦截DNS请求，这样，当应用程序发起对my-remote-service.com的DNS查询时
    # Cilium能够学习到域名对应的IP地址
    - toEndpoints:
      - matchLabels:
          "k8s:io.kubernetes.pod.namespace": kube-system
          "k8s:k8s-app": kube-dns
      toPorts:
        - ports:
           - port: "53"
             protocol: ANY
          # DNS代理允许查询的域名
          rules:
            dns:
              - matchPattern: "*"
    - toFQDNs:
        # 将精确匹配此名称的IP地址插入到网路策略中
        - matchName: "my-remote-service.com"
        # 将匹配此模式的所有名称对应的IP地址插入到网络策略中
        # * 匹配所有域名，导致所有缓存的DNS IPs插入到规则
        # *.gmem.cc 匹配子域名，不匹配gmem.cc
        - matchPattern: "*"</pre>
<p>很多情况下，应用程序打开的长连接，生存期大于DNS的TTL，如果没有发生后续、针对此长连接域名的查询，则DNS缓存会过期。这种情况下，已经建立的长连接会继续运行。DNS缓存的TTL可以通过<pre class="crayon-plain-tag">--tofqdns-min-ttl</pre>配置。</p>
<p>相反的，对于短连接场景，可能由于反复的DNS查询（服务backed by大量主机）导致FQDN映射的IP地址很快增加，到达默认 <pre class="crayon-plain-tag">--tofqdns-max-ip-per-hostname=50</pre>的限制，并导致最旧的IP被剔除。这种情况下，已经建立的短连接也不会受到影响，直到它断开。</p>
<div class="blog_h3"><span class="graybg">关于DNS Proxy</span></div>
<p>DNS代理能够拦截DNS请求，记录IP和域名的对应关系。为了实现拦截，必须配置一个管理DNS请求的策略规则。</p>
<p><span style="background-color: #c0c0c0;">某些常用的容器镜像（例如alpine/musl）将DNS的Refused应答（当DNS代理拒绝某个查询时）看作更一般性的错误</span>，并且停止遍历/etc/resolv.conf的search list。例如，当Pod访问gmem.cc时，它会首先查询gmem.cc.svc.cluster.local.而得到DNS Proxy的Refused应答，停止遍历，不再查询gmem.cc.并且最终导致Pod认为DNS查询失败。</p>
<p>要解决此问题，可以配置<pre class="crayon-plain-tag">--tofqdns-dns-reject-response-code</pre>，默认值是refused，可以改为nameError，这样DNS代理会返回NXDomain应答。</p>
<div class="blog_h2"><span class="graybg">L4策略</span></div>
<p>主要是在L3的基础上，进行端口限制。</p>
<div class="blog_h3"><span class="graybg">限制端口</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许myService访问80端口
  egress:
    - toPorts:
      - ports:
        - port: "80"
          protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">标签依赖的端口限制</span></div>
<p>下面的例子，允许针对特定标签（所关联的端点）的端口的访问：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  # 允许frontend服务访问backend服务的80端口
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">CIDR依赖的端口限制</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: crawler
  # 允许爬虫访问192.0.2.0/24的80端口
  egress:
  - toCIDR:
    - 192.0.2.0/24
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">L7策略</span></div>
<p>目前Cilium支持的L7协议很有限，仅仅HTTP和Kafka（beta）。</p>
<div class="blog_h3"><span class="graybg">HTTP</span></div>
<p>策略可以根据URL路径、HTTP方法、主机名、HTTP头来设置。</p>
<pre class="crayon-plain-tag"># 限定URL路径
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "Allow HTTP GET /public from env=prod to app=service"
  endpointSelector:
    matchLabels:
      app: service
  # 允许生产环境访问service的/public
  ingress:
  - fromEndpoints:
    - matchLabels:
        env: prod
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/public"


# 限定URL和请求头
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  ingress:
  - toPorts:
    - ports:
      - port: '80'
        protocol: TCP
      rules:
        http:
        - method: GET
          path: "/path1$"
        - method: PUT
          path: "/path2$"
          headers:
          - 'X-My-Header: true'</pre>
<div class="blog_h3"><span class="graybg">Deny策略</span></div>
<p>用于明确的拒绝特定的流量，<span style="background-color: #c0c0c0;">优先级比Allow策略（CiliumNetworkPolicy/CiliumClusterwideNetworkPolicy/NetworkPolicy）高</span>，上文提及的所有策略都是Allow策略。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "external-lockdown"
spec:
  endpointSelector: {}
  # 明确禁止外部访问
  ingressDeny:
  - fromEntities:
    - "world"
  ingress:
  - fromEntities:
    - "all"</pre>
<div class="blog_h2"><span class="graybg">CiliumClusterwideNetworkPolicy</span></div>
<p>类似于上面的CiliumNetworkPolicy，区别是：</p>
<ol>
<li>不限定到某个命名空间，集群范围的</li>
<li>支持使用节点选择器</li>
</ol>
<div class="blog_h3"><span class="graybg">主机策略</span></div>
<p>使用节点选择器，可以将策略应用到特定的一个/一组节点。主机策略仅仅应用到宿主机的初始命名空间，包括使用hostnetwork的Pod。</p>
<p>要支持主机策略，需要使用Helm值： <pre class="crayon-plain-tag">--set devices='{interface}'</pre>、<pre class="crayon-plain-tag">--set hostFirewall=true</pre>。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "lock-down-ingress-worker-node"
spec:
  # 允许据表标签type=ingress-worker的宿主机的所有指定端口的入站流量
  description: "Allow a minimum set of required ports on ingress of worker nodes"
  nodeSelector:
    matchLabels:
      type: ingress-worker
  ingress:
  - fromEntities:
    - remote-node
    - health
  - toPorts:
    - ports:
      - port: "6443"
        protocol: TCP
      - port: "22"
        protocol: TCP
      - port: "2379"
        protocol: TCP
      - port: "4240"
        protocol: TCP
      - port: "8472"
        protocol: UDP
      - port: "REMOVE_ME_AFTER_DOUBLE_CHECKING_PORTS"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">CiliumEndpoint</span></div>
<p>管理K8S中的Pod的过程中，Cilium会自动创建CiliumEndpoint对象，和对应Pod具有相同的namespace+name。</p>
<p>CiliumEndpoint和<pre class="crayon-plain-tag">cilium endpoint get</pre>命令得到的<pre class="crayon-plain-tag">.status</pre>字段有相同的信息：</p>
<pre class="crayon-plain-tag">kubectl get ciliumendpoints.cilium.io  nginx-0 -o jsonpath="{.status}" | jq
{
  // 通信加密设置
  "encryption": {},
  "external-identifiers": {
    "container-id": "eac9972f57187a7afe7bb3edf97c4e70eff8edff26b6923dda8f398d7e622ec9",
    "k8s-namespace": "default",
    "k8s-pod-name": "nginx-0",
    "pod-name": "default/nginx"
  },
  // 端点ID，每个端点都有唯一的ID
  "id": 2318,
  "identity": {
    // 身份标识
    "id": 34796,
    // 具有相同标签的Pod，共享同一身份标识
    "labels": [
      "k8s:app=nginx",
      "k8s:io.cilium.k8s.policy.cluster=default",
      "k8s:io.cilium.k8s.policy.serviceaccount=default",
      "k8s:io.kubernetes.pod.namespace=default"
    ]
  },
  "networking": {
    "addressing": [
      {
        "ipv4": "172.27.2.23"
      }
    ],
    "node": "10.0.3.1"
  },
  "state": "ready"
}

kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint get 2318</pre>
<p>每个cilium-agent会创建一个名为<pre class="crayon-plain-tag">cilium-health-&lt;node-name&gt;</pre>的CiliumEndpoint，表示inter-agent健康检查端点。</p>
<div class="blog_h1"><span class="graybg">Istio集成</span></div>
<p>Cilium和Istio都使用Envoy作为七层代理。</p>
<p>集成Cilium和Istio，可以为启用了mTLS的Istio流量提供L7网络策略。如果不进行集成，则可以在Istio Sidecar之外应用应用L7策略，且不能识别mTLS流量。</p>
<div class="blog_h2"><span class="graybg">cilium-istioctl</span></div>
<p>Cilium增强的Istio版本，可以通过cilium-istioctl安装，当前版本1.8.2：</p>
<pre class="crayon-plain-tag">curl -L https://github.com/cilium/istio/releases/download/1.8.2/cilium-istioctl-1.8.2-linux-amd64.tar.gz | tar xz</pre>
<p>运行下面的命令安装Istio：</p>
<pre class="crayon-plain-tag"># 使用默认的Istio配置安装
cilium-istioctl install -y</pre>
<p>启用Istio自动的Envoy Sidecar注入：</p>
<pre class="crayon-plain-tag">kubectl label namespace default istio-injection=enabled</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">网络策略</span></div>
<div class="blog_h2"><span class="graybg">准备</span></div>
<p>我们在default命名空间下，创建以下Pod，用于测试Cilium的功能：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-0
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-1
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: alpine
  name: alpine
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/alpine:3.11
    imagePullPolicy: Always
    name: apline

---

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/ubuntu:16.04
    imagePullPolicy: Always
    name: ubuntu</pre>
<p>当Pod就绪后，查看端点状态：</p>
<pre class="crayon-plain-tag"># 在端点所在节点的cilium-agent中执行cilium endpoint list
# kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint list | grep -E 'ubuntu|alpine|nginx'

ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value]) IPv6   IPv4           STATUS   
           ENFORCEMENT        ENFORCEMENT                                                                                                              
888        Disabled           Disabled          5371       k8s:app=alpine                     172.27.2.118   ready   
931        Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.97    ready   
1781       Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.148   ready   
2363       Disabled           Disabled          42034      k8s:app=ubuntu                     172.27.2.162   ready</pre>
<p>可以看到两个Nginx的Pod具有相同的身份标识，这是因为它们的标签一样。由于没有应用任何策略，因此ingress/egress policy为Disabled。</p>
<p>在为两个Nginx端点创建一个服务：</p>
<pre class="crayon-plain-tag">kubectl create service clusterip nginx --tcp=80:80</pre>
<p>确认客户端可以访问服务：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200
kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<div class="blog_h2"><span class="graybg">身份感知和HTTP感知</span></div>
<div class="blog_h3"><span class="graybg">身份感知（L3/L4策略）</span></div>
<p>下面我们增加一个策略，允许app=alpine访问app=nginx：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<p>应用上述策略后，通过cilium endpoint list可以看到，两个Nginx的ingress policy为Enabled。</p>
<p>现在，在alpine中还能够访问nginx：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<p>在ubuntu中不能访问：</p>
<pre class="crayon-plain-tag">kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 000
# 超时
# command terminated with exit code 7</pre>
<p>这说明策略生效，并且是白名单 —— 如果对某个identity应用了（accept）ingress policy，则只有明确声明的fromEndpoints才具有访问权限。</p>
<div class="blog_h3"><span class="graybg">HTTP感知（L7策略）</span></div>
<p>现在alpine能够访问nginx，假设我们向限制它仅仅能访问/welcome这个URL路径，就需要用到Cilium的L7策略：</p>
<pre class="crayon-plain-tag"># kubectl edit cnp nginx-ingress
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      # 在L4策略的基础上，添加以下内容
      rules:
        http:
        - method: "GET"
          # 支持正则式，例如/welcome/.*
          path: "/welcome"</pre>
<p>现在，alpine访问index.html时会得到403（禁止访问）错误，而GET /welcome则正常访问：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 403

kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" -X GET http://nginx/welcome
# 200</pre>
<p>你可以通过下面的命令对流量进行监控：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec -it cilium-skvr6 -- cilium monitor -v --type l7

&lt;- Request http from 0 ([k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Denied HEAD http://nginx/welcome =&gt; 403

&lt;- Request http from 0 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 0
&lt;- Response http to 0 ([k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default]) 
                from 931 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 200</pre>
<div class="blog_h2"><span class="graybg">使用DNS规则</span></div>
<div class="blog_h3"><span class="graybg">锁死外部访问</span></div>
<p>假设我们想仅允许nginx访问docker.gmem.cc，可以使用下面的策略：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: nginx-egress
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  egress:
  # 两个规则
  # 第一个规则：允许访问域名docker.gmem.cc
  - toFQDNs:
    # 支持使用通配符，例如 *.gmem.cc
    - matchName: "docker.gmem.cc"  
  # 第二个规则：允许访问kube-dns
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      # 这个规则提示Cilium检查匹配pattern的DNS查询，并将结果缓存
      rules:
        dns:
        - matchPattern: "*"</pre>
<p>第二个规则的作用在于，允许nginx访问kube-dns服务，进行域名查询。同时，让Cilium的DNS Proxy能够记录nginx执行的所有DNS查询，并且记录域名和IP地址的对应关系。</p>
<p>Cilium缓存的DNS查询结果中的IP地址，才是真正放到BPF Map中的、允许访问的白名单。</p>
<p>应用上述策略后，nginx将无法访问任何集群内部服务，除了kube-dns，除非你配置额外的策略。测试一下效果：</p>
<pre class="crayon-plain-tag">kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://docker.gmem.cc         
# 200
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://blog.gmem.cc
# 000
# command terminated with exit code 7
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure http://nginx
# 000
# command terminated with exit code 7</pre>
<div class="blog_h3"><span class="graybg">联用toPorts </span></div>
<p>可以联合使用toFQDNs和toPorts，以限制访问外部服务使用的端口、通信协议：</p>
<pre class="crayon-plain-tag"># ...
  egress:
  - toFQDNs:
    - matchPattern: "*.gmem.cc" 
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">拦截和探查TLS </span></div>
<p>Cilium支持透明的探查TLS加密连接的内容。 基于这个能力，即使是HTTPS流量，Cilium也能做到API感知并应用L7策略。这种能力完全基于软件实现，并且是策略驱动的，仅仅探测策略选中的网络连接。</p>
<p>我们需要以下步骤，以实现TLS拦截/探查：</p>
<ol>
<li>创建一个内部使用的CA，并基于此CA创建办法证书，以实现TLS拦截。端点访问外部TLS服务时，请求被Cilium拦截，并使用此内部CA颁发的证书为端点提供TLS服务</li>
<li>使用Cilium网络策略的DNS规则，选择需要拦截的流量</li>
<li>进行TLS探查，例如：
<ol>
<li>利用cilium monitor来探查HTTP请求的详细内容</li>
<li>使用L7策略过滤/修改HTTP请求</li>
<li>通过Hubble进行观察</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">创建CA和证书</span></div>
<pre class="crayon-plain-tag"># 自签名CA证书
openssl genrsa -des3 -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.crt

# 生成被探查目标服务，这里是docker.gmem.cc的证书，注意填写正确的Common Name
openssl genrsa -out gmem.cc.key 2048
openssl req -new -key gmem.cc.key -out gmem.cc.csr

# 签名证书
openssl x509 -req -days 360 -in gmem.cc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out gmem.cc.crt -sha256

# 将证书和密钥写入为secret备用
kubectl create secret tls gmem-tls-data -n kube-system --cert=gmem.cc.crt --key=gmem.cc.key</pre>
<div class="blog_h3"><span class="graybg">将CA加入受信根证书列表</span></div>
<p>上面的自签名CA，需要加到源端点（客户端Pod）的受信任根证书列表：</p>
<pre class="crayon-plain-tag">kubectl cp ca.crt default/ubuntu:/usr/local/share/ca-certificates/ca.crt
kubectl exec ubuntu -- update-ca-certificates</pre>
<p>目标服务的CA证书，则需要写入secret备用。最简单办法是，将系统所有受信任证书的列表，一起写入：</p>
<pre class="crayon-plain-tag">kubectl cp default/ubuntu:/etc/ssl/certs/ca-certificates.crt ca-certificates.crt
kubectl -n kube-system create secret generic tls-orig-data --from-file=ca.crt=./ca-certificates.crt</pre>
<div class="blog_h3"><span class="graybg">创建DNS/TLS感知Egress策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-visibility-tls"
spec:
  endpointSelector:
    matchLabels:
      app: ubuntu
  egress:
  - toFQDNs:
    - matchName: "docker.gmem.cc"
    toPorts:
    - ports:
      - port: "443"
        protocol: "TCP"
      # 第一个TLS连接，也就是Cilium扮演服务端的连接，使用的证书（和密钥）
      terminatingTLS:
        secret:
          namespace: "kube-system"
          name: "gmem-tls-data"
      # 第二个TLS连接，也就是Cilium扮演客户端的连接，使用的受信任证书列表
      originatingTLS:
        secret:
          namespace: "kube-system"
          name: "tls-orig-data"
      # 启用L7策略
      rules:
        http:
        # 允许所有HTTP流量
        - {}
  - toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
          - matchPattern: "*"</pre>
<p>应用上述策略后，尝试从ubuntu访问docker.gmem.cc，然后通过cilium monitor -v --type l7探查发生的流量。</p>
<div class="blog_h2"><span class="graybg">gRPC安全策略</span></div>
<p>gRPC是基于HTTP2协议的，Cilium不支持gRPC的原语，但是gRPC服务/方法是映射到特定URL路径的POST方法的：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          #      gRPC服务          gRPC方法
          path: "/gmem.UserManager/GetName"</pre>
<div class="blog_h2"><span class="graybg">Kafka安全策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule2"
spec:
  endpointSelector:
    matchLabels:
      app: kafka
  ingress:
  - fromEndpoints:
    - matchLabels:
        # 允许alpine访问kafka
        app: alpine
    toPorts:
    - ports:
      - port: "9092"
        protocol: TCP
      rules:
        kafka:
        # 允许消费msgs主题
        - role: "consume"
          topic: "msgs"</pre>
<div class="blog_h2"><span class="graybg">Cassanadra安全策略</span></div>
<p>目前Cilium提供了对Apache Cassanadra的Beta支持。</p>
<p>Apache Cassanadra是一种NoSQL数据库，专注于提供高性能的（特别是写）事务能力，同时不以牺牲可用性和可扩容性为代价。Cassanadra以集群方式运行，客户端通过Cassanadra协议与集群通信。</p>
<p>Cilium能理解Cassanadra协议，从而控制客户端可以访问哪些表，可以对表进行哪些操作。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "secure-empire-cassandra"
specs:
    endpointSelector:
      matchLabels:
        app: cass-server
    ingress:
    - fromEndpoints:
      - matchLabels:
          app: alpine
      toPorts:
      - ports:
        - port: "9042"
          protocol: TCP
        rules:
          # Cassanadra协议
          l7proto: cassandra
          l7:
            # 允许的操作
          - query_action: "select"
            # 操作针对的表，正则式。指定表需要&lt;keyspace&gt;.&lt;table&gt;形式
            query_table: "system\\..*"
          - query_action: "select"
            query_table: "system_schema\\..*"
          - query_action: "insert"
            query_table: "attendance.daily_records"</pre>
<div class="blog_h2"><span class="graybg">本地重定向策略</span></div>
<p>所谓本地重定向，是指Pod发向IP地址/Service的流量，被重定向到本机Pod的情况。本地重定向策略管理这种流量 —— 它可以将<span style="background-color: #c0c0c0;">匹配策略的流量重定向到本机</span>。</p>
<p>该特性需要4.19+内核。使用选项 <pre class="crayon-plain-tag">--set localRedirectPolicy=true</pre> 开启该特性。</p>
<p>本地重定向策略对应自定义资源<pre class="crayon-plain-tag">CiliumLocalRedirectPolicy</pre>。以下配置字段：</p>
<ol>
<li><pre class="crayon-plain-tag">ServiceMatcher</pre>：用于被重定向的ClusterIP类型的服务</li>
<li><pre class="crayon-plain-tag">AddressMatcher</pre>：用于目的地是IP地址，不属于任何服务的情况</li>
</ol>
<p>当启用本地重定向策略后，非backend Pod访问frontend时，Cilium BPF数据路径会将frontend地址转换为一个本地backend Pod地址。如果流量从backend Pod发往frontend地址，则不会进行进行转换（导致的结果是访问frontend的原始端点），否则就导致循环。Cilium通过调用sk_lookup_助手函数实现这一逻辑。</p>
<div class="blog_h3"><span class="graybg">根据地址匹配</span></div>
<p>下面这个例子，将发往169.254.169.254:8080的TCP流量，重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-addr"
spec:
  redirectFrontend:
    addressMatcher:
      ip: "169.254.169.254"
      toPorts:
        - port: "8080"
          protocol: TCP
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">根据服务匹配</span></div>
<p>下面的例子，如果访问default/my-service，则重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-svc"
spec:
  redirectFrontend:
    serviceMatcher:
      serviceName: my-service
      namespace: default
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<ol>
<li>策略应用之前，匹配策略的已经存在的连接，不受策略影响</li>
<li>此策略不支持更新，只能删除重建</li>
</ol>
<div class="blog_h3"><span class="graybg">应用场景</span></div>
<p>本地重定向策略的一个应用场景是节点本地DNS缓存。 </p>
<p>节点本地DNS缓存在一个静态的IP地址上监听，配合本地重定向策略，可以拦截来自应用程序Pod的、发往kubed-dns ClusterIP的流量。</p>
<p>策略定义示例：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "node-local-dns"
  namespace: kube-system
spec:
  # 如果访问kube-dns
  redirectFrontend:
    serviceMatcher:
      serviceName: kube-dns
      namespace: kube-system
  redirectBackend:
    # 那么重定向到Node local DNS
    localEndpointSelector:
      matchLabels:
        k8s-app: node-local-dns
    # TCP和UDP都支持
    toPorts:
      - port: "53"
        name: dns
        protocol: UDP
      - port: "53"
        name: dns-tcp
        protocol: TCP</pre>
<div class="blog_h1"><span class="graybg">网络连接</span></div>
<div class="blog_h2"><span class="graybg">路由</span></div>
<div class="blog_h3"><span class="graybg">Encapsulation</span></div>
<p>如果不提供任何配置，Cilium自动运行在overlay（encapsulation）模式下，这种模式对底层网络的要求最小。overlay模式下所有节点组成基于UDP封装的网格。支持的封装协议包括：</p>
<ol>
<li>VxLAN：默认封装模式，占用8472/UDP端口</li>
<li>Geneve：占用6081/UDP端口</li>
</ol>
<p>所有Cilium节点之间的流量都被封装。</p>
<p>overlay模式的优点：</p>
<ol>
<li>简单：集群节点所在的网络，不需要对PodCIDR有任何感知。只要底层网络支持IP/UDP，即可构建出overlay网络</li>
<li>地址空间：由于不依赖底层网络，因而可以使用很大的IP地址范围，支持很大规模的Pod数量</li>
<li>自动配置：在编排系统中，每个节点可以被分配一个IP前缀，并独立进行IPAM</li>
<li>身份标识上下文：利用封装协议，可以为网络封包附带元数据。Cilium利用这种能力，来传输源节点的安全标识信息，让目标节点不必查询封包所属的实体</li>
</ol>
<p>overlay模式的缺点：</p>
<ol>
<li>MTU overhead：由于额外的封装头，导致有效MTU比native-routing小。对于VxLAN每个封包的有效MTU减少50字节。这会导致单个特定网络连接的最大吞吐率减小。使用Jumbo frames则实际影响大大减小</li>
</ol>
<div class="blog_h3"><span class="graybg">Native-Routing</span></div>
<p>配置<pre class="crayon-plain-tag">tunnel: disabled</pre>可以启用此datapath，这种模式下，目的地不是本机的封包，被委托给Linux的路由子系统处理。这要求连接节点的网络能够正确处理路由：</p>
<ol>
<li>要么所有节点直接位于L2网络中，可以配置<pre class="crayon-plain-tag">auto-direct-node-routes: true</pre></li>
<li>要么连接它们的路由器能够处理路由：
<ol>
<li>在云环境下，VPC需要和Cilium进行集成，以获得路由信息。目前主流云厂商已经支持</li>
<li>在支持BGP的路由器的配合下，基于BGP协议分发路由。可以通过kube-router来运行BGP守护程序</li>
</ol>
</li>
</ol>
<p>配置<pre class="crayon-plain-tag">native-routing-cidr: x.x.x.x/y</pre>指定可以进行native-routing的CIDR。</p>
<div class="blog_h2"><span class="graybg">IPAM</span></div>
<p>IPAM负责分配和管理网络端点（容器或其它）的IP地址。Cilium支持多种IPAM模式。</p>
<div class="blog_h3"><span class="graybg">kubernetes</span></div>
<p>使用Kubernetes自带的host-scope IPAM。地址分配委托给每个节点进行，per-node的Pod CIDR存放在v1.Node中。</p>
<div class="blog_h3"><span class="graybg">cluster-pool</span></div>
<p>这是默认的IPAM mode，它分配per-node的Pod CIDR，并在每个节点上使用host-scope的分配器来分配IP地址。</p>
<p>此模式和kubernetes类似，区别在于后者在v1.Node资源中存储per-node的Pod CIDR，而Cilium在<pre class="crayon-plain-tag">v2.CiliumNode</pre>中存储此信息。</p>
<p>此模式下，cilium-agent在启动时会等待v2.CiliumNode中的<pre class="crayon-plain-tag">Spec.IPAM.PodCIDRs</pre>字段可用。</p>
<p>通过Helm安装时，使用下面的值来启用此模式：</p>
<pre class="crayon-plain-tag">helm install ...
  --set ipam.mode=cluster-pool

  --set ipam.operator.clusterPoolIPv4PodCIDR=&lt;IPv4CIDR&gt;
  # 调整每个节点的CIDR规模
  --set ipam.operator.clusterPoolIPv4MaskSize=&lt;IPv4MaskSize&gt;

  --set ipam.operator.clusterPoolIPv6PodCIDR=&lt;IPv6CIDR&gt;
  --set ipam.operator.clusterPoolIPv6MaskSize=&lt;IPv6MaskSize&gt;</pre>
<p>在运行时，使用下面的命令查询IP分配错误：</p>
<pre class="crayon-plain-tag">kubectl get ciliumnodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.operator.error}{"\n"}{end}'</pre>
<p>使用下面的命令查看IP分配情况：</p>
<pre class="crayon-plain-tag">cilium status --all-addresses</pre>
<div class="blog_h3"><span class="graybg">crd</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png"><img class="size-full wp-image-37601 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png" alt="crd_arch" width="787" height="250" /></a></p>
<p>此模式下，cilium-agent会监听当前节点同名的v2.CiliumNode资源，每当CiliumNode被更新，cilium-agent会利用列在<pre class="crayon-plain-tag">spec.ipam.available</pre>的IP地址来更新本节点的IP池。如果<span style="background-color: #c0c0c0;">已经分配的IP地址从spec.ipam.available中移除</span>，仍然可以正常使用，但是释放后不能重新分配。</p>
<p>当IP被分配出去之后，会记录到<pre class="crayon-plain-tag">status.ipam.inuse</pre>字段。</p>
<p>你需要开发一个Operator，将IP地址分配给特定节点，此模式提供了很大的灵活性。</p>
<div class="blog_h2"><span class="graybg">IP遮掩</span></div>
<p>对于IPv4，容器访问外部流量时Cilium会自动进行SNAT，替换源地址为节点的IP地址。对于IPv6，IP遮掩仅在iptables模式下被支持。</p>
<p>使用选项<pre class="crayon-plain-tag">enable-ipv4-masquerade: false</pre>和<pre class="crayon-plain-tag">enable-ipv6-masquerade: false</pre>可以改变上述默认行为。</p>
<p>如果Pod IP在节点网络中可以路由，可以配置<pre class="crayon-plain-tag">native-routing-cidr</pre>，如果<span style="background-color: #c0c0c0;">目的地址</span>在此CIDR中，则不进行IP遮掩。</p>
<p>Cilium支持多种IP遮掩的实现模式。</p>
<div class="blog_h3"><span class="graybg">ebpf</span></div>
<p>最高效的实现，要求内核版本4.19+，默认启用。对应Helm值<pre class="crayon-plain-tag">bpf.masquerade=true</pre>。当前版本此特性依赖<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#kubeproxy-free">BPF NodePort</a>特性。</p>
<p>基于eBPF的IP遮掩，只能发生在挂钩了eBPF masquerading程序的节点出口设备上。哪些出口设备进行挂钩，可以通过Helm值<pre class="crayon-plain-tag">devices</pre>指定，如果不指定则基于<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#nodeport-devices">BPF NodePort device detection metchanism</a>自动选择。</p>
<p>使用cilium status命令可以检查哪些设备挂钩了：</p>
<pre class="crayon-plain-tag">kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep Masquerading
#                                       已挂钩设备     不遮掩的CIDR
# Masquerading:   BPF (ip-masq-agent)   [eth0, eth1]  10.0.0.0/16</pre>
<p>该模式支持TCP/UDP/ICMP这三类IPv4的L4协议，其中ICMP仅仅支持Echo请求/应答。</p>
<p>除了配置native-routing-cidr，你还可以配置Helm值<pre class="crayon-plain-tag">ipMasqAgent.enabled=true</pre>，更细粒度的控制，访问哪些目的IP时不需要进行遮掩。这个能力是依靠Cilium开发的eBPF版本的<a href="https://github.com/kubernetes-sigs/ip-masq-agent">ip-masq-agent</a>来实现的。</p>
<div class="blog_h3"><span class="graybg">iptables</span></div>
<p>遗留模式，支持在所有版本的内核上运行。</p>
<div class="blog_h2"><span class="graybg">IP分片处理</span></div>
<p>默认情况下，Cilium配置eBPF数据路径，进行IP分片跟踪，以允许不支持分段的协议能透明的通过网络传输大报文。</p>
<p>IP分片跟踪在eBPF中通过LRU Map实现，要求4.10+内核。该特性通过以下选项启用：</p>
<ol>
<li><pre class="crayon-plain-tag">enable-ipv4-fragment-tracking</pre>：启用或禁用IPv4分片跟踪，默认启用</li>
<li><pre class="crayon-plain-tag">bpf-fragments-map-max</pre>：控制使用IP分配的活动并发连接的数量</li>
</ol>
<p>UDP这样的协议，它没有TCP那种分段和重组的能力，大报文只能依赖于IP层的分片机制。由于IP分片缺乏重传机制，因此大UDP报文一旦丢失一个片段，就需要整个报文的重传。</p>
<div class="blog_h2"><span class="graybg">BGP</span></div>
<div class="blog_h3"><span class="graybg">集成BIRD</span></div>
<p>BIRD是一个开源软件，支持BGP协议。利用BIRD可以将Cilium管理的端点暴露到集群外部。</p>
<p>通过下面的命令安装bird2：</p>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt install bird2
# CentOS
yum install -y bird2

sudo systemctl enable bird
sudo systemctl restart bird</pre>
<p>节点配置文件示例：</p>
<pre class="crayon-plain-tag"># 其它节点只是ID不同
router id 10.0.3.1;

debug protocols all;

# 如果使用直接路由模式
filter cnionly {
    if net ~ 172.27.0.0/16 &amp;&amp; ifname != "cilium_host" then accept;
    else reject;
}

protocol kernel {
    learn;
    scan time 10;
    ipv4 {
        import none;            # 如果使用隧道模式
        import filter cnionly;  # 如果使用直接路由模式
        export none;
    };
}

protocol device {
    scan time 5;
}

# 直接添加到BIRD的路由表
protocol static {
    ipv4;
    # 宣告Pod CIDR
    route 172.27.0.0/16 via "cilium_host";  # 如果使用隧道模式
    # 宣告ClusterIP CIDR。不能和kube-proxy replacement联用，因为后者不允许集群外访问ClusterIP
    route 10.96.0.0/24  via "eth0";
    # 宣告LoadBalancer CIRD
    route 10.0.10.0/24 via "eth0";
}

# 连接到上游路由器，并宣告上面的静态路由
protocol bgp k8s {
    local as 65000;
    neighbor 10.0.0.1 as 65000;
    direct;
    ipv4 {
        export all;
    };
}
# 查看路由     birdc show route
# 查看BGP状态  birdc show protocols all k8s</pre>
<p>上游路由（反射器）配置示例：</p>
<pre class="crayon-plain-tag">log syslog all;

router id 10.0.0.1;

debug protocols all;

protocol kernel {
    scan time 10;
    ipv4 {
        import none;
        export all;
    };
}

protocol device {
    scan time 5;
}

protocol bgp k8s {
    local as 65000;
    neighbor range 10.0.3.0/24 as 65000;
    direct;
    rr client;
    ipv4 {
        import all;
        export all;
    };
}</pre>
<p>可以启用双向转发检测（Bidirectional Forwarding Detection，BFD），以加入路径故障检测（path failure detection）。BFD由一系列几乎独立的BFD会话组成，每个会话在<span style="background-color: #c0c0c0;">双方都启用了</span>BFD的路由器之间进行双向单播路径的监控。监控方式是周期性的、双向发送控制封包。</p>
<p>BFD不会进行邻居发现，BFD会话是按需（例如被BGP协议请求）创建的。</p>
<pre class="crayon-plain-tag">protocol bfd {
    interface "eth*" {
        min rx interval 100 ms;
        min tx interval 100 ms;
        idle tx interval 300 ms;
        multiplier 10;
    };
    # 不需要按需创建，直接初始化和这些邻居的BFD会话
    neighbor 10.0.3.2;
    neighbor 10.0.3.3;
}

protocol bgp k8s {
    # BGP支持使用BFD来发现邻居是否存活
    bfd on;
}</pre>
<p>下面的命令查看BFD会话状态：</p>
<pre class="crayon-plain-tag">birdc show bfd sessions

bfd1:
IP address                Interface  State      Since         Interval  Timeout
10.0.3.2                  virbr0     Up         11:56:01.055    0.100    1.000
10.0.3.1                  virbr0     Up         11:56:00.094    0.100    1.000
10.0.3.3                  virbr0     Up         11:56:00.389    0.100    1.000</pre>
<p>为了某些特殊目的，例如L4负载均衡，你需要在多个节点上配置Pod CIDR的静态路由，并且在Bird中配置<a href="/network-faq#ecmp">ECMP（Equal-cost multi-path）路由</a>。</p>
<pre class="crayon-plain-tag">protocol kernel {
    merge paths yes limit 3;
}</pre>
<div class="blog_h3"><span class="graybg">宣告LoadBalancerIP</span></div>
<p>Cilium可以原生支持，将LoadBalancer服务分配IP地址、并通过BGP协议将地址宣告出去。是否宣告LoadBalancer服务的IP，取决于服务的externalTrafficPolicy设置。</p>
<p>使用下面的Helm值启用该特性：<pre class="crayon-plain-tag">--set bgp.enabled=true --set bgp.announce.loadbalancerIP=true</pre>。该特性依赖于MetalLB。</p>
<p>添加bgp-config这个ConfigMap，参考：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: bgp-config
  namespace: kube-system
data:
  config.yaml: |
    peers:
      - peer-address: 10.0.0.1
        peer-asn: 65000
        my-asn: 65000
    address-pools:
      - name: default
        protocol: bgp
        addresses:
          - 10.0.10.0/24 </pre>
<div class="blog_h2"><span class="graybg">IPVLAN模式</span></div>
<div class="blog_h3"><span class="graybg">VETH vs IPVLAN<br /></span></div>
<p>容器通常都使用虚拟设备，例如veth对，作为连接初始命名空间的桥梁。通过在宿主机端veth挂钩tc ingress钩子，Cilium能够监控容器的任何流量。</p>
<p>veth对处理流量时，需要两次通过网络栈，相比起ipvlan有性能上的劣势。对于两个在同一节点上的容器veth端点，一个封包需要4次通过网络栈。</p>
<p>Cilium CNI也支持L3/L3S的ipvlan，这种模式下，宿主机物理设备作为ipvlan master，而容器端的ipvlan虚拟设备是slave。使用ipvlan时<span style="background-color: #c0c0c0;">将封包从其它网络命名空间推入ipvlan slave设备时消耗更少的资源，因而可能改善网络延迟</span>。使用ipvlan时Cilium在容器命名空间中挂钩BPF程序到ipvlan slave设备的egress钩子，以便应用L3/L4策略（因为初始命名空间下所有容器共享单个设备）。同时挂钩到ipvlan master的tc ingress钩子，可以对节点的所有入站流量应用网络策略。</p>
<p>为了支持老版本的不支持ipvlan hairpin模式的内核，Cilium在ipvlan slave设备（位于容器网络命名空间）的tc gress上挂钩了BPF程序。</p>
<p>当前版本的ipvlan支持有以下限制：</p>
<ol>
<li>NAT64不被支持</li>
<li>基于Envoy的L7 Policy不被支持</li>
<li>容器到host-local的通信不被支持</li>
<li>Service不支持LB到本地端点</li>
</ol>
<div class="blog_h3"><span class="graybg">启用IPVLAN</span></div>
<p>Cilium默认使用veth提供容器网络连接。你可以选用Beta支持的IPVLAN，目前尚未提供的特性包括：</p>
<ol>
<li>IPVLAN L2模式</li>
<li>L7策略支持</li>
<li>FQDN策略支持</li>
<li>NAT64</li>
<li>IPVLAN+隧道</li>
<li>基于eBPF的IP遮掩</li>
</ol>
<p>这些特性将在未来版本提供。</p>
<p>由于使用IPVLAN L3模式，需要4.12+的内核。如果使用L3S模式（流量经过宿主机网络栈因而被netfilter处理），这需要修复<a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=d5256083f62e2720f75bb3c5a928a0afe47d6bc3">d5256083f62e</a>（4.19.20）的稳定版内核。</p>
<p>参考下面的方式进行安装：</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用IPVLAN
  --set datapathMode=ipvlan \
  # 选择IPVLAN主设备，要求所有节点主设备名字相同
  --set ipvlan.masterDevice=eth0 \
  # IPVLAN数据路径目前仅支持直接路由，因此必须禁用tunnel
  --set tunnel=disabled  \
  # 要让IPVLAN跨节点工作，每个主机都必须安装正确的路由
  # 路由要么手工设置，要么由Cilium自动安装。对于后者，设置：
  --set autoDirectNodeRoutes="true" \
  # 下面的选项，用于控制是否安装iptables规则，这些规则主要用于和kube-proxy交互
  # 如果设置为false，则不安装，并且IPVLAN工作在L3模式
  # 默认值为true，IPVLAN工作在L3S模式，初始命名空间的netfilter会对容器封包进行过滤
  --set installIptablesRules="true"  \
  # 对所有离开IPVLAN master设备的流量进行IP遮掩
  --set masquerade="true"</pre>
<p>IPVLAN L3模式中宿主机的netfilters钩子被绕过，因此无法进行IP遮掩，必须使用L3S模式（会降低性能）。</p>
<div class="blog_h2"><span class="graybg">透明加密（IPsec）</span></div>
<p>Cilium支持使用IPsec/WireGuard透明的加密：</p>
<ol>
<li>Cilium管理的宿主机之间</li>
<li>Cilium管理的端点之间</li>
</ol>
<p>的流量。</p>
<p>为了确定某个连接是否可以被加密，Cilium需要明确封包目的地址是否是受管理的端点。在明确之前，流量可能不被加密。</p>
<p>同一主机内部的流量不会被加密。</p>
<p>如果在其它CNI插件之上链接Cilium，则目前无法支持透明加密特性。</p>
<div class="blog_h3"><span class="graybg">生成和导入PSK</span></div>
<pre class="crayon-plain-tag">kubectl create -n kube-system secret generic cilium-ipsec-keys \
    --from-literal=keys="3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2&gt; /dev/null | xxd -p -c 64)) 128"</pre>
<div class="blog_h3"><span class="graybg">启用加密</span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用Pod之间流量的加密
  --set encryption.enabled=true \
  # 启用节点流量加密（Beta）
  --set encryption.nodeEncryption=false \
  # 算法，默认ipsec
  --set encryption.type=ipsec    \
  # 如果启用直接路由（不使用隧道），则不指定下面选项的时候，会查询路由表，选择默认路由对应的网络接口
  --set encryption.ipsec.interface=ethX</pre>
<div class="blog_h2"><span class="graybg">宿主机可达服务</span></div>
<p>通过本节的配置，可以让服务从初始命名空间无需NAT的访问。</p>
<p>此特性要求4.19.57, 5.1.16, 5.2.0等版本以上的内核。如果仅要支持TCP（不支持UDP）则需要4.17.0。</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用此特性
  --set hostServices.enabled=true \
  # 仅仅支持TCP
  --set hostServices.protocols=tcp</pre>
<p>此特性的工作原理：在connect系统调用（TCP, connected UDP），或者 sendmsg/recvmsg系统调用（UDP）时，Cilium会检查目的地址，如果它是一个Service IP，则<span style="background-color: #c0c0c0;">直接将目的地址更换为一个后端的地址</span>。这样，套接字实际上会直接连接真实后端，<span style="background-color: #c0c0c0;">不会在更低层次的数据路径上发生NAT</span>，也就是对数据路径的更低层次透明。</p>
<p>宿主机可达服务，允许从宿主机/Pod中，以多种IP:NODE_PORT访问到NodePort服务。这些IP包括：环回地址、服务ClusterIP、节点本地接口（除了docker*）地址。</p>
<div class="blog_h2"><span class="graybg">kube-proxy replacement<br /></span></div>
<p>Cilium能够完全代替kube-proxy。此特性依赖“宿主机可达服务”，因此对内核有着相同的要求。Cilium还利用5.3/5.8添加的额外特性，进行了更进一步的优化。</p>
<div class="blog_h3"><span class="graybg">移除kube-proxy</span></div>
<p>基于kubeadm安装K8S时，可以用下面的命令跳过kube-proxy：</p>
<pre class="crayon-plain-tag">kubeadm init --skip-phases=addon/kube-proxy</pre>
<p>需要注意：如果节点有多网卡，确保kubelet的<pre class="crayon-plain-tag">--node-ip</pre>设置正确，否则Cilium可能无法正常工作。</p>
<p>如果集群已经安装了kube-proxy，可以使用下面的命令移除：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system delete ds kube-proxy
# 删除cm，可以防止升级K8S（1.19+）时候重新安装kube-proxy
kubectl -n kube-system delete cm kube-proxy</pre>
<div class="blog_h3"><span class="graybg">启用kube-proxy replacement<br /></span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
    --namespace kube-system \
    # 代替kube-proxy，取值：
    #   strict，如果内核不支持，则导致cilium-agent退出
    #   probe，探测内核特性，自动禁用不支持的特性子集。该取值假设kube-proxy不被删除，作为可能的fallback
    --set kubeProxyReplacement=strict \
    # 替换为API Server的地址和端口
    --set k8sServiceHost=10.0.3.1 \
    --set k8sServicePort=6443</pre>
<p>使用如上命令安装的Cilium，可以作为ClisterIP、NodePort、LoadBalancer，以及具有externalIP的服务的控制器。在此之上，eBPF kube-proxy replacement<span style="background-color: #c0c0c0;">还能够支持容器的hostPort，从而不再需要portmap</span>。</p>
<p>kube-proxy replacement同时支持直接路由和隧道模式。</p>
<p>使用下面的命令可以验证kube-proxy replacement已经正常安装：</p>
<pre class="crayon-plain-tag"># kubectl exec -it -n kube-system cilium-ch5qk --  cilium status --verbose 
# ...
KubeProxyReplacement:   Strict   [eth0 10.0.3.2 (Direct Routing)]
# ...
KubeProxyReplacement Details:
  Status:                Strict
  Socket LB Protocols:   TCP, UDP
  Devices:               eth0 10.0.3.2 (Direct Routing)
  Mode:                  SNAT
  Backend Selection:     Random
  Session Affinity:      Enabled
  XDP Acceleration:      Disabled
  Services:
  - ClusterIP:      Enabled
  - NodePort:       Enabled (Range: 30000-32767) 
  - LoadBalancer:   Enabled 
  - externalIPs:    Enabled 
  - HostPort:       Enabled</pre>
<div class="blog_h3"><span class="graybg">磁悬浮一致性哈希</span></div>
<p>kube-proxy replacement支持<a href="https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44824.pdf">磁悬浮（Maglev）一致性哈希算法</a>的变体，作为负载均衡算法。一致性哈希是一类算法，它将后端（RS）计算哈希值后，分布在一个环上。进行负载均衡时，对5元组计算哈希，然后看落在环上哪两个RS之间，取哈希值较小的RS作为LB目标。磁悬浮算法通过将每个RS在环上映射多次，减少当RS数量增加/减少时，必须映射到其它RS的五元组的数量。自然，减少一个RS之后，原先映射到其上的5元组必然需要重新映射，磁悬浮的目标是尽量减少除此之外的重新映射</p>
<p>该算法增强了故障时的弹性。新增节点后，在不需要和其它节点同步的前提下，能够对任意指定5元组能够保持相同的、一致性的后端选择；移除节点后，除了那些后端对应被移除节点的5元组，不超过1% difference in reassignments</p>
<p>通过<pre class="crayon-plain-tag">--set loadBalancer.algorithm=maglev</pre>启用</p>
<p>需要注意，<span style="background-color: #c0c0c0;">该LB算法仅用于外部（南北向）流量</span>。对于集群内部（东西向）流量，套接字直接分配到服务的后端，也就是说在TCP connect的时候，目的地址被修改为后端，不会使用该算法</p>
<p>Cilium XDP加速支持磁悬浮一致性哈希算法。</p>
<p>该算法有两个专用的配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">maglev.tableSize</pre>：每单个服务的Maglev查找表的大小。理想值M，应当大大大于期望后端数N的质数。最好大于100*N，以确保当后端发生变化时最多1% difference in reassignments。支持的取值包括251 509 1021 2039 4093 8191 16381 32749 65521 131071。取值16381用于大概160个后端的服务</li>
<li><pre class="crayon-plain-tag">maglev.hashSeed</pre>：用于避免受限于Cilium内置的固定的seed，seed是base64编码的16byte随机数。所有节点必须具有相同的seed</li>
</ol>
<div class="blog_h3"><span class="graybg">保留客户端源IP</span></div>
<p>Cilium基于eBPF的kube-proxy replacement实现了保留客户端源IP的能力。</p>
<p>Service的<pre class="crayon-plain-tag">externalTrafficPolicy</pre>选项决定Cilium的行为：</p>
<ol>
<li>Local：集群内的服务可以相互访问，也可以从没有该服务后端的节点上访问。集群内的端点，不需要SNAT就能实现访问服务时的负载均衡</li>
<li>Cluster：默认值。有多种途径保留客户端源IP。如果仅TCP服务需要暴露到集群外部，可以让kube-proxy replacement运行在DSR/Hybrid模式</li>
</ol>
<div class="blog_h3"><span class="graybg">直接服务器返回（DSR）</span></div>
<p>默认情况下，Cilium的eBPF NodePort实现，在SNAT模式下运作。也就是说，当来自外部的、访问集群服务的流量到达时，如果<span style="background-color: #c0c0c0;">入群节点判断出服务</span>（LoadBalancer/NodePort/其它具有ExternalIP的服务）<span style="background-color: #c0c0c0;">的后端位于其它节点</span>，<span style="background-color: #c0c0c0;">它就需要将请求重定向到远程节点。这个重定向时需要SNAT，将外部流量的源地址换成入群节点的地址</span></p>
<p>这个<span style="background-color: #c0c0c0;">SNAT的代价是，访问链路多了一跳，同时丢失了源IP信息</span>。为了进行reverse SNAT，返回报文还必须经过入群节点，然后传回给外部客户端</p>
<p>设置<pre class="crayon-plain-tag">loadBalancer.mode=dsr</pre>，可以让Cilium的eBPF NodePort实现切换到DSR模式。这种模式下，后端直接应答外部客户端，不经过入群节点。<span style="background-color: #c0c0c0;">这一特性必须和Direct Routing一起使用</span>，也就是不能使用隧道。</p>
<p>DSR模式的另外一个优势是，源IP地址被保留，因此，运行<span style="background-color: #c0c0c0;">在服务后端节点上的Cilium策略，可以正确的根据依据源IP进行过滤</span>。</p>
<p>由于一个后端可能被多个Service引用，<span style="background-color: #c0c0c0;">后端（所在节点的cilium-agent）需要知道生成（直接回复给原始客户端的）应答报文时，使用什么Service IP/Port（作为源地址）</span>。Cilium的解决办法是，<span style="background-color: #c0c0c0;">使用IPv4选项</span>或IPv6 Destination选项扩展，<span style="background-color: #c0c0c0;">将Service IP/Port信息编码到IP头中</span>，代价是MTU变小。对于TCP服务，<span style="background-color: #c0c0c0;">仅仅SYN封包需要编码Service IP/Port信息，因此MTU变小不会有影响</span>。</p>
<p>需要注意，在某些公有云环境下，DSR模式可能无法工作。原因可能是：</p>
<ol>
<li>底层Fabric<span style="background-color: #c0c0c0;">可能丢弃掉Cilium的IP选项</span></li>
<li>某些云实现了源/目的地址检查，你需要禁用此特性DSR才能正常工作</li>
</ol>
<p>为了避免UDP的MTU变小问题，可以设置<pre class="crayon-plain-tag">loadBalancer.mode=hybrid</pre>，这样对于UDP协议，会工作在SNAT模式，对于TCP则工作在DSR模式</p>
<div class="blog_h3"><span class="graybg">NodePort XDP加速</span></div>
<p>对于LoadBalancer/NodePort/其它具有ExternalIP的服务，如果外部流量入群节点上没有服务后端，则入群节点需要将请求转发给其它节点。Cilium 1.8+支持基于XDP进行加速这一转发行为。XDP工作在驱动层，大部分支持10G+bps的驱动都支持native XDP。云上环境中大多数具有SR-IOV变体的驱动也支持native XDP。在裸金属环境下，XDP加速可以和MetalLB这样的LoadBalancer控制器联用</p>
<p>要启用XDP加速，需要设置<pre class="crayon-plain-tag">loadBalancer.acceleration=native</pre>，默认值<pre class="crayon-plain-tag">disabled</pre>。对于大规模环境，可以考虑调优Map的容量：<pre class="crayon-plain-tag">config.bpfMapDynamicSizeRatio</pre></p>
<p>XDP加速可以和loadBalancer.mode：DSR/SNAT/hybrid一起使用。</p>
<div class="blog_h3"><span class="graybg">NodePort设备/端口/绑定设置</span></div>
<p>启用Cilium的eBPF kube-proxy replacement时，默认情况下，LoadBalancer/NodePort/其它具有ExternalIP的服务，可以通过这样的网络接口访问：</p>
<ol>
<li>具有默认路由的接口</li>
<li>被分配的K8S节点的InternalIP / ExternalIP的接口</li>
</ol>
<p>要改变设备，可以配置devices选项，例如<pre class="crayon-plain-tag">devices='{eth0,eth1,eth2}'</pre>。需要注意每个节点的名字必须一致，如果不一致，可以考虑用通配符<pre class="crayon-plain-tag">devices=eth+</pre></p>
<p>如果使用多个网络接口，仅其中单个可用于Cilium节点之间的直接路由。Cilium会选择具有InternalIP / ExternalIP的接口，InternalIP优先。你也可以手工指定直接路由设备<pre class="crayon-plain-tag">nodePort.directRoutingDevice=eth1</pre>，如果该选项中的设备，不在<pre class="crayon-plain-tag">devices</pre>中，Cilium会自动加入</p>
<p>直接路由设备也用于NodePort XDP加速，也就是说该设备的驱动应该支持native XDP</p>
<p>如果kube-apiserver使用了非默认的NodePort范围，则相同的配置必须传递给Cilium，例如<pre class="crayon-plain-tag">nodePort.range="10000\,32767"</pre></p>
<p>如果NodePort返回和内核临时端口范围（net.ipv4.ip_local_port_range）重叠，则Cilium会将NodePort范围附加到保留端口范围（net.ipv4.ip_local_reserved_ports）。这可以避免NodePort服务劫持宿主机本地应用程序发起的（源端口在和NodePort冲突的）连接。要禁用这种端口范围保护的行为，设置<pre class="crayon-plain-tag">nodePort.autoProtectPortRanges=false</pre></p>
<p>默认情况下，NodePort实现禁止应用程序对NodePort服务端口的bind系统调用，应用程序会接收到bind: Operation not permitted 错误。对于5.7+内核，在Pod内部bind不会报此错误。如果需要完全允许（包括老版本内核、5.7+在初始命名空间）bind，可以设置<pre class="crayon-plain-tag">nodePort.bindProtection=false</pre></p>
<div class="blog_h3"><span class="graybg">容器HostPort支持</span></div>
<p>尽管不是kube-proxy的一部分，Cilium的eBPF kube-proxy replacement也原生实现了hostPort，因此不需要使用CNI chaining：<pre class="crayon-plain-tag">cni.chainingMode=portmap</pre></p>
<p>如果启用了eBPF kube-proxy replacement，hostPort就自动支持，不需要额外配置。其它情况下，可以使用<pre class="crayon-plain-tag">hostPort.enabled=true</pre>启用此特性</p>
<p>如果指定hostPort时没有额外指定hostIP，则Pod的端口将通过宿主机用于暴露NodePort服务的那些IP地址，对外暴露出去。包括K8S的InternalIP/ExternalIP、环回地址。如果指定了hostIP则仅仅从该IP暴露，hostIP指定为0.0.0.0效果等于未指定。</p>
<div class="blog_h3"><span class="graybg">kube-proxy混合模式</span></div>
<p>除了完全代替kube-proxy，Cilium的eBPF kube-proxy replacement还可以与自共存，成为混合模式。混合模式的目的是解决某些内核版本不足以实现完全的kube-proxy replacement的问题。</p>
<p>kubeProxyReplacement取值：</p>
<ol>
<li>strict：严格完全替代或者失败</li>
<li>probe：混合模式。自动探测内核，并尽量替代</li>
<li>partial：混合模式。手工指定需要启用哪些eBPF kube-proxy replacement组件。取该值时必须设置<pre class="crayon-plain-tag">enableHealthCheckNodeport=false</pre>，以确保cilium-agent不会启动NodePort健康检查服务器。可以手工开启的特性如下，默认全部false：
<ol>
<li><pre class="crayon-plain-tag">hostServices.enabled</pre></li>
<li><pre class="crayon-plain-tag">nodePort.enabled</pre></li>
<li><pre class="crayon-plain-tag">externalIPs.enabled</pre></li>
<li><pre class="crayon-plain-tag">hostPort.enabled</pre></li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">会话绑定</span></div>
<p>Cilium的eBPF kube-proxy replacement支持K8S服务的会话绑定设置。对于<pre class="crayon-plain-tag">sessionAffinity: ClientIP</pre>，它会确保同一个Pod/宿主机总是被LB到同一个服务后端。会话绑定的默认超时为3h，可通过K8S的<pre class="crayon-plain-tag">sessionAffinityConfig</pre>改变。</p>
<p>会话绑定的依据，取决于请求的来源：</p>
<ol>
<li>对于集群外部发送给服务的请求，源IP地址用于会话绑定</li>
<li>对于集群内部发起的请求，则客户端网络命名空间的cookie用于会话绑定。这个特性5.7+内核支持，用于在socket layer实现会话绑定（此时源IP尚不可用，封包结构还没被内核创建）</li>
</ol>
<p>如果启用了eBPF kube-proxy replacement，则会话绑定默认启用。要启用，设置<pre class="crayon-plain-tag">config.sessionAffinity=false</pre></p>
<p>如果用户内核版本比较老，不支持网络命名空间cookie。则可以使用fallback的in-cluster模式，该模式使用一个固定的cookie，导致同一主机上，所有端点会绑定到某个服务的同一个后端。</p>
<div class="blog_h3"><span class="graybg">健康检查服务器</span></div>
<p>eBPF kube-proxy replacement包含一个health check server。要启用，需要设置<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr</pre>。例如<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr='0.0.0.0:10256'</pre>。/healthz端点用于访问健康状态。</p>
<div class="blog_h3"><span class="graybg">LoadBalancer源地址范围检查</span></div>
<p>如果LoadBalancer服务指定了spec.loadBalancerSourceRanges。则eBPF kube-proxy replacement会限制外部流量对服务的访问。仅仅允许spec.loadBalancerSourceRanges指定的CIDR白名单。从集群内部访问时，忽略此字段。</p>
<p>此特性默认启用，要禁用，设置<pre class="crayon-plain-tag">config.svcSourceRangeCheck=false</pre>。</p>
<div class="blog_h3"><span class="graybg">service-proxy-name</span></div>
<p>和kube-proxy类似，eBPF kube-proxy replacement遵从服务的<pre class="crayon-plain-tag">service.kubernetes.io/service-proxy-name</pre>注解。此注解声明什么服务代理（kube-proxy / replacement...）应该管理此服务。</p>
<p>eBPF kube-proxy replacement的服务代理名通过<pre class="crayon-plain-tag">k8s.serviceProxyName</pre>设置。默认值为空，意味着仅仅没有设置service.kubernetes.io/service-proxy-name的服务可以被replacement管理。</p>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>使用Cilium的eBPF kube-proxy replacement时，有很多限制条件需要注意：</p>
<ol>
<li>不能和透明加密一起使用</li>
<li>依赖宿主机可达服务这一特性。该特性需要依赖于eBPF cgroup hooks来实现服务转换。而eBPF中的getpeername需要5.8+内核才能支持。这意味着replacement无法和libceph一起工作</li>
<li>XDP加速仅支持单个设备的hairpin LB场景。如果具有多个网卡，并且cilium自动检测并选择多个网卡，则必须通过devices选项指定一个</li>
<li>DSR NodePort模式目前不能很好的在启用了TCP Fast Open（TFO）的环境下使用，建议切换到SNAT模式</li>
<li>不支持SCTP协议</li>
<li>不支持Pod配置的hostPort和NodePort范围冲突。这种情况下hostPort被忽略，并且cilium-agent会打印警告日志</li>
<li><span style="background-color: #c0c0c0;">不允许从集群外部访问ClusterIP</span></li>
<li>不支持ping ClusterIP，不像IPVS</li>
</ol>
<div class="blog_h2"><span class="graybg">带宽管理器</span></div>
<p>利用Cilium的带宽管理器，可以有效的在EDT（Earliest Departure Time）、eBPF的帮助下，管理每个Pod的带宽占用。</p>
<p>Cilium的带宽管理器，不依赖于CNI chaining，而是在Cilium内部实现的，它不使用<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping">bandwidth CNI</a>这个插件。出于可扩容性考虑（特别是对于多队列的网卡），不建议使用bandwith CNI插件，因为它基于qdisc TBF而非EDT。</p>
<p>Cilium带宽管理器支持Pod注解<pre class="crayon-plain-tag">kubernetes.io/egress-bandwidth</pre>，它在“原生宿主网络设备”上控制egress流量带宽。不管是直接路由还是隧道，都可以进行流量限制。</p>
<p>Pod注解<pre class="crayon-plain-tag">kubernetes.io/ingress-bandwidth</pre>不被支持，也不推荐使用。</p>
<p>带宽限制天然应该发生在egress以降低/整平在网线上的带宽使用。如果在ingress段进行带宽限制，会额外的、通过ifb设备，在节点的关键fast-path增加一层缓冲队列，这种情况下，流量需要被重定向到ifb设备的egress端以实现塑形。这本质上没有意义，因为流量已经占用了网线山的带宽，节点也已经消耗了资源处理它，唯一的作用就是引入ifb让上层应用遭受带宽限制的痛苦。</p>
<p>带宽管理器需要Linux 5.1+内核。</p>
<p>带宽管理器默认启用，不需要在安装时指定特殊的选项。如果想禁用，设置<pre class="crayon-plain-tag">bandwidthManager=false</pre>。</p>
<p>所谓“原生宿主网络设备”是指具有默认路由的网络接口，或者分配了InternalIP/ExternalIP的接口，分配InternalIP的接口优先。如果要手工指定设置，设置<pre class="crayon-plain-tag">devices</pre>选项。</p>
<p>对Pod进行带宽限制的例子：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  name: netperf
spec:
  selector:
    matchLabels:
      run: netperf
  replicas: 1
  template:
    metadata:
      labels:
        run: netperf
      annotations:
        kubernetes.io/egress-bandwidth: "10M"
    spec:
      nodeName: foobar
      containers:
      - name: netperf
        image: cilium/netperf
        ports:
        - containerPort: 12865</pre>
<div class="blog_h3"><span class="graybg">限制条件 </span></div>
<p>目前带宽管理器不能和L7策略联用。如果L7策略选择了Pod，则Pod上设置的注解被忽略，不进行带宽限制。</p>
<div class="blog_h2"><span class="graybg">联用Kata Containers</span></div>
<p>Cilium可以和Kata联用，后者提供计算层安全性。根据你使用的容器运行时，配置Cilium：</p>
<ol>
<li>如果使用CRI-O：<pre class="crayon-plain-tag">--set containerRuntime.integration=crio</pre></li>
<li>如果使用CRI-containerd：<pre class="crayon-plain-tag">--set containerRuntime.integration=containerd</pre></li>
</ol>
<p>Kata containers不支持宿主机可达服务特性，因而也不支持kube-proxy replacement的strict模式。</p>
<div class="blog_h2"><span class="graybg">Egress网关</span></div>
<p>出口网关允许将Pod的出口流量重定向到特定的网关节点，功能类似于Istio的出口网关。参考下面的选项启用该特性：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium
   --namespace kube-system \
   --reuse-values \
   --set egressGateway.enabled=true \
   --set bpf.masquerade=true \
   --set kubeProxyReplacement=strict</pre>
<p>你需要配置<pre class="crayon-plain-tag">CiliumEgressNATPolicy</pre>才能让Egress网关对特定端点生效：</p>
<pre class="crayon-plain-tag">apiVersion: cilium.io/v2alpha1
kind: CiliumEgressNATPolicy
metadata:
  name: egress-sample
spec:
  egress:
  - podSelector:
      matchLabels:
        # 如果端点是运行在default命名空间的app=alpine
        app: alpine
        io.kubernetes.pod.namespace: default
    # 也可以用命名空间选择器，匹配多个命名空间（中的所有Pod）
    # namespaceSelector:
    #  matchLabels:
    #    ns: default
  # 并且尝试访问下面的CIDR（集群外部服务）
  destinationCIDRs:
  - 192.168.33.13/32
  # 那么将流量转发给Egress网关，该网关（节点）配置了IP地址192.168.33.100
  # 出集群封包将SNAT为192.168.33.100
  egressSourceIP: "192.168.33.100"</pre>
<p>作为Egress网关的节点，需要在网络接口上配置额外的IP（对应上面的 egressSourceIP）。</p>
<div class="blog_h1"><span class="graybg">集群网格</span></div>
<p>Cluster Mesh将网络数据路径延伸到多个集群，支持以下特性：</p>
<ol>
<li>实现<span style="background-color: #c0c0c0;">所有集群的Pod之间相互连通</span>，不管使用直接路由还是隧道模式。不需要额外的网关节点或代理</li>
<li>支持全局服务，可以在所有集群访问</li>
<li>支持全局性的安全策略</li>
<li>支持跨集群边界通信的透明加密</li>
</ol>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">高可用</span></div>
<p>两个（位于不同Region或AZ的）集群组成高可用，当一个集群的后端服务（不是整个AZ不可用）出现故障时，可以failover到另外一个集群的对等物。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha.png"><img class="size-large wp-image-37965 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha-1024x342.png" alt="usecase_ha" width="710" height="237" /></a></p>
<div class="blog_h3"><span class="graybg">共享服务</span></div>
<p>最初的K8S用法是，倾向于创建巨大的、多租户的集群。而现在，更场景的用法是为每个租户创建独立的集群，甚至为不同类型的服务（例如安全级别不同）创建独立的集群。尽管如此，仍然有一些服务具有共享特征，不适合在每个集群中都部署一份。这类服务包括：日志、监控、DNS、密钥管理，等等。</p>
<p>使用集群网格，可以将共享服务独立部署在一个集群中，租户集群可以访问其中的全局服务。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services.png"><img class="size-large wp-image-37969 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services-1024x444.png" alt="usecase_shared_services" width="710" height="307" /></a></p>
<div class="blog_h3"><span class="graybg">联合Istio Multicluster<br /></span></div>
<p>Cilium Clustermesh和Istio Multicluster可以相互补充。典型的用法是，<span style="background-color: #c0c0c0;">Cilium提供跨集群的Pod IP可路由性，而这是Istio Multiplecluster所需要的前置条件</span>。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<ol>
<li>所有集群的Pod CIDR不冲突</li>
<li>所有节点的IP地址不冲突</li>
<li>所有集群的节点，都具有IP层的相互连接性。可能需要创建对等/VPN隧道</li>
<li>集群之间的网络必须允许跨集群通信，到底需要哪些端口本章后续会详述</li>
</ol>
<div class="blog_h2"><span class="graybg">启用网格</span></div>
<div class="blog_h3"><span class="graybg">指定集群标识</span></div>
<p>每个集群都需要唯一的名字和ID：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium \
   --namespace kube-system \
   --reuse-values \
   --set cluster.name=k8s --set cluster.id=27</pre>
<p>注意，如果改变正在运行的集群的ID/名字，其中所有工作负载都需要重新启动。因为ID用于生成安全标识（security identity），安全标识需要重新创建才能创建跨集群的通信。</p>
<pre class="crayon-plain-tag">helm --kube-context tke install cilium cilium/cilium --version 1.10.1                        \
  --namespace kube-system                                                                    \
  --set debug.enabled=true                                                                   \
  --set cluster.id=28                                                                        \
  --set rollOutCiliumPods=true                                                               \
  --set cluster.name=tke                                                                     \
  --set image.repository=docker.gmem.cc/cilium/cilium                                        \
  --set preflight.image.repository=docker.gmem.cc/cilium/cilium                              \
  --set image.useDigest=false                                                                \
  --set operator.image.repository=docker.gmem.cc/cilium/operator                             \
  --set operator.image.useDigest=false                                                       \
  --set certgen.image.repository=docker.gmem.cc/cilium/certgen                               \
  --set hubble.relay.image.repository=docker.gmem.cc/cilium/hubble-relay                     \
  --set hubble.relay.image.useDigest=false                                                   \
  --set hubble.ui.backend.image.repository=docker.gmem.cc/cilium/hubble-ui-backend           \
  --set hubble.ui.backend.image.tag=v0.7.9                                                   \
  --set hubble.ui.frontend.image.repository=docker.gmem.cc/cilium/hubble-ui                  \
  --set hubble.ui.frontend.image.tag=v0.7.9                                                  \
  --set hubble.ui.proxy.image.repository=docker.gmem.cc/envoyproxy/envoy                     \
  --set hubble.ui.proxy.image.tag=v1.18.2                                                    \
  --set etcd.image.repository=docker.gmem.cc/cilium/cilium-etcd-operator                     \
  --set etcd.image.tag=v2.0.7                                                                \
  --set nodeinit.image.repository=docker.gmem.cc/cilium/startup-script                       \
  --set nodeinit.image.tag=62bfbe88c17778aad7bef9fa57ff9e2d4a9ba0d8                          \
  --set clustermesh.apiserver.image.repository=docker.gmem.cc/cilium/clustermesh-apiserver   \
  --set clustermesh.apiserver.image.useDigest=false                                          \
  --set clustermesh.apiserver.etcd.image.repository=docker.gmem.cc/coreos/etcd               \
  --set tunnel=disabled                                                                      \
  --set autoDirectNodeRoutes=true                                                            \
  --set nativeRoutingCIDR=172.28.0.0/16                                                      \
  --set bpf.hostRouting=true                                                                 \
  --set ipam.mode=cluster-pool                                                               \
  --set ipam.operator.clusterPoolIPv4PodCIDR=172.28.0.0/16                                   \
  --set ipam.operator.clusterPoolIPv4MaskSize=24                                             \
  --set fragmentTracking=true                                                                \
  --set bpf.masquerade=true                                                                  \
  --set hostServices.enabled=true                                                            \
  --set kubeProxyReplacement=strict                                                          \
  --set k8sServiceHost=10.2.0.61                                                             \
  --set k8sServicePort=6443                                                                  \
  --set loadBalancer.algorithm=maglev                                                        \
  --set loadBalancer.mode=hybrid                                                             \
  --set bandwidthManager=true</pre>
<div class="blog_h3"><span class="graybg">创建cilium-ca</span></div>
<p>集群网格会基于此CA创建其API Server的数字证书：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system create secret generic --from-file=ca.key=ca.key --from-file=ca.crt=ca.crt cilium-ca </pre>
<div class="blog_h3"><span class="graybg">启用网格</span></div>
<p>需要在组成网格的两个集群中都执行cilium clustermesh enable命令：</p>
<pre class="crayon-plain-tag">cilium clustermesh enable --context k8s --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1
cilium clustermesh enable --context tke --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1</pre>
<p>上述命令会：</p>
<ol>
<li>部署clustermesh-apiserver到集群</li>
<li>生成所有必须的数字证书、保存为Secret</li>
<li>自动检测最佳的service类型，以暴露集群网格的控制平面给其它集群。某些时候，service类型不能自动检测，你可手工通过<pre class="crayon-plain-tag">--service-type</pre>指定</li>
</ol>
<p>通过下面的命令等待集群网格组件就绪：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre>，如果服务类型选择LoadBalancer，该命令也会等待LoadBalancer IP就绪。</p>
<div class="blog_h3"><span class="graybg">连接集群</span></div>
<p>最后一步是连接集群，只需要在网格的一端进行连接即可。对向连接会自动创建：</p>
<pre class="crayon-plain-tag">cilium clustermesh connect --context k8s --destination-context tke</pre>
<p>通过下面的命令等待连接成功：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre></p>
<div class="blog_h3"><span class="graybg">测试跨集群连接性</span></div>
<pre class="crayon-plain-tag">cilium connectivity test --context k8s --multi-cluster tke</pre>
<p>注意：<span style="background-color: #c0c0c0;">两个集群的Pod网络会被打通，你可以从一个集群直接访问另外一个集群的Pod</span>。<span style="background-color: #c0c0c0;">默认情况下，Cilium不允许从集群外部访问PodCIDR，可以ping但是访问端口会RST</span>。</p>
<div class="blog_h3"><span class="graybg">查看网格状态</span></div>
<pre class="crayon-plain-tag">cilium clustermesh status --context k8s
cilium clustermesh status --context tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>目前最多支持相互连接在一起的集群数量为255，未来此限制会放开，当：</p>
<ol>
<li>运行在直接路由模式时</li>
<li>运行在隧道模式，且启用加密时</li>
</ol>
<div class="blog_h2"><span class="graybg">服务发现</span></div>
<p>Cilium的集群网格，支持跨集群的服务发现和负载均衡。</p>
<div class="blog_h3"><span class="graybg">全局服务</span></div>
<p>跨集群的负载均衡，依赖于全局服务。所谓全局服务：</p>
<ol>
<li>在所有集群中具有相同的namespace和name</li>
<li>设置了注解<pre class="crayon-plain-tag">io.cilium/global-service: "true"</pre>，注意，<span style="background-color: #c0c0c0;">所有集群的服务都要添加此注解</span></li>
</ol>
<p>Cilium会自动跨越多个集群进行负载均衡。A集群中的Pod可能访问到B集群的后端。</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    io.cilium/global-service: "true"
    # 下面这个注解是隐含的
    io.cilium/shared-service: "true"
spec:
  type: ClusterIP
  ports:
  - port: 80
  selector:
    app: nginx</pre>
<div class="blog_h3"><span class="graybg">远程服务</span></div>
<p>如果设置<pre class="crayon-plain-tag">io.cilium/shared-service: "false"</pre>，则该服务的端点，仅由远程集群提供。</p>
<div class="blog_h2"><span class="graybg">网络策略</span></div>
<p>CiliumNetworkPolicy、NetworkPolicy自然就能跨集群生效，这是因为Cilium解耦了网络安全和网络连接性。但是这些对象不会自动复制到各集群，你需要手工处理。</p>
<p>下面的网络策略，允许特定端点跨集群的访问服务：</p>
<pre class="crayon-plain-tag"># 允许k8s中的alpine访问tke中的nginx
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-cross-cluster"
spec:
  endpointSelector:
    matchLabels:
      app: alpine
      io.cilium.k8s.policy.cluster: k8s
  egress:
  - toEndpoints:
    - matchLabels:
        app: nginx
        io.cilium.k8s.policy.cluster: tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>L7策略仅仅在以下条件下可以跨集群生效：</p>
<ol>
<li>启用直接路由模式，也就是禁用隧道</li>
<li>节点安装了路由，允许路由所有集群的Pod IP</li>
</ol>
<p>第2点，如果节点L2互联，可以通过设置--auto-direct-node-routes=true满足。</p>
<div class="blog_h2"><span class="graybg">集群外工作负载</span></div>
<p>你可以将外部工作负载（例如VM）加入到K8S集群，并且应用安全策略。</p>
<div class="blog_h3"><span class="graybg"> 前提条件</span></div>
<ol>
<li>必须配置基于K8S进行身份标识（identity）分配，即<pre class="crayon-plain-tag">identityAllocationMode=crd</pre>（默认值）</li>
<li>外部工作负载必须基于4.17+的内核，这样它才能访问K8S服务</li>
<li>外部工作负载必须和集群节点IP层互联。如果在同一VPC中运行虚拟机和K8S，通常可以满足。否则，可能需要在K8S集群网络和外部工作负载网络之间进行对等/VPN连接</li>
<li>外部工作负载必须具有唯一的IP地址，和集群内节点不冲突</li>
<li>目前此特性仅在VXLAN隧道模式下测试过</li>
</ol>
<div class="blog_h3"><span class="graybg">启用集群网格</span></div>
<pre class="crayon-plain-tag">cilium install --config tunnel=vxlan ...
cilium clustermesh enable</pre>
<div class="blog_h3"><span class="graybg">测试工作负载</span></div>
<p>必须创建<pre class="crayon-plain-tag">CiliumExternalWorkload</pre>来通知集群，外部工作负载的存在。该自定义资源：</p>
<ol>
<li>为外部工作负载指定命名空间和身份标识标签</li>
<li>名字必须和外部工作负载的主机名（hostname命令输出）一致</li>
<li>为外部工作负载分配一个很小的CIDR</li>
</ol>
<p>可以通过命令创建CiliumExternalWorkload：</p>
<pre class="crayon-plain-tag">#                  vm是子命令external-workload的别名
#                            工作负载名字
#                                      加入的命名空间
#                                              分配的CIDR
cilium clustermesh vm create zircon -n default --ipv4-alloc-cidr 10.0.0.1/32</pre>
<p>下面的命令可以查看现有外部工作负载的状态：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm status</pre>
<p>此时，可以看到 zircon 的<pre class="crayon-plain-tag">IP</pre>状态为<pre class="crayon-plain-tag">N/A</pre>，这提示工作负责尚未加入集群。</p>
<p>下面的命令会生成一个安装脚本：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm install install-external-workload.sh</pre>
<p>该脚本从集群中抽取了TLS证书、其它访问信息 ，可用于在外部工作负责中安装Cilium并连接到你的K8S集群。脚本中嵌入了clustermesh-apiserver服务的IP地址，如果你没有使用LoadBalancer类型而是使用NodePort，则IP是第一个K8S节点的地址。</p>
<p>拷贝install-external-workload.sh到外部工作负载节点，然后执行，该脚本会：</p>
<ol>
<li>创建并运行一个名为cilium的容器</li>
<li>拷贝cilium CLI到文件系统</li>
<li>等待节点连接到集群，集群服务可用。然后修改/etc/resolv.conf，将kube-dns地址设置到其中</li>
</ol>
<p>注意，如果外部工作负载有多个网络接口，在运行脚本之前你需要设置环境变量<pre class="crayon-plain-tag">HOST_IP</pre>。</p>
<p>在外部工作负载执行命令<pre class="crayon-plain-tag">cilium status</pre>检查连接性。</p>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">状态解读</span></div>
<p>每个cilium-agnet节点的各种状态信息，可以通过cilium status命令获得：</p>
<pre class="crayon-plain-tag"># kubectl -n kube-system exec cilium-m8wf2 -- cilium status
# 是否启用外部的KV存储
KVStore:                Ok   Disabled
# K8S状态
Kubernetes:             Ok   1.20 (v1.20.5) [linux/amd64]
Kubernetes APIs:        ["cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "core/v1::Namespace", "core/v1::Node", "core/v1::Pods", "core/v1::Service", "discovery/v1beta1::EndpointSlice", "networking.k8s.io/v1::NetworkPolicy"]
# kube-proxy replacement工作模式
KubeProxyReplacement:   Strict   [eth0 10.0.3.1 (Direct Routing)]
# cilium-agnet状态
Cilium:                 Ok   1.10.1 (v1.10.1-e6f34c3)
NodeMonitor:            Listening for events on 8 CPUs with 64x4096 of shared memory
Cilium health daemon:   Ok   
# 本节点IP池信息           已分配/总计                分配的节点CIDR
IPAM:                   IPv4: 9/254 allocated from 172.27.2.0/24, 
# 集群网格状态
ClusterMesh:            0/0 clusters ready, 0 global-services
# 带宽管理器             这里提示基于qdisc EDT实现，在eth0上进行带宽管理
BandwidthManager:       EDT with BPF   [eth0]
# 直接路由需要通过宿主机的网络栈
Host Routing:           Legacy
# 基于BPF实现IP遮掩                     不进行遮掩的CIDR
Masquerading:           BPF   [eth0]   172.27.0.0/16 [IPv4: Enabled, IPv6: Disabled]
Controller Status:      55/55 healthy
Proxy Status:           OK, ip 172.27.2.122, 0 redirects active on ports 10000-20000
Hubble:                 Ok   Current/Max Flows: 4095/4095 (100.00%), Flows/s: 67.08   Metrics: Disabled
# 流量加密已禁用
Encryption:             Disabled
# 集群节点/端点状态。如果存在不健康的对象，这里可以看到
Cluster health:         3/3 reachable   (2021-07-02T07:09:23Z)</pre>
<div class="blog_h1"><span class="graybg">开发</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>迁出项目后，在项目根目录下执行命令进行开发环境检查：<pre class="crayon-plain-tag">make dev-doctor</pre> 。</p>
<p>为了运行单元测试，需要docker；为了在虚拟机中运行Cilium，需要Vagrant和VirtualBox。建议在虚拟机中进行开发、构建、运行。</p>
<div class="blog_h3"><span class="graybg">Vagrant配置</span></div>
<p>通过下面的命令启动包含Cilium依赖的Vagrant虚拟机：</p>
<pre class="crayon-plain-tag"># 基于base系统cilium/ubuntu
contrib/vagrant/start.sh [vm_name]</pre>
<p>可选的vm_name用于添加新的虚拟机到现有集群中：</p>
<pre class="crayon-plain-tag"># 在节点上构建并安装K8S，主节点k8s1
#     创建一个从节点k8s2
#                使用net-next内核
K8S=1 NWORKERS=1 NETNEXT=1 ./contrib/vagrant/start.sh k8s2+

# 其它环境变量

# 执行vagrant reload而非vagrant up，用于恢复挂起的虚拟机
RELOAD=1
# 不在虚拟机中构建Cilium，用于快速重启（不去完全重新构建Cilium）
NO_PROVISION=1
# 启用Cilium的IPv4支持
IPV4=1
# 选择容器运行时：docker, containerd, crio
RUNTIME=docker
# 设置代理
VM_SET_PROXY=http://10.0.0.1:8088 
# 重新安装Cilium、K8S等，如果安装过程被打断有用
INSTALL=1
# 在虚拟机中构建Cilium前执行make clean
MAKECLEAN=1
# 不在虚拟机中进行构建，假设开发者先前已经在虚拟机中执行过make build
NO_BUILD=1
# 定义额外的挂载点
# USER_MOUNTS=foo 将宿主机的~/foo挂载为虚拟机的/home/vagrant/foo
# USER_MOUNTS=foo,/tmp/bar=/tmp/bar 额外挂载宿主机的/tmp/bar为虚拟机的/tmp/bar
USER_MOUNTS=
# 设置虚拟机内存,单位MB
VM_MEMORY=4096
# 设置虚拟机CPU数量
VM_CPUS=2</pre>
<p>Vagrantfile会在项目根目录寻找文件 <pre class="crayon-plain-tag">.devvmrc</pre>，如果文件存在且可执行，则VM启动时会执行它。你可以用该文件定制VM。</p>
<p>宿主机上的Cilium代码树不需要手工同步到虚拟机中，该目录默认已经通过VirtualBox NFS共享给虚拟机。</p>
<p>你也可以不使用start.sh脚本，手工启动虚拟机并构建Cilium：</p>
<pre class="crayon-plain-tag">vagrant init cilium/ubuntu
vagrant up
vagrant ssh [...]

go get github.com/cilium/cilium
cd go/src/github.com/cilium/cilium/
# 修改代码后，构建Cilium
make
# 重新安装Cilium
make install

mkdir -p /etc/sysconfig/
cp contrib/systemd/cilium.service /etc/systemd/system/
cp contrib/systemd/cilium-docker.service /etc/systemd/system/
cp contrib/systemd/cilium-consul.service /etc/systemd/system/
cp contrib/systemd/cilium  /etc/sysconfig/cilium
usermod -a -G cilium vagrant
systemctl enable cilium-docker
systemctl restart cilium-docker
systemctl enable cilium-consul
systemctl restart cilium-consul
systemctl enable cilium
# 重新启动新安装的Cilium
systemctl restart cilium

# 冒烟测试，确保Cilium正确启动，和Envoy的集成正常工作
tests/envoy-smoke-test.sh</pre>
<div class="blog_h3"><span class="graybg">构建开发者镜像</span></div>
<p>使用下面的命令，依据本地修改，构建cilium-agnet的镜像：</p>
<pre class="crayon-plain-tag">ARCH=amd64 DOCKER_REGISTRY=docker.gmem.cc DOCKER_DEV_ACCOUNT=cilium DOCKER_IMAGE_TAG=1.10.1 make dev-docker-image</pre>
<p>使用下面的命令，依据本地修改，构建cilium-operator的镜像：</p>
<pre class="crayon-plain-tag">make docker-operator-generic-image
# 类似，针对特定云平台的Operator镜像
make docker-operator-aws-image
make docker-operator-azure-image</pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg"> 数据路径代码</span></div>
<p>无法单步跟踪，主要依靠<pre class="crayon-plain-tag">cilium monitor</pre>。当cilium-agent或者某个特定的端点在debug模式下运行的话，Cilium会发送调试信息。</p>
<p>要让cilium-agent运行在debug模式，使用<pre class="crayon-plain-tag">--debug</pre>选项或在在运行时执行<pre class="crayon-plain-tag">cilium config debug=true</pre>。</p>
<p>要让特定端点进入debug模式，执行命令<pre class="crayon-plain-tag">cilium endpoint config ID debug=true</pre>。</p>
<p>使用<pre class="crayon-plain-tag">cilium monitor -v -v</pre>可以显示更多调试信息。</p>
<p>开发eBPF时，常遇到的问题是代码无法载入内核，此时，通过<pre class="crayon-plain-tag">cilium endpoint list</pre>会看到<pre class="crayon-plain-tag">not-ready</pre>状态的端点。你可以利用命令<pre class="crayon-plain-tag">cilium endpoint get</pre>来获取端点的eBPF校验日志。</p>
<p>目录<pre class="crayon-plain-tag">/var/run/cilium/state</pre>下的文件说明Cilium如何建立和管理BPF数据路径。.h文件包含了用于BPF程序编译的头文件配置，以数字为名的目录对应特定端点的状态，包括头文件和BPF二进制文件。</p>
<div class="blog_h3"><span class="graybg">查看eBPF Map</span></div>
<p>eBPF Map状态存放在/sys/fs/bpf/下，工具bpf-map可以用于查看其中的内容。</p>
<div class="blog_h1"><span class="graybg">源码分析(cilium-agent)</span></div>
<div class="blog_h2"><span class="graybg">环境准备</span></div>
<p>K8S中cilium-agent启动时的命令行为：</p>
<pre class="crayon-plain-tag">/usr/bin/cilium-agent --config-dir=/tmp/cilium/config-map</pre>
<p>/tmp/cilium/config-map是一个目录， 每个文件对应ConfigMap cilium-config中的一项。我们在本地调试cilium-agent时，可以将配置项写在YAML中：</p>
<pre class="crayon-plain-tag">auto-direct-node-routes: "true"
bpf-lb-map-max: "65536"
bpf-map-dynamic-size-ratio: "0.0025"
bpf-policy-map-max: "16384"
cilium-endpoint-gc-interval: 5m0s
cluster-id: "27"
cluster-name: k8s
cluster-pool-ipv4-cidr: 172.27.0.0/16
cluster-pool-ipv4-mask-size: "24"
custom-cni-conf: "false"
debug: "true"
disable-cnp-status-updates: "true"
enable-auto-protect-node-port-range: "true"
enable-bandwidth-manager: "true"
enable-bpf-clock-probe: "true"
enable-bpf-masquerade: "true"
enable-endpoint-health-checking: "true"
enable-health-check-nodeport: "true"
enable-health-checking: "true"
enable-host-legacy-routing: "false"
enable-host-reachable-services: "true"
enable-hubble: "true"
enable-ipv4: "true"
enable-ipv4-fragment-tracking: "true"
enable-ipv4-masquerade: "true"
enable-ipv6: "false"
enable-ipv6-masquerade: "true"
enable-l7-proxy: "true"
enable-local-redirect-policy: "false"
enable-policy: default
enable-remote-node-identity: "true"
enable-session-affinity: "true"
enable-well-known-identities: "false"
enable-xt-socket-fallback: "true"
hubble-disable-tls: "false"
hubble-listen-address: :4244
hubble-socket-path: /var/run/cilium/hubble.sock
hubble-tls-cert-file: /var/lib/cilium/tls/hubble/server.crt
hubble-tls-client-ca-files: /var/lib/cilium/tls/hubble/client-ca.crt
hubble-tls-key-file: /var/lib/cilium/tls/hubble/server.key
identity-allocation-mode: crd
install-iptables-rules: "true"
install-no-conntrack-iptables-rules: "false"
ipam: cluster-pool
kube-proxy-replacement: strict
kube-proxy-replacement-healthz-bind-address: ""
monitor-aggregation: medium
monitor-aggregation-flags: all
monitor-aggregation-interval: 5s
native-routing-cidr: 172.27.0.0/16
node-port-bind-protection: "true"
operator-api-serve-addr: 127.0.0.1:9234
preallocate-bpf-maps: "false"
sidecar-istio-proxy-image: cilium/istio_proxy
tunnel: disabled
wait-bpf-mount: "false"</pre>
<p>然后使用<pre class="crayon-plain-tag">--config=ciliumd.yaml</pre>启动cilium-agent。</p>
<div class="blog_h2"><span class="graybg">核心数据结构</span></div>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>端点表示一个容器或者类似的，能够在L3独立寻址（具有独立IP地址）的网络实体。端点由端点管理器管理。</p>
<p>Cilium中的端点，仅仅<span style="background-color: #c0c0c0;">在当前节点的视角下</span>考虑。</p>
<pre class="crayon-plain-tag">type Endpoint struct {
	owner regeneration.Owner

	// 节点范围内唯一的ID
	ID uint16

	// 端点创建时间
	createdAt time.Time

	// 保护端点状态写入操作
	mutex lock.RWMutex

	// 端点对应容器的名字
	containerName string

	// 端点对应容器的ID
	containerID string

	// 如果端点由Docker管理，该字段填写libnetwork网络ID
	dockerNetworkID string

	// 如果端点由Docker管理，该字段填写libnetwork端点ID
	dockerEndpointID string

	// IPVLAN数据路径下，对应的用于尾调用的BPF Map标识符
	datapathMapID int

	// 宿主机端的，连接到端点的网络接口名。通常是veth
	ifName string

	// 宿主机段网络接口索引
	ifIndex int

	// 端点标签配置
	// FIXME: 该字段应该命名为Label
	OpLabels labels.OpLabels

	// 身份标识版本号，该版本号在端点的身份标识标签变化后会递增
	identityRevision int

	// 端点的出口速率
	bps uint64

	// 端点的MAC地址
	mac mac.MAC

	// 端点的IPv6地址
	IPv6 addressing.CiliumIPv6

	// 端点的IPv4地址
	IPv4 addressing.CiliumIPv4

	// 宿主节点的MAC地址，对于每个端点不一样
	nodeMAC mac.MAC

	// 根据端点标签计算的安全标识
	SecurityIdentity *identity.Identity `json:"SecLabel"`

	// 提示端点是否被Istio注入了Cilium兼容的sidecar proxy：
	// 1. 如果是，该sidecar用于应用L7策略规则
	// 2. 如果否，则节点级别的Cilium Envoy用于应用L7策略规则
	// 
	// 目前仅针对HTTP L7规则，Kafka只能在节点级别Enovy中应用
	hasSidecarProxy bool

	// 数据路径的策略相关的Map，包含对所有策略相关的BPF的引用
	policyMap *policymap.PolicyMap

	// 跟踪policyMap压力的指标
	policyMapPressureGauge *metrics.GaugeWithThreshold

	// 端点的数据路径配置
	Options *option.IntOptions

	// 最后N次端点的状态转换信息
	status *EndpointStatus

	// 当前端点特定的DNS代理规则的集合。cilium-agent重启时能够恢复
	DNSRules restore.DNSRules

	// 为此端点拦截的，依然有效的DNS响应缓存
	DNSHistory *fqdn.DNSCache

	// 已经过期或者从DNSHistory中驱除的DNS IPs，在确认没有连接时用这些IP后会自动删除
	DNSZombies *fqdn.DNSZombieMappings

	// dnsHistoryTrigger is the trigger to write down the ep_config.h to make
	// sure that restores when DNS policy is in there are correct
	dnsHistoryTrigger *trigger.Trigger

	// 端点状态
	state State

	// 最后一次编译和安装的BPF头文件的哈希
	bpfHeaderfileHash string

	// 端点对应的K8S Pod名
	K8sPodName string

	// 端点对应的K8S Namespace
	K8sNamespace string

	// 端点对应的Pod
	pod *slim_corev1.Pod

	// 关联到Pod 的容器端口。基于端口名应用K8S网络策略时需要
	k8sPorts policy.NamedPortMap

	// 限制重复的警告日志
	logLimiter logging.Limiter

	// 跟踪k8sPorts被设置至少一次的情况
	hasK8sMetadata bool

	// 端点当前使用的策略的版本
	policyRevision uint64

	// policyRevisionSignals contains a map of PolicyRevision signals that
	// should be triggered once the policyRevision reaches the wanted wantedRev.
	policyRevisionSignals map[*policySignal]bool

	// 应用到代理的策略修订版
	proxyPolicyRevision uint64

	// 写proxyStatistics用的锁
	proxyStatisticsMutex lock.RWMutex

	proxy EndpointProxy

	// 代理重定向的统计信息，键是 policy.ProxyIDs
	proxyStatistics map[string]*models.ProxyStatistics

	// 端点已经更新的、下一次regenerate时使用的策略修订版
	nextPolicyRevision uint64

	// 当端点选项变化后，是否强制重新计算端点的策略
	forcePolicyCompute bool

	// buildMutex synchronizes builds of individual endpoints and locks out
	// deletion during builds
	buildMutex lock.Mutex

	// logger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic.LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	logger unsafe.Pointer

	// policyLogger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	policyLogger unsafe.Pointer

	// controllers is the list of async controllers syncing the endpoint to
	// other resources
	controllers *controller.Manager

	// realizedRedirects maps the ID of each proxy redirect that has been
	// successfully added into a proxy for this endpoint, to the redirect's
	// proxy port number.
	// You must hold Endpoint.mutex to read or write it.
	realizedRedirects map[string]uint16

	// ctCleaned indicates whether the conntrack table has already been
	// cleaned when this endpoint was first created
	ctCleaned bool

	hasBPFProgram chan struct{}

	// selectorPolicy represents a reference to the shared SelectorPolicy
	// for all endpoints that have the same Identity.
	selectorPolicy policy.SelectorPolicy

	desiredPolicy *policy.EndpointPolicy

	realizedPolicy *policy.EndpointPolicy

	visibilityPolicy *policy.VisibilityPolicy

	eventQueue *eventqueue.EventQueue

	// skippedRegenerationLevel is the DatapathRegenerationLevel of the regeneration event that
	// was skipped due to another regeneration event already being queued, as indicated by
	// state. A lower-level current regeneration is bumped to this level to cover for the
	// skipped regeneration levels.
	skippedRegenerationLevel regeneration.DatapathRegenerationLevel

	// DatapathConfiguration is the endpoint's datapath configuration as
	// passed in via the plugin that created the endpoint, e.g. the CNI
	// plugin which performed the plumbing will enable certain datapath
	// features according to the mode selected.
	DatapathConfiguration models.EndpointDatapathConfiguration

	aliveCtx        context.Context
	aliveCancel     context.CancelFunc
	regenFailedChan chan struct{}

	allocator cache.IdentityAllocator

	isHost bool

	noTrackPort uint16
}</pre>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>程序入口点位于daemon/cmd/daemon_main.go文件的RootCmd中： </p>
<pre class="crayon-plain-tag">var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, daemonSubsys)

	bootstrapTimestamp = time.Now()

	// RootCmd represents the base command when called without any subcommands
	RootCmd = &amp;cobra.Command{
		Use:   "cilium-agent",
		Short: "Run the cilium agent",
		Run: func(cmd *cobra.Command, args []string) {
			cmdRefDir := viper.GetString(option.CMDRef)
			if cmdRefDir != "" {
				genMarkdown(cmd, cmdRefDir)
				os.Exit(0)
			}

			// gops监听套接字，gops能够获取Go进程的网络连接、调用栈等诊断信息
			addr := fmt.Sprintf("127.0.0.1:%d", viper.GetInt(option.GopsPort))
			addrField := logrus.Fields{"address": addr}
			if err := gops.Listen(gops.Options{
				Addr:                   addr,
				ReuseSocketAddrAndPort: true,
			}); err != nil {
				log.WithError(err).WithFields(addrField).Fatal("Cannot start gops server")
			}
			log.WithFields(addrField).Info("Started gops server")

			bootstrapStats.earlyInit.Start()
			// 环境初始化
			initEnv(cmd)
			bootstrapStats.earlyInit.End(true)
			// 运行cilium-agent
			runDaemon()
		},
	}

	bootstrapStats = bootstrapStatistics{}
)</pre>
<div class="blog_h2"><span class="graybg">环境初始化</span></div>
<p>该部分的整体逻辑包括：</p>
<ol>
<li>初始化配置option.Config
<ol>
<li>初始化Map尺寸（sizeof***Element）相关配置</li>
<li>利用viper读取命令行选项、配置文件中的的选项</li>
<li>配置K8S API Server客户端参数</li>
<li>各种调试选项：flow/envoy/datapath/policy</li>
<li>其它选项处理</li>
</ol>
</li>
<li>打印Logo和版本信息</li>
<li>确然当前用户具有root权限</li>
<li>检查PATH下cilium-envoy文件的版本</li>
<li>如果identity-allocation-mode=crd，断言启用了K8S集成</li>
<li>如果必要，启用PProf端口</li>
<li>如果必要，打开自动BPF Map分配开关</li>
<li>检查目录：
<ol>
<li>LibDir: "/var/lib/cilium"</li>
<li>RunDir: "/var/run/cilium"</li>
<li>BpfDir: "/var/lib/cilium/bpf"</li>
<li>StateDir: "/var/run/cilium/state"</li>
</ol>
</li>
<li>检查数据路径最小要求
<ol>
<li>Linux版本最低4.8.0</li>
<li>需要启用策略路由支持，检查内核配置CONFIG_IP_MULTIPLE_TABLES</li>
<li>如果Cilium启用了IPv6，断言路径/proc/net/if_inet6存在</li>
<li>断言clang、llc存在且版本满足需求。需要在运行时编译BPF源码</li>
<li>如果BpfDir不存在，提示make install-bpf拷贝BPF源码</li>
<li>启动probes.NewProbeManager()探测系统的BPF特性，检查内核参数。断言包linux-tools-generic已经安装</li>
</ol>
</li>
<li>检查或挂载BPF文件系统</li>
<li>检查或挂载Cgroups2文件系统</li>
</ol>
<div class="blog_h3"><span class="graybg">检查用户权限</span></div>
<p>直接要求当前用户为root：</p>
<pre class="crayon-plain-tag">func RequireRootPrivilege(cmd string) {
	if os.Getuid() != 0 {
		fmt.Fprintf(os.Stderr, "Please run %q command(s) with root privileges.\n", cmd)
		os.Exit(1)
	}
}</pre>
<div class="blog_h3"><span class="graybg">检查数据路径最小要求</span></div>
<p>解析内核版本major.minor.patch的代码：</p>
<pre class="crayon-plain-tag">func parseKernelVersion(ver string) (semver.Version, error) {
	verStrs := strings.Split(ver, ".")
	switch {
	case len(verStrs) &lt; 2:
		return semver.Version{}, fmt.Errorf("unable to get kernel version from %q", ver)
	case len(verStrs) &lt; 3:
		verStrs = append(verStrs, "0")
	}
	// We are assuming the kernel version will be something as:
	// 4.9.17-040917-generic

	// If verStrs is []string{ "4", "9", "17-040917-generic" }
	// then we need to retrieve patch number.
	patch := regexp.MustCompilePOSIX(`^[0-9]+`).FindString(verStrs[2])
	if patch == "" {
		verStrs[2] = "0"
	} else {
		verStrs[2] = patch
	}
	return versioncheck.Version(strings.Join(verStrs[:3], "."))
}

// GetKernelVersion returns the version of the Linux kernel running on this host.
func GetKernelVersion() (semver.Version, error) {
	var unameBuf unix.Utsname
	if err := unix.Uname(&amp;unameBuf); err != nil {
		return semver.Version{}, err
	}
	return parseKernelVersion(string(unameBuf.Release[:]))
}</pre>
<p>比较版本时使用库<pre class="crayon-plain-tag">github.com/blang/semver/v4</pre>：</p>
<pre class="crayon-plain-tag">type Range func(Version) bool;  // 这是一个函数，调用后直接判断版本是否符合要求
func MustCompile(constraint string) semver.Range {
	verCheck, err := Compile(constraint)
	if err != nil {
		panic(fmt.Errorf("cannot compile go-version constraint '%s' %s", constraint, err))
	}
	return verCheck
}

minKernelVer = "4.8.0"
isMinKernelVer = versioncheck.MustCompile("&gt;=" + minKernelVer)
if !isMinKernelVer(kernelVersion) {

}</pre>
<p>检查是否支持策略路由：</p>
<pre class="crayon-plain-tag">_, err = netlink.RuleList(netlink.FAMILY_V4)
//                该errno表示地址族不支持？  https://man7.org/linux/man-pages/man3/errno.3.html
if errors.Is(err, unix.EAFNOSUPPORT) {
	log.WithError(err).Error("Policy routing:NOT OK. " +
		"Please enable kernel configuration item CONFIG_IP_MULTIPLE_TABLES")
}</pre>
<p>检查是否支持IPv6：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPv6 {
	if _, err := os.Stat("/proc/net/if_inet6"); os.IsNotExist(err) {
		log.Fatalf("kernel: ipv6 is enabled in agent but ipv6 is either disabled or not compiled in the kernel")
	}
}</pre>
<p>查找clang二进制文件：</p>
<pre class="crayon-plain-tag">if filePath, err := exec.LookPath("clang"); err != nil {}</pre>
<p>调用bpftool进行特性探测：</p>
<pre class="crayon-plain-tag">probeManager := probes.NewProbeManager()
func NewProbeManager() *ProbeManager {
	newProbeManager := func() {
		probeManager = &amp;ProbeManager{}
		// 调用bpftool -j feature probe探测内核配置
		probeManager.features = probeManager.Probe()
	}
	// Do只会调用一次，这保证了全局变量probeManager不会被重复初始化
	once.Do(newProbeManager)
	return probeManager
}

//  判断Cilium必须的、可选的内核配置是否满足
if err := probeManager.SystemConfigProbes(); err != nil {
	errMsg := "BPF system config check: NOT OK."
	// TODO(brb) warn after GH#14314 has been resolved
	if !errors.Is(err, probes.ErrKernelConfigNotFound) {
		log.WithError(err).Warn(errMsg)
	}
}
func (p *ProbeManager) SystemConfigProbes() error {
	if !p.KernelConfigAvailable() {
		return ErrKernelConfigNotFound
	}
	requiredParams := p.GetRequiredConfig()
	for param, kernelOption := range requiredParams {
		if !kernelOption.Enabled {
			// err
		}
	}
	optionalParams := p.GetOptionalConfig()
	for param, kernelOption := range optionalParams {
		if !kernelOption.Enabled {
			// warn
		}
	}
	return nil
}
// 必须内核配置列表，大部分取决于cilium-agent的选项
func (p *ProbeManager) GetRequiredConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	coreInfraDescription := "Essential eBPF infrastructure"
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_BPF"] = kernelOption{
		Enabled:     config.ConfigBpf.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_SYSCALL"] = kernelOption{
		Enabled:     config.ConfigBpfSyscall.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_NET_SCH_INGRESS"] = kernelOption{
		Enabled:     config.ConfigNetSchIngress.Enabled() || config.ConfigNetSchIngress.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_BPF"] = kernelOption{
		Enabled:     config.ConfigNetClsBpf.Enabled() || config.ConfigNetClsBpf.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_ACT"] = kernelOption{
		Enabled:     config.ConfigNetClsAct.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_JIT"] = kernelOption{
		Enabled:     config.ConfigBpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_HAVE_EBPF_JIT"] = kernelOption{
		Enabled:     config.ConfigHaveEbpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}

	return kernelParams
}
// 可选内核配置列表
func (p *ProbeManager) GetOptionalConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_CGROUP_BPF"] = kernelOption{
		Enabled:     config.ConfigCgroupBpf.Enabled(),
		Description: "Host Reachable Services and Sockmap optimization",
		CanBeModule: false,
	}
	kernelParams["CONFIG_LWTUNNEL_BPF"] = kernelOption{
		Enabled:     config.ConfigLwtunnelBpf.Enabled(),
		Description: "Lightweight Tunnel hook for IP-in-IP encapsulation",
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_EVENTS"] = kernelOption{
		Enabled:     config.ConfigBpfEvents.Enabled(),
		Description: "Visibility and congestion management with datapath",
		CanBeModule: false,
	}

	return kernelParams
}

// 创建一个头文件 /var/run/cilium/state/globals/bpf_features.h 包含描述内核特性的宏
if err := probeManager.CreateHeadersFile(); err != nil {
	log.WithError(err).Fatal("BPF check: NOT OK.")
}
func (p *ProbeManager) CreateHeadersFile() error {
	// ...
	return p.writeHeaders(featuresFile);
}</pre>
<div class="blog_h3"><span class="graybg">挂载文件系统 </span></div>
<p>Cilium可能不在初始命名空间下运行，并且初始命名空间下的/sys/fs/bpf已经被挂载到命名空间的特定位置（option.Config.BPFRoot）。下面的调用检查BPF文件系统是否已经挂载，如果没有则挂载之：</p>
<pre class="crayon-plain-tag">func checkOrMountFS(bpfRoot string, printWarning bool) error {
	// 如果必要，进行挂载
	if bpfRoot == "" {
		checkOrMountDefaultLocations(printWarning)
	} else {
		checkOrMountCustomLocation(bpfRoot, printWarning)
	}
	// 确保没有重复挂载
	multipleMounts, err := hasMultipleMounts()
	if multipleMounts {
		return fmt.Errorf("multiple mount points detected at %s", mapRoot)
	}
	return nil
}

func checkOrMountDefaultLocations(printWarning bool) error {
	// 首先检查 /sys/fs/bpf 是否挂载了BPFFS
	mounted, bpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)

	// 不是挂载点，则这里进行挂载
	if !mounted {
		mountFS(printWarning)
		return nil
	}
	// 挂载了，但是不是BPFFS。这意味着Cilium在容器中运行且宿主机/sys/fs/bpf没有挂载 （要避免这种情况！）
	// 这种情况下使用备用挂载点  /run/cilium/bpffs。此备用挂载点能够被Cilium使用
	// 但是会在Pod重启的时候导致umount，进而导致BPF Map（例如ct表）不可用，后果是
	// 所有到本地容器的连接被丢弃
	//
	//
	if !bpffsInstance {
		setMapRoot(defaults.DefaultMapRootFallback)

		cMounted, cBpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)
		if !cMounted {
			if err := mountFS(printWarning); err != nil {
				return err
			}
		} else if !cBpffsInstance {
			log.Fatalf("%s is mounted but has a different filesystem than BPFFS", defaults.DefaultMapRootFallback)
		}
	}
	log.Infof("Detected mounted BPF filesystem at %s", mapRoot)
	return nil
}
func IsMountFS(mntType int64, path string) (bool, bool, error) {
	var st, pst unix.Stat_t
	// 类似于stat，但当path是符号连接的时候，查看的是链接自身（而非目标）的信息
	err := unix.Lstat(path, &amp;st)
	if err != nil {
		if errors.Is(err, unix.ENOENT) {
			// non-existent path can't be a mount point
			return false, false, nil
		}
		return false, false, &amp;os.PathError{Op: "lstat", Path: path, Err: err}
	}

	parent := filepath.Dir(path)
	err = unix.Lstat(parent, &amp;pst)
	if err != nil {
		return false, false, &amp;os.PathError{Op: "lstat", Path: parent, Err: err}
	}
	if st.Dev == pst.Dev {
		// 如果路径和父目录的设备一样，意味着它不是挂载点
		return false, false, nil
	}

	// 否则，获取文件系统信息
	fst := unix.Statfs_t{}
	err = unix.Statfs(path, &amp;fst)
	if err != nil {
		return true, false, &amp;os.PathError{Op: "statfs", Path: path, Err: err}
	}
	//           文件系统类型
	return true, fst.Type == mntType, nil

}
func mountFS(printWarning bool) error {
	// ...
	if err := unix.Mount(mapRoot, mapRoot, "bpf", 0, ""); err != nil {
		return fmt.Errorf("failed to mount %s: %s", mapRoot, err)
	}
	return nil
}

func hasMultipleMounts() (bool, error) {
	num := 0
	mountInfos, err := mountinfo.GetMountInfo()
	for _, mountInfo := range mountInfos {
		// 什么时候两个条目具有相同挂载点
		if mountInfo.Root == "/" &amp;&amp; mountInfo.MountPoint == mapRoot {
			num++
		}
	}
	return num &gt; 1, nil
}
// 读取/proc/self/mountinfo获取所有挂载信息
func GetMountInfo() ([]*MountInfo, error) {
	fMounts, err := os.Open(mountInfoFilepath)
	defer fMounts.Close()
	return parseMountInfoFile(fMounts)
}</pre>
<p>除了bpf，还需要检查/挂载cgroup2。和bpf不一样的是，存在多个cgroupv2的root mount是无害的，因此不会作重复挂载检查。</p>
<pre class="crayon-plain-tag">func CheckOrMountCgrpFS(mapRoot string) {
	cgrpMountOnce.Do(func() {
		if mapRoot == "" {
			mapRoot = cgroupRoot
		}
		cgrpCheckOrMountLocation(mapRoot)
	})
}
func cgrpCheckOrMountLocation(cgroupRoot string) error {
	setCgroupRoot(cgroupRoot)
	mounted, cgroupInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeCgroup2, cgroupRoot)
	if !mounted {
		return mountCgroup()
	} else if !cgroupInstance {
		return fmt.Errorf("Mount in the custom directory %s has a different filesystem than cgroup2", cgroupRoot)
	}
	return nil
}
func mountCgroup() error {
	unix.Mount("none", cgroupRoot, "cgroup2", 0, "") //...
}</pre>
<div class="blog_h3"><span class="graybg">其它选项处理</span></div>
<p>initEnv阶段会进行大量的选项校验、处理。这里列出一些比较重要的。</p>
<p>至少启用IPv4/IPv6之一：</p>
<pre class="crayon-plain-tag">if !option.Config.EnableIPv4 &amp;&amp; !option.Config.EnableIPv6 {
		log.Fatal("Either IPv4 or IPv6 addressing must be enabled")
	}</pre>
<p>和数据路径模式相关的判断逻辑：</p>
<pre class="crayon-plain-tag">switch option.Config.DatapathMode {
	case datapathOption.DatapathModeVeth:
		// 使用VETH数据路径时，不能配置IPVLAN master设备名，默认使用隧道模式，隧道默认使用VXLAN技术
		if name := viper.GetString(option.IpvlanMasterDevice); name != "undefined" {
			log.WithField(logfields.IpvlanMasterDevice, name).
				Fatal("ipvlan master device cannot be set in the 'veth' datapath mode")
		}
		if option.Config.Tunnel == "" {
			option.Config.Tunnel = option.TunnelVXLAN
		}
	case datapathOption.DatapathModeIpvlan:
		// 使用IPVLAN数据路径时，必须禁用隧道，不支持IPSec
		if option.Config.Tunnel != "" &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
			log.WithField(logfields.Tunnel, option.Config.Tunnel).
				Fatal("tunnel cannot be set in the 'ipvlan' datapath mode")
		}
		if len(option.Config.Devices) != 0 {
			log.WithField(logfields.Devices, option.Config.Devices).
				Fatal("device cannot be set in the 'ipvlan' datapath mode")
		}
		if option.Config.EnableIPSec {
			log.Fatal("Currently ipsec cannot be used in the 'ipvlan' datapath mode.")
		}

		option.Config.Tunnel = option.TunnelDisabled
		// 尽管IPVLAN模式不允许指定--device，但是后续逻辑都是通过option.Config.Devices保存设备名的，因此这里
		// 读取--ipvlan-master-device并存放在option.Config.Devices
		iface := viper.GetString(option.IpvlanMasterDevice)
		if iface == "undefined" {
			// 必须指定Master设备名
			log.WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("ipvlan master device must be specified in the 'ipvlan' datapath mode")
		}
		option.Config.Devices = []string{iface}
		link, err := netlink.LinkByName(option.Config.Devices[0])
		if err != nil {
			log.WithError(err).WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("Cannot find device interface")
		}
		option.Config.Ipvlan.MasterDeviceIndex = link.Attrs().Index
		option.Config.Ipvlan.OperationMode = connector.OperationModeL3
		if option.Config.InstallIptRules {
			option.Config.Ipvlan.OperationMode = connector.OperationModeL3S
		} else {
			log.WithFields(logrus.Fields{
				logfields.URL: "https://github.com/cilium/cilium/issues/12879",
			}).Warn("IPtables rule configuration has been disabled. This may affect policy and forwarding, see the URL for more details.")
		}
	case datapathOption.DatapathModeLBOnly:
		// 仅LB模式
		log.Info("Running in LB-only mode")
		option.Config.LoadBalancerPMTUDiscovery =
			option.Config.NodePortAcceleration != option.NodePortAccelerationDisabled
		option.Config.KubeProxyReplacement = option.KubeProxyReplacementPartial
		option.Config.EnableHostReachableServices = true
		option.Config.EnableHostPort = false
		option.Config.EnableNodePort = true
		option.Config.EnableExternalIPs = true
		option.Config.Tunnel = option.TunnelDisabled
		option.Config.EnableHealthChecking = false
		option.Config.EnableIPv4Masquerade = false
		option.Config.EnableIPv6Masquerade = false
		option.Config.InstallIptRules = false
		option.Config.EnableL7Proxy = false
	default:
		log.WithField(logfields.DatapathMode, option.Config.DatapathMode).Fatal("Invalid datapath mode")
	}</pre>
<p> 要支持L7策略，必须允许安装iptables规则：</p>
<pre class="crayon-plain-tag">if option.Config.EnableL7Proxy &amp;&amp; !option.Config.InstallIptRules {
		log.Fatal("L7 proxy requires iptables rules (--install-iptables-rules=\"true\")")
	}</pre>
<p>IPSec + 隧道组合，需要4.19+内核：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPSec &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
		if err := ipsec.ProbeXfrmStateOutputMask(); err != nil {
			log.WithError(err).Fatal("IPSec with tunneling requires support for xfrm state output masks (Linux 4.19 or later).")
		}
	}</pre>
<p>如果要求Cilium安装iptables规则install-no-conntrack-iptables-rules，让所有Pod流量跳过netfilter的conntrack，则必须使用直接路由模式。因为隧道模式下，外层封包已经自动跳过conntrack：</p>
<pre class="crayon-plain-tag">if option.Config.InstallNoConntrackIptRules {
		if option.Config.Tunnel != option.TunnelDisabled {
			log.Fatalf("%s requires the agent to run in direct routing mode.", option.InstallNoConntrackIptRules)
		}

		// 此外，跳过conntrack必须和IPv4一起使用，原因是用于匹配PodCIDR的是native routing CIDR
		// 此CIDR目前仅支持IPv4
		if !option.Config.EnableIPv4 {
			log.Fatalf("%s requires IPv4 support.", option.InstallNoConntrackIptRules)
		}
	}</pre>
<p>使用隧道时，不能使用直接路由相关的选项。</p>
<p>auto-direct-node-routes：（L2模式下）将直接路由通过给其它节点</p>
<pre class="crayon-plain-tag">if option.Config.Tunnel != option.TunnelDisabled &amp;&amp; option.Config.EnableAutoDirectRouting {
		log.Fatalf("%s cannot be used with tunneling. Packets must be routed through the tunnel device.", option.EnableAutoDirectRoutingName)
	}</pre>
<div class="blog_h2"><span class="graybg">运行cilium-agent</span></div>
<div class="blog_h3"><span class="graybg">整体逻辑</span></div>
<p>runDaemon</p>
<p style="padding-left: 30px;">enableIPForwarding  启用IPv4/IPv6转发</p>
<p style="padding-left: 30px;">iptablesManager.Init  初始化iptables管理器，检查相关内核模块是否可用</p>
<p style="padding-left: 30px;">k8s.Init  初始化各种K8S客户端对象</p>
<p style="padding-left: 30px;">NewDaemon 创建新的cilium-agent守护进程实例</p>
<p style="padding-left: 60px;">WithDefaultEndpointManager 创建端点管理器，负责从底层数据源同步端点信息</p>
<div class="blog_h3"><span class="graybg"> 设置宿主机设备</span></div>
<pre class="crayon-plain-tag">func runDaemon() {
	datapathConfig := linuxdatapath.DatapathConfiguration{
		HostDevice: option.Config.HostDevice,
}</pre>
<p>使用veth数据路径时，默认使用的宿主机设备是cilium_host，它是一个veth，对端也在初始命名空间中，是cilium_net。 </p>
<p>直接路由模式下，从路由上看，发往PodCIDR的流量都是从cilium_host出去：</p>
<pre class="crayon-plain-tag">172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
<p>但是却找不到什么规则将其发送给Pod的veth（位于初始命名空间的一端）也就是lxc***网卡。</p>
<p>实际上，转发操作是通过挂钩在cilium_host中的BPF程序进行redirect实现的。类似的难以用传统Linux网络拓扑思维难以理解的地方会有很多。</p>
<div class="blog_h3"><span class="graybg">启用IP转发</span></div>
<pre class="crayon-plain-tag">if err := enableIPForwarding(); err != nil {
		log.WithError(err).Fatal("Error when enabling sysctl parameters")
	}

// ...
func enableIPForwarding() error {
	if err := sysctl.Enable("net.ipv4.ip_forward"); err != nil {
		return err
	}
	if err := sysctl.Enable("net.ipv4.conf.all.forwarding"); err != nil {
		return err
	}
	if option.Config.EnableIPv6 {
		if err := sysctl.Enable("net.ipv6.conf.all.forwarding"); err != nil {
			return err
		}
	}
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">初始化iptables管理器</span></div>
<pre class="crayon-plain-tag">iptablesManager := &amp;iptables.IptablesManager{}
	iptablesManager.Init()

// ...

unc (m *IptablesManager) Init() {
	modulesManager := &amp;modules.ModulesManager{}
	haveIp6tables := true
	// 内核模块管理器，读取/proc/modules中的模块信息
	modulesManager.Init()
	// 确保模块加载
	modulesManager.FindOrLoadModules(
		"ip_tables", "iptable_nat", "iptable_mangle", "iptable_raw",
		"iptable_filter");
	modulesManager.FindOrLoadModules(
		"ip6_tables", "ip6table_mangle", "ip6table_raw", "ip6table_filter")

	if err := modulesManager.FindOrLoadModules("xt_socket"); err != nil {
		if option.Config.Tunnel == option.TunnelDisabled {
			// xt_socket执行一个local socket match(根据封包进行套接字查找，匹配应该本地处理的封包)，并且
			// 设置skb mark，这样封包将被Cilium的策略路由导向本地网络栈，不会被ip_forward()处理
			//
			// 如果xt_socket模块不存在，那么可以禁用ip_early_demux，避免在ip_forward()中明确的drop
			// 在隧道模式下可以不需要ip_early_demux，因为我们可以在BPF逻辑中设置skb mark，这个BPF也是
			// 早于策略路由阶段的，可以保证封包被导向本地网络栈，不会ip_forward()转发
			// 
			// 如果对于任何场景，我们都能保证在封包到达策略路由之前，被设置好“to proxy” 这个skb mark
			// 则可以不需要xt_socket模块。目前对于endpoint routing mode，无法满足这一点
			log.WithError(err).Warning("xt_socket kernel module could not be loaded")

			if option.Config.EnableXTSocketFallback {
				v4disabled := true
				v6disabled := true
				if option.Config.EnableIPv4 {
					v4disabled = sysctl.Disable("net.ipv4.ip_early_demux") == nil
				}
				if option.Config.EnableIPv6 {
					v6disabled = sysctl.Disable("net.ipv6.ip_early_demux") == nil
				}
				if v4disabled &amp;&amp; v6disabled {
					m.ipEarlyDemuxDisabled = true
					log.Warning("Disabled ip_early_demux to allow proxy redirection with 
                                               original source/destination address without xt_socket support 
                                               also in non-tunneled datapath modes.")
				} else {
					log.WithError(err).Warning("Could not disable ip_early_demux, traffic
                                           redirected due to an HTTP policy or visibility may be dropped unexpectedly")
				}
			}
		}
	} else {
		m.haveSocketMatch = true
	}
	m.haveBPFSocketAssign = option.Config.EnableBPFTProxy

	v, err := ip4tables.getVersion()
	if err == nil {
		switch {
		case isWaitSecondsMinVersion(v):
			m.waitArgs = []string{waitString, fmt.Sprintf("%d", option.Config.IPTablesLockTimeout/time.Second)}
		case isWaitMinVersion(v):
			m.waitArgs = []string{waitString}
		}
	}
}</pre>
<p>封包到达内核之后，内核需要知道如何路由它。为了避免反复查找路由表，Linux出现过很多路由缓存机制。从3.6开始，Linux废弃了全局路由缓存，由各子系统（例如TCP协议栈）负责维护路由缓存。</p>
<p>在套接字级别，有一个dst字段作为路由缓存。当一个TCP连接最初建立时，此字段为空，随后dst被填充，该套接字的生命周期内，后续到来的skb都不再需要查找路由。</p>
<p>ip_early_demux是一个内核特性。它向（网络栈的）下优化某些类型的本地套接字（目前是已建立的TCP套接字）输入封包的处理，它的工作原理是：</p>
<ol>
<li>内核接收到封包后，需要查找skb对应的路由，然后查找skb对应的socket</li>
<li>这里存在一种浪费，对应已建立的连接，属于同一socket的skb的路由是一致的</li>
<li>如果能将路由信息缓存到套接字上，那么就可以避免为每个skb查找路由</li>
</ol>
<p>问题在于，对于主要（60%+流量）作为路由器使用的Linux系统，它会引入不必要的吞吐量下降，可以禁用。</p>
<p>回到Cilium的场景，使用HTTP策略时，需要将流量重定向给Envoy，同时保持源、目的IP地址不变。这个重定向是依赖于xt_socket模块的。如果xt_socket不可用则需要禁用ip_early_demux，否则会导致原本应该发给Envoy的流量被转发走。</p>
<div class="blog_h3"><span class="graybg">初始化K8S</span></div>
<pre class="crayon-plain-tag">func Init(conf k8sconfig.Configuration) error {
	// 创建K8S核心API客户端
	k8sRestClient, closeAllDefaultClientConns, err := createDefaultClient()
	if err != nil {
		return fmt.Errorf("unable to create k8s client: %s", err)
	}
	// 创建Cilium客户端
	closeAllCiliumClientConns, err := createDefaultCiliumClient()
	if err != nil {
		return fmt.Errorf("unable to create cilium k8s client: %s", err)
	}
	// 创建API Extensions客户端
	if err := createAPIExtensionsClient(); err != nil {
		return fmt.Errorf("unable to create k8s apiextensions client: %s", err)
	}

	// 心跳函数
	heartBeat := func(ctx context.Context) error {
		// Kubernetes默认的心跳是获取所在节点的信息
		// [0] https://github.com/kubernetes/kubernetes/blob/v1.17.3/pkg/kubelet/kubelet_node_status.go#L423
		// 这对于Cilium来说太重，因此这里的心跳采用检查/healthz端点的方式
		res := k8sRestClient.Get().Resource("healthz").Do(ctx)
		return res.Error()
	}
	
	// 对K8S进行心跳检测
	if option.Config.K8sHeartbeatTimeout != 0 {
		controller.NewManager().UpdateController("k8s-heartbeat",
			controller.ControllerParams{
				DoFunc: func(context.Context) error {
					runHeartbeat(
						heartBeat, // 心跳函数
						option.Config.K8sHeartbeatTimeout, // 超时
						// 后面的是超时后的回调，这里是关闭客户端连接
						closeAllDefaultClientConns,
						closeAllCiliumClientConns,
					)
					return nil
				},
				// 心跳运行间隔
				RunInterval: option.Config.K8sHeartbeatTimeout,
			},
		)
	}
	// 获取K8S版本进而推导具有哪些特性
	if err := k8sversion.Update(Client(), conf); err != nil {
		return err
	}
	if !k8sversion.Capabilities().MinimalVersionMet {
		return fmt.Errorf("k8s version (%v) is not meeting the minimal requirement (%v)",
			k8sversion.Version(), k8sversion.MinimalVersionConstraint)
	}

	return nil
}

// 创建Cilium客户端
import 	clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
func createDefaultCiliumClient() (func(), error) {
	restConfig, err := CreateConfig()
	closeAllConns := setDialer(restConfig)
	createdCiliumK8sClient, err := clientset.NewForConfig(restConfig)
	k8sCiliumCLI.Interface = createdCiliumK8sClient
	return closeAllConns, nil
}
// 为 rest.Config设置Dial，一个负责拨号（创建TCP连接）的函数
func setDialer(config *rest.Config) func() {
	if option.Config.K8sHeartbeatTimeout == 0 {
		return func() {}
	}
	ctx := (&amp;net.Dialer{
		Timeout:   option.Config.K8sHeartbeatTimeout,
		KeepAlive: option.Config.K8sHeartbeatTimeout,
	}).DialContext
	dialer := connrotation.NewDialer(ctx)
	// 拨号器的DialContext用于创建连接，CloseAll用于关闭它创建的所有连接
	// 这个关闭函数返回，并在心跳失败时调用以关闭连接
	config.Dial = dialer.DialContext
	return dialer.CloseAll
}</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">native routing cidr must be configured with option --native-routing-cidr in combination with --masquerade --tunnel=disabled</span></div>
<p>禁用隧道，使用直接路由时，必须指定native-routing-cidr选项（对应Helm值nativeRoutingCIDR）。该选项必须设置为PodCIDR，也就是Kubernetes的--cluster-cidr。该选项提示Cilium直接路由的目的地址范围。</p>
<div class="blog_h3"><span class="graybg">Failed to compile XDP program" error="Failed to load prog with ip cilium</span></div>
<p>可能提示不支持XDP，禁用<pre class="crayon-plain-tag">--set loadBalancer.acceleration=disabled</pre>。</p>
<div class="blog_h2"><span class="graybg">cilium status提示端点不通</span></div>
<p>异常时输出示例：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec cilium-4bsb6 -- cilium status
# ...
# Cluster health:           0/3 reachable   (2021-07-02T04:06:04Z)
#   Name                    IP              Node          Endpoints
#   k8s/k8s-3 (localhost)   10.0.3.3        reachable     unreachable
#   k8s/k8s-1               10.0.3.1        reachable     unreachable
#   k8s/k8s-2               10.0.3.2        unreachable   unreachable</pre>
<p>正常时输出示例：</p>
<pre class="crayon-plain-tag"># Cluster health:         3/3 reachable   (2021-07-02T04:09:19Z)</pre>
<p>任何导致网络不通的故障，都会出现类似报错。</p>
<div class="blog_h3"><span class="graybg">禁用隧道但未同步路由</span></div>
<p>配置<pre class="crayon-plain-tag">--set tunnel=disabled</pre>以使用直接路由模式后，容器网络封包不再使用VXLAN包装，而是直接在集群节点之间路由。</p>
<p>这需要集群节点之间有正确的路由规则：</p>
<ol>
<li>如果节点是L2直连的，应当设置<pre class="crayon-plain-tag">--set autoDirectNodeRoutes=true</pre>。这样，路由规则会直接安装到节点路由表中，例如：<br />
<pre class="crayon-plain-tag">route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
# 下面的两个PodCIDR的节点CIDR，通过以太网路由给其它节点
172.27.0.0      10.0.3.2        255.255.255.0   UG    0      0        0 eth0
172.27.1.0      10.0.3.3        255.255.255.0   UG    0      0        0 eth0
# 下面是本节点的CIDR，交给cilium_host虚拟设备处理
172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
</li>
<li>如果节点是L3互联的，则路由器需要和Cilium进行某种形式的集成。例如通过BGP协议，或者使用crd这个IPAM，静态规定每个节点的CIDR，然后更新路由器的路由规则</li>
</ol>
<div class="blog_h3"><span class="graybg">IaaS层配置异常</span></div>
<p>如果节点是虚拟机，则要考虑IaaS层的网络设置。例如OpenStack可能需要为Port配置Allowed Address Pairs，将PodCIDR加入其中。</p>
<div class="blog_h2"><span class="graybg">DSR模式下无法从集群外部访问LB服务</span></div>
<p>LoadBalancer服务的IP地址为：10.0.11.20，服务端点IP地址为：172.28.1.76。流量入口节点为m1，服务端点所在节点为m3。</p>
<p>在m1上通过tcpdump抓包：</p>
<pre class="crayon-plain-tag"># tcpdump -i any -nnn -vvv 'dst 172.28.1.76'
15:21:02.310338 IP (tos 0x10, ttl 64, id 58773, offset 0, flags [DF], proto TCP (6), length 68, options (unknown 154))
    10.2.0.1.36568 &gt; 172.28.1.76.2379: Flags [S], cksum 0xb799 (incorrect -&gt; 0x65e9), seq 2591998745, win 64240, options [mss 1460,sackOK,TS val 899286115 ecr 0,nop,wscale 9], length 0</pre>
<p>可以看到，在入口节点m1，已经尝试将请求直接转发给位于m3的172.28.1.76.2379。携带了IP选项154。但是，在m3上抓包，没有任何相关信息。</p>
<p>禁用DSR后，两个节点抓包可以发现通信正常：</p>
<pre class="crayon-plain-tag"># m1
15:36:13.301471 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5

# m3
15:36:13.301825 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5</pre>
<p>差别在于源地址、IP选项不同。考虑是OpenStack源地址检查的原因。增加Allowed Address Pairs：10.0.0.0/8，问题解决。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/cilium/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>CNI学习笔记</title>
		<link>https://blog.gmem.cc/cni-study-note</link>
		<comments>https://blog.gmem.cc/cni-study-note#comments</comments>
		<pubDate>Sat, 01 Feb 2020 03:02:15 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32707</guid>
		<description><![CDATA[<p>简介 容器网络 Kubernetes没有提供默认可用的容器网络，第三方提供的容器网络，必须满足以下条件： 容器之间可以相互通信，且不需要NAT 宿主机和容器可以相互通信，且不需要NAT 容器看到自己的IP，和其它节点/容器看到的它的IP，是一样的 即集群包含的每一个容器都拥有一个与集群中其它的容器、节点可直接路由的独立IP地址。但是Kubernetes并没有具体实现这样一个网络模型，而是设计了一个开放的容器网络标准CNI。 K8S容器网络，具有两种实现风格： Overlay Network，即通用的虚拟化网络模型，不依赖于宿主机底层网络架构，可以适应任何的应用场景，方便快速体验。但是性能较差，因为在原有网络的基础上叠加了一层Overlay网络，封包解包或者NAT对网络性能都是有一定损耗的 Underlay Network，即基于宿主机物理网络环境的模型，容器与现有网络可以直接互通，不需要经过封包解包或是NAT，其性能最好。但是其普适性较差，且受宿主机网络架构的制约，比如MAC地址可能不够用 网络插件 K8S的网络插件主要支持两类： CNI插件：基于v0.4.0版本，本文的主题 Kubenet插件：没什么用，通过网桥和host-local CNI插件简单的实现cbr0 Kubelet使用单个默认网络插件，全集群使用一个默认网络。在Kubelet启动时，它会探测插件列表，并且记住它们，在Pod生命周期的适当阶段（仅对于Docker，CRI会自行管理CNI插件）调用选择的插件。两个相关的命令行选项： --cni-bin-dir  Kubelet启动时从该目录中探测插件--network-plugin  对于CNI插件，取值[crayon-69d36c808d42a749095031-i/] 除了实现NetworkPlugin接口： <a class="read-more" href="https://blog.gmem.cc/cni-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/cni-study-note">CNI学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">容器网络</span></div>
<p>Kubernetes没有提供默认可用的容器网络，第三方提供的容器网络，必须满足以下条件：</p>
<ol>
<li>容器之间可以相互通信，且不需要NAT</li>
<li>宿主机和容器可以相互通信，且不需要NAT</li>
<li>容器看到自己的IP，和其它节点/容器看到的它的IP，是一样的</li>
</ol>
<p>即集群包含的每一个容器都拥有一个与集群中其它的<span style="background-color: #c0c0c0;">容器、节点可直接路由的独立IP地址</span>。但是Kubernetes并没有具体实现这样一个网络模型，而是设计了一个开放的容器网络标准CNI。</p>
<p>K8S容器网络，具有两种实现风格：</p>
<ol>
<li>Overlay Network，即通用的虚拟化网络模型，<span style="background-color: #c0c0c0;">不依赖于宿主机底层网络架构</span>，可以适应任何的应用场景，方便快速体验。但是<span style="background-color: #c0c0c0;">性能较差</span>，因为在原有网络的基础上叠加了一层Overlay网络，<span style="background-color: #c0c0c0;">封包解包或者NAT对网络性能都是有一定损耗</span>的</li>
<li>Underlay Network，即<span style="background-color: #c0c0c0;">基于宿主机物理网络</span>环境的模型，<span style="background-color: #c0c0c0;">容器与现有网络可以直接互通</span>，不需要经过封包解包或是NAT，其性能最好。但是其普适性较差，且受宿主机网络架构的制约，比如MAC地址可能不够用</li>
</ol>
<div class="blog_h2"><span class="graybg">网络插件</span></div>
<p>K8S的网络插件主要支持两类：</p>
<ol>
<li>CNI插件：基于v0.4.0版本，本文的主题</li>
<li>Kubenet插件：没什么用，通过网桥和host-local CNI插件简单的实现cbr0</li>
</ol>
<p>Kubelet使用单个默认网络插件，全集群使用一个默认网络。在Kubelet启动时，它会探测插件列表，并且记住它们，<span style="background-color: #c0c0c0;">在Pod生命周期的适当阶段（仅对于Docker，CRI会自行管理CNI插件）调用选择的插件</span>。两个相关的命令行选项：</p>
<p style="padding-left: 30px;">--cni-bin-dir  Kubelet启动时从该目录中探测插件<br />--network-plugin  对于CNI插件，取值<pre class="crayon-plain-tag">cni</pre></p>
<p>除了实现NetworkPlugin接口：</p>
<pre class="crayon-plain-tag">type NetworkPlugin interface {
	// Init initializes the plugin.  This will be called exactly once
	// before any other methods are called.
	Init(host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error

	// Called on various events like:
	// NET_PLUGIN_EVENT_POD_CIDR_CHANGE
	Event(name string, details map[string]interface{})

	// Name returns the plugin's name. This will be used when searching
	// for a plugin by name, e.g.
	Name() string

	// Returns a set of NET_PLUGIN_CAPABILITY_*
	Capabilities() utilsets.Int

	// SetUpPod is the method called after the infra container of
	// the pod has been created but before the other containers of the
	// pod are launched.
	SetUpPod(namespace string, name string, podSandboxID kubecontainer.ContainerID, annotations, options map[string]string) error

	// TearDownPod is the method called before a pod's infra container will be deleted
	TearDownPod(namespace string, name string, podSandboxID kubecontainer.ContainerID) error

	// GetPodNetworkStatus is the method called to obtain the ipv4 or ipv6 addresses of the container
	GetPodNetworkStatus(namespace string, name string, podSandboxID kubecontainer.ContainerID) (*PodNetworkStatus, error)

	// Status returns error if the network plugin is in error state
	Status() error
} </pre>
<p>以创建/清理容器网络，网络插件还需要支持kube-proxy。对于iptables模式的kube-proxy来说，对iptables的依赖是很显然的，因此网络插件应当进行适当的配置，<span style="background-color: #c0c0c0;">让容器流量对宿主机的iptabels可见。例如，当容器连接到Linux bridge时，必须设置/net/bridge/bridge-nf-call-iptables为1以确保iptables proxy正常工作</span>。使用其它技术时，你同样需要考虑为kube-proxy设置正确的路由。</p>
<div class="blog_h3"><span class="graybg">使用CNI插件</span></div>
<p>在K8S中，如果设置--network-plugin=cni则选用CNI作为网络插件。这时Kubelet会读取--cni-conf-dir=/etc/cni/net.d中的CNI配置，来创建Pod网络。配置文件中引用的任何插件都必须存在于--cni-bin-dir=/opt/cni/bin目录下。</p>
<p>如果--cni-bin-dir下有多个插件配置，则<span style="background-color: #c0c0c0;">Kubelet使用按字典序<strong>排名最靠前的</strong>那个配置文件</span>。</p>
<p>从CNI 0.2.0开始，除了通过配置文件指定的CNI插件，Kubelet还会使用一个标准的CNI插件lo，它不需要配置。</p>
<div class="blog_h2"><span class="graybg">CNI项目</span></div>
<p>容器网络接口（Container Network Interface，CNI）是CNCF项目之一，它关注容器的网络连接，负责在容器启动时创建网络资源，在容器删除时清理为其创建的网络资源。由于<span style="background-color: #c0c0c0;">网络和运行环境关系很密切，因此很必要进行插件化</span>，这是创建CNI项目的缘由。</p>
<p>CNI项目包含两大部分：</p>
<ol>
<li>CNI规范文档：
<ol>
<li>libcni，Go语言的CNI接口，供运行时调用，转调具体CNI插件</li>
<li>skel，Go语言的CNI插件骨架</li>
<li><a href="https://github.com/containernetworking/cni">https://github.com/containernetworking/cni</a></li>
</ol>
</li>
<li>一系列参考实现和样例插件：
<ol>
<li>接口插件：ptp、bridge、macvlan……</li>
<li>Chained插件：portmap、bandwidth、tuning</li>
<li><a href="https://github.com/containernetworking/plugins">https://github.com/containernetworking/plugins</a></li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">CNI规范</span></div>
<p>具有以下特点：</p>
<ol>
<li>供应商中立，<span style="background-color: #c0c0c0;">不仅仅是为K8S设计</span>。还可以被Mesos、CloudFoundry、podman、CRI-O使用</li>
<li>为网络操作定义了基本的执行流、配置格式</li>
<li>简单、向后兼容</li>
</ol>
<div class="blog_h3"><span class="graybg">配置格式</span></div>
<p>JSON格式，对于任何操作，此配置都从标准输入喂给插件。可以包含<span style="background-color: #c0c0c0;">标准的配置项、插件特有的配置项</span>，示例：</p>
<pre class="crayon-plain-tag">{
  "name": "mynet",
  "type": "bridge",
  "bridge": "mynet0",
  "isDefaultGateway": true,
  "forceAddress": false,
  "ipMasq": true,
  "hairpinMode": true,
  "ipam": {
    "type": "host-local",
    "subnet": "10.10.0.0/16"
  }
}</pre>
<div class="blog_h3"><span class="graybg">运行方式</span></div>
<ol>
<li>CNI插件是可执行文件，支持<span style="background-color: #c0c0c0;">ADD / DEL / CHECK / VERSION</span>几个命令</li>
<li>当期望进行网络配置操作时，由容器运行时调用并产生CNI插件进程</li>
<li>JSON配置、容器相关的数据，<span style="background-color: #c0c0c0;">通过stdin传递给CNI插件</span></li>
<li>CNI插件<span style="background-color: #c0c0c0;">通过stdout报告处理结果 </span></li>
</ol>
<div class="blog_h2"><span class="graybg">未来方向</span></div>
<p>在未来，CNI可能会支持：</p>
<ol>
<li>动态更新现有网络配置</li>
<li>网络带宽和防火墙策略的动态策略支持</li>
</ol>
<div class="blog_h2"><span class="graybg">知名实现</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">CNI实现</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">calico</td>
<td>参考：<a href="/calico">基于Calico的CNI</a></td>
</tr>
<tr>
<td class="blog_h3">galaxy</td>
<td>参考：<a href="/galaxy-study-note">Galaxy学习笔记</a></td>
</tr>
<tr>
<td class="blog_h3">flannel</td>
<td>参考：<a href="/flannel">Flannel学习笔记</a></td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">规范</span></div>
<div class="blog_h2"><span class="graybg">版本</span></div>
<p>规范的版本和CNI库的版本、插件样例/参考实现的版本无关。</p>
<div class="blog_h3"><span class="graybg">多版本支持</span></div>
<p>为了支持平滑升级，CNI插件的作者应当兼容多版本的CNI规范。特别是，已经发布出去的插件，需要保持对老版本的兼容。插件应该在应答<pre class="crayon-plain-tag">VERSION</pre>命令时，返回类似下面的结构：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.3.0",
  "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ]
}</pre>
<p>对于ADD命令，插件必须尊重网络配置JSON中指定的cniVersion：</p>
<ol>
<li>如果网络配置没有提供cniVersion字段，假设使用v0.2.0版本，返回结果应该是v0.2.0格式的</li>
<li>如果插件不支持配置指定的版本，应该返回错误</li>
</ol>
<div class="blog_h3"><span class="graybg">v0.1.0</span></div>
<p>初始版本。</p>
<p>ADD命令正常结果格式：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.1.0",
  "ip4": {
    "ip": &lt;ipv4-and-subnet-in-CIDR&gt;,
    "gateway": &lt;ipv4-of-the-gateway&gt;,  (optional)
    "routes": &lt;list-of-ipv4-routes&gt;    (optional)
  },
  "ip6": {
    "ip": &lt;ipv6-and-subnet-in-CIDR&gt;,
    "gateway": &lt;ipv6-of-the-gateway&gt;,  (optional)
    "routes": &lt;list-of-ipv6-routes&gt;    (optional)
  },
  "dns": {
    "nameservers": &lt;list-of-nameservers&gt;           (optional)
    "domain": &lt;name-of-local-domain&gt;               (optional)
    "search": &lt;list-of-additional-search-domains&gt;  (optional)
    "options": &lt;list-of-options&gt;                   (optional)
  }
}</pre>
<p>出错时的结果格式：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.1.0",
  "code": &lt;numeric-error-code&gt;,
  "msg": &lt;short-error-message&gt;,
  "details": &lt;long-error-message&gt; (optional)
}</pre>
<div class="blog_h3"><span class="graybg">v0.2.0</span></div>
<p>该版本主要增加了VERSION命令。该命令的返回结果格式参考上文。</p>
<p>该版本中args被作为一个保留字段。这样可以提供任意的结构化信息，而不是仅仅靠CNI_ARGS传递<pre class="crayon-plain-tag">k1=v1;k2=v2</pre>形式的字符串。</p>
<div class="blog_h3"><span class="graybg">v0.3.0</span></div>
<p>此版本提供了更加丰富的关于容器网络配置的信息，包括网络接口的细节信息，并且支持多个IP地址。</p>
<p>ADD命令正常结果格式：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.3.0",
  "interfaces": [                                            (this key omitted by IPAM plugins)
      {
          "name": "&lt;name&gt;",
          "mac": "&lt;MAC address&gt;",                            (required if L2 addresses are meaningful)
          "sandbox": "&lt;netns path or hypervisor identifier&gt;" (required for container/hypervisor interfaces, empty/omitted for host interfaces)
      }
  ],
  "ip": [
      {
          "version": "&lt;4-or-6&gt;",
          "address": "&lt;ip-and-prefix-in-CIDR&gt;",
          "gateway": "&lt;ip-address-of-the-gateway&gt;",          (optional)
          "interface": &lt;numeric index into 'interfaces' list&gt;
      },
      ...
  ],
  "routes": [                                                (optional)
      {
          "dst": "&lt;ip-and-prefix-in-cidr&gt;",
          "gw": "&lt;ip-of-next-hop&gt;"                           (optional)
      },
      ...
  ]
  "dns": {
    "nameservers": &lt;list-of-nameservers&gt;                     (optional)
    "domain": &lt;name-of-local-domain&gt;                         (optional)
    "search": &lt;list-of-additional-search-domains&gt;            (optional)
    "options": &lt;list-of-options&gt;                             (optional)
  }
}</pre>
<p>可以看到，和容器有关的网络接口的详细信息包含在结果中，每个接口都可以包含多个ip信息。</p>
<div class="blog_h3"><span class="graybg">v0.3.1</span></div>
<p>0.3.0版本存在一个BUG，Result结构中的ip字段应当已经被重命名为ips，以保证和IPAM的Result结构一致。v0.3.1解决此问题，并且所有first-party CNI插件（bridge，host-local等）都已经在0.3.1修改为，使用ips字段。</p>
<p>所有v.0.3.0的CNI插件需要注意此差异。</p>
<div class="blog_h3"><span class="graybg">v0.4.0</span></div>
<p>该版本主要增加了CHECK命令。</p>
<div class="blog_h2"><span class="graybg">总览</span></div>
<p>CNI规范来自rkt网络提案，它规定了Linux下应用容器网络的插件化解决方案。在此规范中，术语：</p>
<ol>
<li>容器（container）可以认为是Linux网络命名空间的同义词，其关联的单元取决于容器运行时，对于K8S来说是Pod，对于Docker来说是一个裸容器</li>
<li>网络（network）是一组可被唯一寻址的实体，它们能够相互通信。这些实体可以是上一条提到的容器、一个主机、某些网络设备（例如路由器）。容器可以被添加到1-N个网络，或者从中删除</li>
</ol>
<p>CNI规范规定的是<span style="background-color: #c0c0c0;">运行时和插件之间的接口</span>。 </p>
<div class="blog_h2"><span class="graybg">一般性建议</span></div>
<ol>
<li>在调用任何插件之前，容器运行时必须<span style="background-color: #c0c0c0;">先为容器创建网络命名空间</span> </li>
<li>运行时必须确定，<span style="background-color: #c0c0c0;">容器属于哪些网络</span>，对于这些网络，<span style="background-color: #c0c0c0;">分别应当调用什么插件</span></li>
<li>配置文件格式为JSON，包含name、type等必须字段。每次插件调用，参数可以不一样，为此目的，可选的<pre class="crayon-plain-tag">args</pre>字段包含变化的信息</li>
<li>为了将容器添加到每个网络，运行时必须<span style="background-color: #c0c0c0;">逐个的为每个网络调用插件</span></li>
<li>在容器销毁时，运行时<span style="background-color: #c0c0c0;">必须以相反顺序调用插件</span>，将容器从网络断开</li>
<li>运行时可以并行的为多个容器调用插件，但是对于<span style="background-color: #c0c0c0;">单个容器，必须串行调用</span></li>
<li>容器必须有<span style="background-color: #c0c0c0;">唯一性的标识</span>ContainerID，如果需要存储状态，插件必须以网络名、CNI_CONTAINERID、CNI_IFNAME（容器内接口名）作为联合主键</li>
<li>对于任一个上述联合主键，ADD不得被运行时调用两次。这隐含表示：一个容器不得被加入同一网络两次，除非它具有两个网络接口</li>
</ol>
<div class="blog_h2"><span class="graybg">CNI接口</span></div>
<p>插件<span style="background-color: #c0c0c0;">必须实现为可执行文件</span>，rkt/K8S等容器管理系统将通过命令行来调用它们。<span style="background-color: #c0c0c0;">运行时配置（RuntimeConf）一般通过环境变量传递，网络配置（NetworkConfig）则是通过标准输入喂入</span>。</p>
<p>CNI插件负责把一个网络接口（例如VETH对的一端）插入到容器的网络命名空间，并在宿主机上执行<span style="background-color: #c0c0c0;">任何必要的</span>操作（例如将VETH对的另外一端接到网桥）。CNI插件还必须<span style="background-color: #c0c0c0;">给接口分配IP地址</span>，并且通过调用适当的IPAM插件，<span style="background-color: #c0c0c0;">创建和IPAM一致的路由规则</span>。</p>
<div class="blog_h3"><span class="graybg">ADD</span></div>
<p>该接口负责将容器添加到网络中。</p>
<p>参数：</p>
<ol>
<li>Container ID，容器唯一标识符文本，由运行时分配，不得为空。以数字/字母开头，可以包含<pre class="crayon-plain-tag">_.-</pre>字符</li>
<li>Network namespace path，网络命名空间的路径，形式<pre class="crayon-plain-tag">/proc/PID/ns/net</pre>，或者指向该文件的绑定挂载/链接</li>
<li>Network configuration，描述网络的JSON配置</li>
<li>Extra arguments，基于容器级别的定制化配置参数</li>
<li>Name of the interface inside the container，在容器网络命名空间中创建并分配的网络接口的名字，符合Linux接口命名空间，不得超过16字符，不含<pre class="crayon-plain-tag">/ :</pre>或空白字符</li>
</ol>
<p>结果：</p>
<ol>
<li>Interfaces list，取决于插件，返回的信息可能包括沙盒（容器或Hypervisor）接口名、宿主机接口名、接口的硬件地址、沙盒的详细信息</li>
<li>IP configuration assigned to each interface，分配到沙盒/宿主机的IPv4/IPv6地址、网关、路由</li>
<li>DNS information，包含DNS信息（服务器、domain、search domains、options）的字典</li>
</ol>
<div class="blog_h3"><span class="graybg">DEL</span></div>
<p>该接口负责将容器从网络中移除。</p>
<p>参数：和ADD相同。</p>
<p>注意点：</p>
<ol>
<li>所有参数必须和ADD时一致</li>
<li>DEL必须清理掉容器持有的、在目标网络中的所有资源</li>
<li>如果存在已知的，针对容器的先前ADD操作，则运行时必须在插件（or all plugins in a chain）JSON中添加prevResult字段，该字段的内容是上一个ADD操作的结果，JSON形式</li>
<li>如果CNI_NETNS或/和prevResult没有提供，则插件应当尽可能清理更多的资源（例如释放IPAM分配的地址）并返回提示操作成功的响应</li>
<li>如果运行时缓存了针对容器的上一个ADD的结果，则它应该在成功DEL后清理掉此缓存</li>
</ol>
<p>如果某些资源丢失，此接口一般也不返回错误。例如：</p>
<ol>
<li>即使容器网络命名空间已不存在，IPAM插件也应该释放IP地址并且返回成功，除非网络命名空间对于IPAM管理是关键的</li>
<li>bridge插件应当将DEL委托给IPAM插件，并清理自身的资源，即使网络命名空间/容器接口已不存在</li>
</ol>
<div class="blog_h3"><span class="graybg">VERSION</span></div>
<p>该接口报告CNI插件相关的版本信息。没有参数，结果示例：</p>
<pre class="crayon-plain-tag">{
  // 当前CNI版本
  "cniVersion": "0.4.0",
  // 支持的CNI版本
  "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ]
}</pre>
<div class="blog_h3"><span class="graybg">CHECK</span></div>
<p>检查容器的网络是否如预期一样。</p>
<p>参数：和ADD一样， 但是Network configuration包含prevResult字段，存放上一个ADD调用的结果。</p>
<p>结果：返回错误或空。</p>
<p>注意点：</p>
<ol>
<li>插件必须检查prevResult，确定接口、地址符合预期</li>
<li>插件必须允许后续的chained plugin看到修改后的网络资源，例如路由</li>
<li>如果prevResult中列出的资源（接口、路由、地址）不存在，或者状态异常，应该返回错误</li>
<li>同样的，没有在prevResult中跟踪的网络资源，例如防火墙规则、流量塑形控制规则、IP预留、外部依赖，不存在或状态异常时，也应该返回错误</li>
<li>如果发现容器不可达，应单返回错误</li>
<li>插件需要处理在ADD后紧跟着的CHECK调用，这意味着需要考虑某些异步资源的合理创建延迟</li>
<li>插件需要调用所有被委托插件（例如IPAM）的CHECK，并将错误收集并返回</li>
<li>运行时不会对尚未ADD的容器调用CHECK，也不会对在ADD后进行DEL的容器进行CHECK</li>
<li>如果在配置列表中，disableCheck设置为true，则运行时不会调用CHECK</li>
<li>如果chain中一个插件返回错误，运行时可能选择停止继续迭代后续插件的CHECK</li>
<li>在ADD调用之后，直到DEL调用之前，运行时都可以调用CHECK</li>
<li>运行时可以假设CHECK失败的容器处于不可恢复的错误配置状态中</li>
</ol>
<div class="blog_h2"><span class="graybg">CNI调用结果</span></div>
<p>如果ADD操作成功，插件应该以0退出，并且在标准输出上打印JSON。JSON中ips、dns部分必须和IPAM插件输出的一致，interfaces数组则需要插件填写，因为IPAM插件对接口无感知：</p>
<pre class="crayon-plain-tag">{
  // 必须和Network Configuration中的一致
  "cniVersion": "0.4.0",
  // IPAM插件忽略此键值
  // 插件创建的接口列表
  // 如果变量CNI_IFNAME存在，插件必须使用此名字来创建sandbox/hypervisor接口，如果不能，返回错误
  "interfaces": [
      {
          "name": "&lt;name&gt;",
          // 如果L2地址有价值，则必须
          "mac": "&lt;MAC address&gt;",
          // 对于容器接口，必须是网络命名空间路径
          // 对于虚拟机，必须是虚拟化沙盒的ID，网络接口在其中创建
          // 对于宿主机接口，为空或忽略
          "sandbox": "&lt;netns path or hypervisor identifier&gt;"
      }
  ],
  // IP配置信息
  "ips": [
      {
          "version": "&lt;4-or-6&gt;",
          "address": "&lt;ip-and-prefix-in-CIDR&gt;",
          "gateway": "&lt;ip-address-of-the-gateway&gt;", // 可选
          "interface": &lt;numeric-index-into-interfaces-list&gt;
      },
      ...
  ],
  // 路由配置信息
  "routes": [ // 可选
      {
          "dst": "&lt;ip-and-prefix-in-cidr&gt;",
          "gw": "&lt;ip-of-next-hop&gt;"                           // 可选
      },
      ...
  ],
  // DNS信息
  "dns": { // 可选
    "nameservers": &lt;list-of-nameservers&gt;
    "domain": &lt;name-of-local-domain&gt;
    "search": &lt;list-of-additional-search-domains&gt;
    "options": &lt;list-of-options&gt;
  }
}</pre>
<p>如果ADD操作失败，则应该以非0退出，并打印： </p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.4.0",
  "code": &lt;numeric-error-code&gt;,
  "msg": &lt;short-error-message&gt;,
  "details": &lt;long-error-message&gt; (optional)
}</pre>
<div class="blog_h2"><span class="graybg">网络配置</span></div>
<p>网络配置为JSON格式，可以存放在磁盘上，或者由运行时从其它源动态生成。</p>
<p>插件可以定义自己的字段，知名字段包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">cniVersion</td>
<td>字符串。当前配置遵循的CNI规范版本</td>
</tr>
<tr>
<td class="blog_h3">name</td>
<td>字符串。网络名，在主机范围内必须唯一</td>
</tr>
<tr>
<td class="blog_h3">type</td>
<td>字符串。CNI插件的可执行文件的名字</td>
</tr>
<tr>
<td class="blog_h3">args</td>
<td>
<p>字典，可选。由运行时提供的额外参数。例如可以传递一组标签</p>
<p>此字段可以传入任何值</p>
</td>
</tr>
<tr>
<td class="blog_h3">ipMasq</td>
<td>布尔，可选。如果插件支持，则在宿主机上为此网络设置IP遮掩（动态SNAT）。如果宿主机需要充当容器IP的网关，则为true</td>
</tr>
<tr>
<td class="blog_h3">ipam</td>
<td>
<p>字典，可选。IPAM相关的配置：</p>
<p style="padding-left: 30px;">type：IPAM插件的可执行文件名</p>
</td>
</tr>
<tr>
<td class="blog_h3">dns</td>
<td>
<p>字典，可选。DNS相关配置：</p>
<p style="padding-left: 30px;">nameservers：此网络可以感知的，优先级排序的DNS服务器列表<br />domain：用于短名查找的本地域后缀<br />search：字符串列表，优先级排序的短名查找DNS后缀<br />options：传递给resolver的选项</p>
</td>
</tr>
<tr>
<td>runtimeConfig</td>
<td>
<p>知名非标准字段。运行时动态填充的信息应该存放在此</p>
<p>通过列出capabilities，插件可以请求运行时填充必要的动态信息</p>
</td>
</tr>
<tr>
<td>capabilities</td>
<td>
<p>知名非标准字段。和runtimeConfig配合使用，提示运行时插件具有哪些特性，运行时因而能够提供这些特性所需的动态参数</p>
<p>例如，一个端口映射插件，可以这样配置：</p>
<pre class="crayon-plain-tag">{
  "name" : "ExamplePlugin",
  "type" : "port-mapper",
  "capabilities": {"portMappings": true}
}</pre>
<p>这样，运行时传递给插件的、填充后的网络配置可能如下：</p>
<pre class="crayon-plain-tag">{
  "name" : "ExamplePlugin",
  "type" : "port-mapper",
  "runtimeConfig": {
    "portMappings": [
      {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
    ]
  }
}</pre>
</td>
</tr>
</tbody>
</table>
<p>插件可以定义额外的字段，如果配置文件中传入它不能理解的字段，可能会报错。例外是args字段，可以配置任何值。 
<div class="blog_h3"><span class="graybg">知名args</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Arg</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>labels</td>
<td>传递一系列键值对给插件<br />
<pre class="crayon-plain-tag">"labels" : [
  { "key" : "app", "value" : "myapp" },
  { "key" : "env", "value" : "prod" }
]</pre>
</td>
</tr>
<tr>
<td>ips</td>
<td>
<p>用于请求插件分配静态IP地址</p>
<pre class="crayon-plain-tag">"ips": ["10.2.2.42/24", "2001:db8::5"]</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">知名capabilities</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Capability</td>
<td style="text-align: center;">目的/runtimeConfig</td>
</tr>
</thead>
<tbody>
<tr>
<td>portMappings</td>
<td>
<p>传递宿主机端口、容器网络命名空间端口的映射关系
<pre class="crayon-plain-tag">[
  { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" },
  { "hostPort": 8000, "containerPort": 8001, "protocol": "udp" }
]</pre>
</td>
</tr>
<tr>
<td>ipRanges</td>
<td>
<p>动态配置分配的IP地址的范围。对于那些负责管理IP地址池（但是不管理单个IP）的运行时，可以传递这些信息给插件
<pre class="crayon-plain-tag">[
  [
    { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.3", "rangeEnd": 10.1.2.99", "gateway": "10.1.2.254" } 
  ]
]</pre>
</td>
</tr>
<tr>
<td>bandwidth</td>
<td>
<p>用于动态配置网络接口的带宽限制。单位bits/sec
<pre class="crayon-plain-tag">{ "ingressRate": 2048, "ingressBurst": 1600, "egressRate": 4096, "egressBurst": 1600 }  </pre>
</td>
</tr>
<tr>
<td>dns</td>
<td>
<p>由运行时动态提供DNS信息
<pre class="crayon-plain-tag">{ 
  "searches" : [ "internal.yoyodyne.net", "corp.tyrell.net" ] 
  "servers": [ "8.8.8.8", "10.0.0.10" ] 
}  </pre>
</td>
</tr>
<tr>
<td>ips</td>
<td>
<p>由运行时动态的给容器网络接口分配IP地址。如果容器运行时具有IP分配能力，可以传递
<pre class="crayon-plain-tag">[ "10.10.0.1/24", "3ffe:ffff:0:01ff::1/64" ]</pre>
</td>
</tr>
<tr>
<td>mac</td>
<td>
<p>容器运行时可以将MAC传递给那些需要mac作为输入的CNI插件
<pre class="crayon-plain-tag">"c2:11:22:33:44:55"  </pre>
</td>
</tr>
<tr>
<td>aliases</td>
<td>
<p>提供映射到容器IP地址的别名，便于位于同一个容器网络中的实体可以用此名字访问容器
<pre class="crayon-plain-tag">["my-container", "primary-db"]</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">配置示例</span></div>
<p>bridge：
<pre class="crayon-plain-tag">{
  "cniVersion": "0.4.0",
  "name": "dbnet",
  "type": "bridge",
  // 特有字段
  "bridge": "cni0",
  "ipam": {
    "type": "host-local",
    // 特有字段
    "subnet": "10.1.0.0/16",
    "gateway": "10.1.0.1"
  },
  "dns": {
    "nameservers": [ "10.1.0.1" ]
  }
}</pre>
<p><span class="graybg">macvlan：</span></p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.4.0",
  "name": "wan",
  "type": "macvlan",
  "ipam": {
    "type": "dhcp",
    "routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ]
  },
  "dns": {
    "nameservers": [ "10.0.0.1" ]
  }
}</pre>
<div class="blog_h2"><span class="graybg">网络配置列表</span></div>
<p>使用网络配置列表，可以<span style="background-color: #c0c0c0;">针对单个容器、按照特定顺序依次调用多个CNI</span>插件，并<span style="background-color: #c0c0c0;">将前一个插件的结果传递给后一个插件</span>。</p>
<p>注意：网络配置列表的目的并不是为了将容器加入到多个网络，而是为了实现端口映射、流量塑形等额外能力。运行时调用时不会为每个插件指定一个网络接口名称。</p>
<p>可用字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">cniVersion</td>
<td>字符串。当前配置遵循的CNI规范版本</td>
</tr>
<tr>
<td class="blog_h3">name</td>
<td>字符串。网络名，在主机范围内必须唯一</td>
</tr>
<tr>
<td class="blog_h3">disableCheck</td>
<td>布尔。如果为true则运行时不会调用CHECK</td>
</tr>
<tr>
<td class="blog_h3">plugins</td>
<td>
<p>标准的CNI插件的配置列表</p>
</td>
</tr>
</tbody>
</table>
<p>运行时调用单个插件时：</p>
<ol>
<li>会将name, cniVersion换成列表中的name, cniVersion</li>
<li>会将上一个插件的结果写入到prevResult字段</li>
<li>在DEL时，调用插件的顺序和ADD相反</li>
<li>在相同环境下调用所有插件</li>
<li>如果出错，则终止后续插件调用</li>
</ol>
<div class="blog_h3"><span class="graybg">calico配置示例</span></div>
<pre class="crayon-plain-tag">{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.0",
  "plugins": [
    {
      "type": "calico",
      "log_level": "info",
      "etcd_endpoints": "https://10.0.5.1:2379,https://10.0.2.1:2379,https://10.0.3.1:2379",
      "etcd_key_file": "/etc/cni/net.d/calico-tls/etcd-key",
      "etcd_cert_file": "/etc/cni/net.d/calico-tls/etcd-cert",
      "etcd_ca_cert_file": "/etc/cni/net.d/calico-tls/etcd-ca",
      "mtu": 1440,
      "ipam": {
          "type": "calico-ipam"
      },
      "policy": {
          "type": "k8s"
      },
      "kubernetes": {
          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
      }
    },
    {
      "type": "portmap",
      "snat": true,
      "capabilities": {"portMappings": true}
    }
  ]
}</pre>
<div class="blog_h2"><span class="graybg">IP地址分配</span></div>
<p>CNI插件可能需要为网络接口分配IP地址，并为网络接口安装相关的路由规则。为了支持不同的IP管理需求（DHCP、host-local），让IP分配和CNI插件基本功能解耦，规范定义了新的插件类型：IPAM Plugin。<span style="background-color: #c0c0c0;">调用IPAM Plugin是CNI插件的职责</span>，它应该在适当的时机发起调用，为网络接口获得IP地址。</p>
<p>IPAM插件需要决定：</p>
<ol>
<li>网络接口IP/子网</li>
<li>网关</li>
<li>路由</li>
</ol>
<p>并将这些信息返回给“主”CNI插件来应用。IPAM插件的信息来源可能是DHCP协议、本地文件系统中的数据、网络配置ipam段中的配置信息。</p>
<div class="blog_h3"><span class="graybg">IPAM接口</span></div>
<p>类似于CNI插件，IPAM插件也是通过运行可执行文件来调用的、参数也是通过stdin传递。 IPAM插件必须接收传递给CNI插件的全部环境变量。</p>
<p>如果调用成功，应当以0退出，并跟着如下格式的JSON：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.4.0",
  "ips": [
      {
          "version": "&lt;4-or-6&gt;",
          "address": "&lt;ip-and-prefix-in-CIDR&gt;",     // e.g. 192.168.1.3/24
          "gateway": "&lt;ip-address-of-the-gateway&gt;"  // optional
      },
      ...
  ],
  "routes": [                                       // optional
      {
          "dst": "&lt;ip-and-prefix-in-cidr&gt;",
          "gw": "&lt;ip-of-next-hop&gt;"                  // optional
      },
      ...
  ]
  "dns": {                                          // optional
    "nameservers": &lt;list-of-nameservers&gt;            // optional
    "domain": &lt;name-of-local-domain&gt;                // optional
    "search": &lt;list-of-search-domains&gt;              // optional
    "options": &lt;list-of-options&gt;                    // optional
  }
}</pre>
<p>上述返回结果类似于CNI的返回结果，但是不包含interfaces键，IPAM不需要关心网络接口的信息，将IP配置给网络接口是“主”插件的职责。</p>
<div class="blog_h2"><span class="graybg">知名错误码</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">错误码</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>不兼容的CNI版本</td>
</tr>
<tr>
<td>2</td>
<td>网络配置中包含不支持的字段，错误消息中应该指明不支持的键值</td>
</tr>
<tr>
<td>3</td>
<td>未知或不存在的容器，隐含运行时不需要进行任何容器网络清理</td>
</tr>
<tr>
<td>4</td>
<td>无效的必须环境变量，例如CNI_COMMAND、CNI_CONTAINERID</td>
</tr>
<tr>
<td>5</td>
<td>I/O错误，例如无法从stdin读取网络配置</td>
</tr>
<tr>
<td>6</td>
<td>无法解析网络配置</td>
</tr>
<tr>
<td>7</td>
<td>无效网络配置</td>
</tr>
<tr>
<td>11</td>
<td>出现临时性的错误，提示运行时后续重试</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">环境变量</span></div>
<p>对于Kubelet来说，CNI的网络配置通常是静态文件，那么针对容器/Pod的运行时参数，都需要通过环境变量传递。</p>
<p>即使使用了插件列表，Kubelet也仅会执行一次CNI调用，这些环境变量对应RuntimeConfig的字段。每个插件看到的都是一样的值。</p>
<p>例如CNI_IFNAME，它总是eth0，是Kubelet给的一个提示，容器的主网络接口的名字是什么。至于CNI插件是否使用eth0，会不会创建eth1，并不受限制。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">环境变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CNI_PATH</td>
<td>从什么目录寻找CNI插件，默认<pre class="crayon-plain-tag">/opt/cni/bin</pre></td>
</tr>
<tr>
<td>CNI_CONTAINERID</td>
<td>容器唯一标识</td>
</tr>
<tr>
<td>CNI_NETNS</td>
<td>所在网络命名空间</td>
</tr>
<tr>
<td>CNI_IFNAME</td>
<td>
<p>网络接口名称，通常是eth0</p>
</td>
</tr>
<tr>
<td>CNI_COMMAND</td>
<td>调用的CNI接口，例如ADD、DEL、CHECK</td>
</tr>
<tr>
<td>CNI_ARGS</td>
<td>
<p>CNI参数，格式为<pre class="crayon-plain-tag">key1=val1;key2=val2</pre></p>
<p>对于Kubelet来说，它肯定会传递以下键：</p>
<p style="padding-left: 30px;">K8S_POD_NAMESPACE  当前Pod所属命名空间<br />K8S_POD_NAME  当前Pod的名字<br />K8S_POD_INFRA_CONTAINER_ID Pod对应的基础容器的ID</p>
</td>
</tr>
<tr>
<td>NETCONFPATH</td>
<td>CNI网络配置所在目录，默认 <pre class="crayon-plain-tag">/etc/cni/net.d</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">开发CNI</span></div>
<p>要开发自己的CNI插件，应当先去阅读CNI规范、样例/参考实现。 </p>
<div class="blog_h2"><span class="graybg">cnitool</span></div>
<p>这是一个小工具，可以执行CNI配置，在已存在的网络命名空间中添加、删除网络接口。</p>
<p>cnitool会搜索<pre class="crayon-plain-tag">$NETCONFPATH</pre>下面所有的<pre class="crayon-plain-tag">*.conf</pre>或<pre class="crayon-plain-tag">*.json</pre>文件，加载它们，然后寻找网络名称和传递给cnitool匹配的网络配置。</p>
<div class="blog_h3"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">go get github.com/containernetworking/cni
go install github.com/containernetworking/cni/cnitool</pre>
<p>cnitool是参考实现的一部分，因此你可以直接构建参考实现，以获得cnitool： </p>
<pre class="crayon-plain-tag">git clone https://github.com/containernetworking/plugins.git
cd plugins
./build_linux.sh</pre>
<div class="blog_h3"><span class="graybg">使用</span></div>
<p>首先需要创建网络命名空间： </p>
<pre class="crayon-plain-tag">sudo ip netns add testing
# 自动创建/var/run/netns/testing，也就是/run/netns/testing
# 此文件指向的inode即代表网络命名空间</pre>
<p>并创建CNI网络配置：</p>
<pre class="crayon-plain-tag">{
  "cniVersion": "0.4.0",
  "name": "myptp",
  "type": "ptp",
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "172.16.29.0/24",
    "routes": [ { "dst": "0.0.0.0/0" } ]
  }
}</pre>
<p>然后调用cnitool：</p>
<pre class="crayon-plain-tag">sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing</pre>
<p>检查配置是否符合预期：</p>
<pre class="crayon-plain-tag">sudo CNI_PATH=./bin cnitool check myptp /var/run/netns/testing</pre>
<p>检查命名空间中的网络接口：</p>
<pre class="crayon-plain-tag">sudo ip -n testing addr
sudo ip netns exec testing ping -c 172.16.29.1</pre>
<p>清理：</p>
<pre class="crayon-plain-tag">sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing
sudo ip netns del testing</pre>
<div class="blog_h2"><span class="graybg">参考实现</span></div>
<p><a href="https://github.com/containernetworking/plugins">CNI项目提供了一个代码库</a>，其中包含了若干参考实现。本文后面的内容会分析其中一些CNI插件的源代码。</p>
<div class="blog_h2"><span class="graybg">运行插件</span></div>
<p>构建、安装了参考实现，或者你自己编写的CNI插件后，可以利用<a href="https://github.com/containernetworking/plugins">参考实现项目</a>的scripts/目录中的priv-net-run.sh、docker-run.sh来执行插件，以进行快速的验证。</p>
<div class="blog_h3"><span class="graybg">构建参考实现</span></div>
<pre class="crayon-plain-tag">cd $GOPATH/src/github.com/containernetworking/plugins
./build_linux.sh </pre>
<div class="blog_h3"><span class="graybg">创建网络配置</span></div>
<p>你需要为被测试的CNI插件创建网络配置：</p>
<pre class="crayon-plain-tag">{
	"cniVersion": "0.2.0",
	"name": "mynet",
	"type": "bridge",
	"bridge": "cni0",
	"isGateway": true,
	"ipMasq": true,
	"ipam": {
		"type": "host-local",
		"subnet": "10.22.0.0/16",
		"routes": [
			{ "dst": "0.0.0.0/0" }
		]
	}
}</pre><br />
<pre class="crayon-plain-tag">{
	"cniVersion": "0.2.0",
	"name": "lo",
	"type": "loopback"
}</pre>
<div class="blog_h3"><span class="graybg">执行CNI插件</span></div>
<p>调用命令：</p>
<pre class="crayon-plain-tag">CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin
cd $GOPATH/src/github.com/containernetworking/cni/scripts
sudo CNI_PATH=$CNI_PATH ./priv-net-run.sh ifconfig

# eth0      Link encap:Ethernet  HWaddr f2:c2:6f:54:b8:2b  
#           inet addr:10.22.0.2  Bcast:0.0.0.0  Mask:255.255.0.0

# lo        Link encap:Local Loopback  
#           inet addr:127.0.0.1  Mask:255.0.0.0</pre>
<p>你可以在私有网络命名空间下看到和上面两个CNI配置对应的网络接口。 </p>
<div class="blog_h3"><span class="graybg">运行Docker容器</span></div>
<p>创建并配置网络命名空间，然后在其中运行Docker容器：</p>
<pre class="crayon-plain-tag">CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin
cd $GOPATH/src/github.com/containernetworking/cni/scripts
sudo CNI_PATH=$CNI_PATH ./docker-run.sh --rm busybox:latest ifconfig</pre>
<div class="blog_h1"><span class="graybg">源码分析</span></div>
<div class="blog_h2"><span class="graybg">libcni</span></div>
<div class="blog_h3"><span class="graybg">CNI</span></div>
<p>libcni.CNI，这是CNI提供的一套接口，供容器运行时调用，实现CNI相关的操控。接口规格如下：</p>
<pre class="crayon-plain-tag">type CNI interface {
	// 将容器添加到一组网络，也就是调用一组插件
	AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
	// 检查一组网络
	CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	// 将容器从一组网络中删除
	DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	// 获取一组网络的ADD调用结果
	GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)

	// 添加、检查、删除、获取单个网络
	AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
	CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
	DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
	GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)

	// 进行配置校验
	ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
	ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}</pre>
<div class="blog_h3"><span class="graybg">NetworkConfig</span></div>
<p>NetworkConfigList、NetworkConfig就对应了规范中的网络配置列表、网络配置，JSON部分直接存放在字节数组：</p>
<pre class="crayon-plain-tag">type NetworkConfig struct {
	Network *types.NetConf
	Bytes   []byte
}

type NetworkConfigList struct {
	Name         string
	CNIVersion   string
	DisableCheck bool
	Plugins      []*NetworkConfig
	Bytes        []byte
}</pre>
<div class="blog_h3"><span class="graybg">NetConf</span></div>
<p>网络配置，转换为的结构：</p>
<pre class="crayon-plain-tag">type NetConf struct {
	CNIVersion string `json:"cniVersion,omitempty"`

	Name         string          `json:"name,omitempty"`
	Type         string          `json:"type,omitempty"`
	Capabilities map[string]bool `json:"capabilities,omitempty"`
	IPAM         IPAM            `json:"ipam,omitempty"`
	DNS          DNS             `json:"dns"`

	RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
	PrevResult    Result                 `json:"-"`
} </pre>
<div class="blog_h3"><span class="graybg">RuntimeConf</span></div>
<p>RuntimeConf则包含了一次CNI调用中，除了网络配置之外的参数信息。通常由容器运行时根据上下文来构建：</p>
<pre class="crayon-plain-tag">type RuntimeConf struct {
	// 当前针对的容器ID
	ContainerID string
	// 当前操控的网络命名空间
	NetNS       string
	// 当前网络接口名，注意，尽管会通过插件列表调用多个插件，但是这个名字是唯一的，一般是eth0
	IfName      string
	// 参数
	Args        [][2]string
	// 运行时传递给插件的Capability相关的数据
	CapabilityArgs map[string]interface{}
	// 缓存目录
	CacheDir string
}</pre>
<div class="blog_h3"><span class="graybg">current.Result</span></div>
<p>上一个CNI插件的调用结果，存放在此结构中：</p>
<pre class="crayon-plain-tag">type current.Result struct {
	CNIVersion string         `json:"cniVersion,omitempty"`
	Interfaces []*Interface   `json:"interfaces,omitempty"`
	IPs        []*IPConfig    `json:"ips,omitempty"`
	Routes     []*types.Route `json:"routes,omitempty"`
	DNS        types.DNS      `json:"dns,omitempty"`
}

// 实现了下面的接口
// Result is an interface that provides the result of plugin execution
type types.Result interface {
	// 不需要转换，该结果即可支持的最高CNI版本
	Version() string

	// 转换此Result到指定的CNI版本
	GetAsVersion(version string) (Result, error)

	// 以JSON格式打印此Result到标准输出
	Print() error

	// 以JSON格式打印此Result
	PrintTo(writer io.Writer) error

	// 返回JSON格式的Result
	String() string
} </pre>
<div class="blog_h3"><span class="graybg">dockershim调用libcni</span></div>
<p>dockershim是kubelet中的一个模块，Kubelet通过CRI gRPC调用进程内的dockershim，后者则将CRI请求适配为对Docker守护进程的请求。可以认为dockershim是一个容器运行时。</p>
<p>在创建Pod时，NetworkPlugin.SetUpPod方法会被调用，来为Pod安装网络：</p>
<pre class="crayon-plain-tag">func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, 
        id kubecontainer.ContainerID, annotations, options map[string]string) error {
	if err := plugin.checkInitialized(); err != nil {
		return err
	}
	// 获得网络命名空间
	netnsPath, err := plugin.host.GetNetNS(id.ID)
	if err != nil {
		return fmt.Errorf("CNI failed to retrieve network namespace path: %v", err)
	}

	cniTimeoutCtx, cancelFunc := context.WithTimeout(context.Background(), network.CNITimeoutSec*time.Second)
	defer cancelFunc()
	// Windows doesn't have loNetwork. It comes only with Linux
	if plugin.loNetwork != nil {
        // 对于Linux，先加入lo网络
		if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, 
                        netnsPath, annotations, options); err != nil {
			return err
		}
	}
	// 将Pod加入到默认网络                       *cniNetwork
	_, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, 
                        netnsPath, annotations, options)
	return err
}

// 表示一个网络
type cniNetwork struct {
	name          string
	// 网络配置列表
	NetworkConfig *libcni.NetworkConfigList
	// 提供CNI的接口
	CNIConfig     libcni.CNI
	Capabilities  []string
}

func (plugin *cniNetworkPlugin) addToNetwork(ctx context.Context, network *cniNetwork, 
        podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, 
        podNetnsPath string, annotations, options map[string]string) (cnitypes.Result, error) {
	// 生成RuntimeConfig
	rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, 
        annotations, options)
	if err != nil {
		klog.Errorf("Error adding network when building cni runtime conf: %v", err)
		return nil, err
	}

	pdesc := podDesc(podNamespace, podName, podSandboxID)
	netConf, cniNet := network.NetworkConfig, network.CNIConfig
	// 调用CNI接口
	res, err := cniNet.AddNetworkList(ctx, netConf, rt)
	if err != nil {
		klog.Errorf("Error adding %s to network %s/%s: %v", pdesc, netConf.Plugins[0].Network.Type, 
            netConf.Name, err)
		return nil, err
	}
	klog.V(4).Infof("Added %s to network %s: %v", pdesc, netConf.Name, res)
	return res, nil
}</pre>
<div class="blog_h3"><span class="graybg">invoke</span></div>
<p>此包提供了一些（通过命令行）调用CNI插件的函数。</p>
<p>下面这个函数，根据名字查找CNI插件，并且传递网络配置、来自环境变量的CNI参数，调用插件的ADD命令：</p>
<pre class="crayon-plain-tag">func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) {
	if os.Getenv("CNI_COMMAND") != "ADD" {
		return nil, fmt.Errorf("CNI_COMMAND is not ADD")
	}

	paths := filepath.SplitList(os.Getenv("CNI_PATH"))

	pluginPath, err := FindInPath(delegatePlugin, paths)
	if err != nil {
		return nil, err
	}

	return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv())
}</pre>
<p>ExecPluginWithResult / ExecPluginWithoutResult函数发起CNI调用：</p>
<pre class="crayon-plain-tag">// 关注结果
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
	return defaultPluginExec.WithResult(pluginPath, netconf, args)
}

// 不关心结果
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
	return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
}
// 示例
return invoke.ExecPluginWithoutResult(pluginPath, netconfBytes, &amp;invoke.Args{
	Command:       "DEL",
	ContainerID:   args.ContainerID,
	NetNS:         args.Netns,
	PluginArgsStr: args.Args,
	IfName:        ifName,
	Path:          args.Path,
})</pre>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">unitest </span></div>
<p>参考实现的测试用例基于<a href="/ginkgo-study-note">Ginkgo</a>，本节阅读loopback插件的测试用例，依此了解如何调用、测试CNI插件。</p>
<div class="blog_h3"><span class="graybg">测试套件</span></div>
<pre class="crayon-plain-tag">package main_test

import (
	"github.com/onsi/gomega/gexec"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"testing"
)

var pathToLoPlugin string

func TestLoopback(t *testing.T) {
	RegisterFailHandler(Fail)
	// 入口点，启动测试套件
	RunSpecs(t, "plugins/main/loopback")
}

var _ = BeforeSuite(func() {
	var err error
	// 使用go build来执行构建，返回临时目录中的二进制文件的路径
	pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/loopback")
	Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() {
	// 清理套件启动后构建的二进制文件
	gexec.CleanupBuildArtifacts()
})</pre>
<div class="blog_h3"><span class="graybg">测试用例</span></div>
<pre class="crayon-plain-tag">package main_test

import (
	"fmt"
	"net"
	"os/exec"
	"strings"

	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containernetworking/plugins/pkg/testutils"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
)

// 一组用例
var _ = Describe("Loopback", func() {
	var (
		networkNS ns.NetNS
		command   *exec.Cmd
		environ   []string
	)
	// 准备工作
	BeforeEach(func() {
		// 插件命令
		command = exec.Command(pathToLoPlugin)
		var err error
		// 通过unshare系统调用创建新NS，但是不会切换到此NS
		networkNS, err = testutils.NewNS()
		Expect(err).NotTo(HaveOccurred())

		// 准备调用CNI插件需要的环境变量
		environ = []string{
			fmt.Sprintf("CNI_CONTAINERID=%s", "dummy"),
			// CNI插件需要在哪个命名空间中运作
			fmt.Sprintf("CNI_NETNS=%s", networkNS.Path()),
			fmt.Sprintf("CNI_IFNAME=%s", "this is ignored"),
			fmt.Sprintf("CNI_ARGS=%s", "none"),
			fmt.Sprintf("CNI_PATH=%s", "/some/test/path"),
		}
		// 准备插件输入参数
		command.Stdin = strings.NewReader(`{ "name": "loopback-test", "cniVersion": "0.1.0" }`)
	})

	// 清理工作，关闭命名空间描述符、卸载命名空间
	AfterEach(func() {
		Expect(networkNS.Close()).To(Succeed())
		Expect(testutils.UnmountNS(networkNS)).To(Succeed())
	})

	Context("when given a network namespace", func() {
		It("sets the lo device to UP", func() {
			// 调用ADD接口
			command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "ADD"))

			session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			// 对命令的标准输出进行断言
			Eventually(session).Should(gbytes.Say(`{.*}`))
			Eventually(session).Should(gexec.Exit(0))

			var lo *net.Interface
			
			err = networkNS.Do(func(ns.NetNS) error {
				// 如果CNI插件运行成功，则在此NS中会出现一个lo网卡
				var err error
				lo, err = net.InterfaceByName("lo")
				return err
			})
			Expect(err).NotTo(HaveOccurred())

			Expect(lo.Flags &amp; net.FlagUp).To(Equal(net.FlagUp))
		})

		It("sets the lo device to DOWN", func() {
			// 调用DEL接口
			command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "DEL"))

			session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
			Expect(err).NotTo(HaveOccurred())

			Eventually(session).Should(gbytes.Say(``))
			Eventually(session).Should(gexec.Exit(0))

			var lo *net.Interface
			err = networkNS.Do(func(ns.NetNS) error {
				// 如果CNI插件运行成功，则在此NS中不存在lo网卡
				var err error
				lo, err = net.InterfaceByName("lo")
				return err
			})
			Expect(err).NotTo(HaveOccurred())

			Expect(lo.Flags &amp; net.FlagUp).NotTo(Equal(net.FlagUp))
		})
	})
})</pre>
<div class="blog_h2"><span class="graybg">skel</span></div>
<p>这个包提供了CNI插件开发的骨架代码，它实现了参数解析和校验，你可以将其作为库使用，简化CNI开发。</p>
<pre class="crayon-plain-tag">package skel

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"strings"

	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/version"
)

// 来自环境变量、标准输入的CNI参数
type CmdArgs struct {
	ContainerID string
	Netns       string
	IfName      string
	Args        string
	Path        string
	StdinData   []byte
}

// 解析并得到CmdArgs、执行CNI调用
type dispatcher struct {
	Getenv func(string) string
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	ConfVersionDecoder version.ConfigDecoder
	VersionReconciler  version.Reconciler
}

type reqForCmdEntry map[string]bool

// internal only error to indicate lack of required environment variables
type missingEnvError struct {
	msg string
}

func (e missingEnvError) Error() string {
	return e.msg
}

// 从环境变量解析参数
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
	var cmd, contID, netns, ifName, args, path string

	vars := []struct {
		name      string
		val       *string
		reqForCmd reqForCmdEntry
	}{
		{
			"CNI_COMMAND", // 接口名
			&amp;cmd,
			reqForCmdEntry{
				"ADD":   true,
				"CHECK": true,
				"DEL":   true,
			},
		},
		{
			"CNI_CONTAINERID", // 容器ID
			&amp;contID,
			reqForCmdEntry{
				"ADD":   true,
				"CHECK": true,
				"DEL":   true,
			},
		},
		{
			"CNI_NETNS", // 网络NS文件路径
			&amp;netns,
			reqForCmdEntry{
				"ADD":   true,
				"CHECK": true,
				"DEL":   false,
			},
		},
		{
			"CNI_IFNAME", // 网络接口名
			&amp;ifName,
			reqForCmdEntry{
				"ADD":   true,
				"CHECK": true,
				"DEL":   true,
			},
		},
		{
			"CNI_ARGS", // CNI参数
			&amp;args,
			reqForCmdEntry{
				"ADD":   false,
				"CHECK": false,
				"DEL":   false,
			},
		},
		{
			"CNI_PATH", // CNI二进制文件路径
			&amp;path,
			reqForCmdEntry{
				"ADD":   true,
				"CHECK": true,
				"DEL":   true,
			},
		},
	}

	argsMissing := make([]string, 0)
	for _, v := range vars {
		*v.val = t.Getenv(v.name)
		if *v.val == "" {
			if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
				argsMissing = append(argsMissing, v.name)
			}
		}
	}

	if len(argsMissing) &gt; 0 {
		joined := strings.Join(argsMissing, ",")
		return "", nil, missingEnvError{fmt.Sprintf("required env variables [%s] missing", joined)}
	}

	if cmd == "VERSION" {
		t.Stdin = bytes.NewReader(nil)
	}

	stdinData, err := ioutil.ReadAll(t.Stdin)
	if err != nil {
		return "", nil, fmt.Errorf("error reading from stdin: %v", err)
	}

	cmdArgs := &amp;CmdArgs{
		ContainerID: contID,
		Netns:       netns,
		IfName:      ifName,
		Args:        args,
		Path:        path,
		StdinData:   stdinData,
	}
	return cmd, cmdArgs, nil
}

func createTypedError(f string, args ...interface{}) *types.Error {
	return &amp;types.Error{
		Code: 100,
		Msg:  fmt.Sprintf(f, args...),
	}
}

// 检查CNI并执行调用
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo 
        version.PluginInfo, toCall func(*CmdArgs) error) error {
	configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
	if err != nil {
		return err
	}
	verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
	if verErr != nil {
		return &amp;types.Error{
			Code:    types.ErrIncompatibleCNIVersion,
			Msg:     "incompatible CNI versions",
			Details: verErr.Details(),
		}
	}

	return toCall(cmdArgs)
}

// 校验配置
func validateConfig(jsonBytes []byte) error {
	var conf struct {
		Name string `json:"name"`
	}
	if err := json.Unmarshal(jsonBytes, &amp;conf); err != nil {
		return fmt.Errorf("error reading network config: %s", err)
	}
	if conf.Name == "" {
		return fmt.Errorf("missing network name")
	}
	return nil
}

// CNI调用的核心逻辑
func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, 
        versionInfo version.PluginInfo, about string) *types.Error {
	// 解析命令和参数
	cmd, cmdArgs, err := t.getCmdArgsFromEnv()
	if err != nil {
		// 如果没有指定命令，则打印About后退出
		if _, ok := err.(missingEnvError); ok &amp;&amp; t.Getenv("CNI_COMMAND") == "" &amp;&amp; about != "" {
			fmt.Fprintln(t.Stderr, about)
			return nil
		}
		return createTypedError(err.Error())
	}
	// 进行配置（标准输入）校验
	if cmd != "VERSION" {
		err = validateConfig(cmdArgs.StdinData)
		if err != nil {
			return createTypedError(err.Error())
		}
	}

	switch cmd {
	case "ADD":
		// ADD接口，直接转调函数
		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
	case "CHECK":
		// CHECK接口
		// 检查版本
		configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
		if err != nil {
			return createTypedError(err.Error())
		}
		if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
			return createTypedError(err.Error())
		} else if !gtet {
			return &amp;types.Error{
				Code: types.ErrIncompatibleCNIVersion,
				Msg:  "config version does not allow CHECK",
			}
		}
		for _, pluginVersion := range versionInfo.SupportedVersions() {
			gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
			if err != nil {
				return createTypedError(err.Error())
			} else if gtet {
				// 转调函数
				if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil {
					return createTypedError(err.Error())
				}
				return nil
			}
		}
		return &amp;types.Error{
			Code: types.ErrIncompatibleCNIVersion,
			Msg:  "plugin version does not allow CHECK",
		}
	case "DEL":
		// DEL接口，直接转调函数
		err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
	case "VERSION":
		// 打印版本信息
		err = versionInfo.Encode(t.Stdout)
	default:
		return createTypedError("unknown CNI_COMMAND: %v", cmd)
	}

	if err != nil {
		if e, ok := err.(*types.Error); ok {
			// don't wrap Error in Error
			return e
		}
		return createTypedError(err.Error())
	}
	return nil
}

// PluginMainWithError是一个CNI的核心main逻辑骨架，调用后产生的错误需要你自行处理
// 实际上就是创建dispatcher，并调用它的pluginMain方法
func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, 
        versionInfo version.PluginInfo, about string) *types.Error {
	return (&amp;dispatcher{
		Getenv: os.Getenv,
		Stdin:  os.Stdin,
		Stdout: os.Stdout,
		Stderr: os.Stderr,
	}).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about)
}

// 此函数是一个CNI的核心main逻辑骨架，支持自动化的错误处理
//
// 调用者（CNI插件）需要指明它支持哪些CNI规范版本
//
// 如果没有指定CNI_COMMAND，则调用者提供的about打印到stderr
// about的推荐格式： CNI plugin &lt;foo&gt; v&lt;version&gt;
//
// cmdAdd, cmdCheck或cmdDel调用出错，则PluginMain打印错误JSON到stdout，并os.Exit(1)
//
// 如果需要定制错误处理，调用PluginMainWithError()
func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo 
        version.PluginInfo, about string) {
	if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil {
		if err := e.Print(); err != nil {
			log.Print("Error writing error JSON to stdout: ", err)
		}
		os.Exit(1)
	}
}</pre>
<p>有了skel，我们实现自己的CNI插件时，只需要编写几个函数就可以了。 </p>
<div class="blog_h2"><span class="graybg">loopback </span></div>
<p>这个插件很简单，就是在命名空间中添加一个lo接口：</p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net"

	"github.com/vishvananda/netlink"

	"github.com/containernetworking/cni/pkg/skel"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/types/current"
	"github.com/containernetworking/cni/pkg/version"

	"github.com/containernetworking/plugins/pkg/ns"
	bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
// 从标准输入的JSON解析CNI配置
func parseNetConf(bytes []byte) (*types.NetConf, error) {
	conf := &amp;types.NetConf{}
	if err := json.Unmarshal(bytes, conf); err != nil {
		return nil, fmt.Errorf("failed to parse network config: %v", err)
	}
	// 可能存在前序被调用插件
	if conf.RawPrevResult != nil {
		// 前序调用结果是存放在网络配置中的
		if err := version.ParsePrevResult(conf); err != nil {
			return nil, fmt.Errorf("failed to parse prevResult: %v", err)
		}
		// 尝试解析前序结果，失败则出错
		if _, err := current.NewResultFromResult(conf.PrevResult); err != nil {
			return nil, fmt.Errorf("failed to convert result to current version: %v", err)
		}
	}

	return conf, nil
}

func cmdAdd(args *skel.CmdArgs) error {
	// 解析出配置
	conf, err := parseNetConf(args.StdinData)
	if err != nil {
		return err
	}

	var v4Addr, v6Addr *net.IPNet
	// 强制使用lo作为网络接口名称，忽略配置
	args.IfName = "lo"

	// 在路径args.Netns代表的网络命名空间下执行
	err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
		// 获取Link对象，lo接口不需要添加，默认就存在，只是可能没有up
		link, err := netlink.LinkByName(args.IfName)
		if err != nil {
			return err // not tested
		}
		// 设置网络接口为UP状态
		err = netlink.LinkSetUp(link)
		if err != nil {
			return err // not tested
		}

		// 获得网络接口的IPv4地址接口
		v4Addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
		if err != nil {
			return err // not tested
		}
		// 如果有IPv4的地址，则确认是loopback地址
		if len(v4Addrs) != 0 {
			v4Addr = v4Addrs[0].IPNet
			// sanity check that this is a loopback address
			for _, addr := range v4Addrs {
				if !addr.IP.IsLoopback() {
					return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
				}
			}
		}
		// 获得网络接口的IPv6地址接口
		// 如果有IPv6的地址，则确认是loopback地址
		v6Addrs, err := netlink.AddrList(link, netlink.FAMILY_V6)
		if err != nil {
			return err // not tested
		}
		if len(v6Addrs) != 0 {
			v6Addr = v6Addrs[0].IPNet
			// sanity check that this is a loopback address
			for _, addr := range v6Addrs {
				if !addr.IP.IsLoopback() {
					return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
				}
			}
		}

		return nil
	})
	if err != nil {
		return err // not tested
	}

	var result types.Result
	// 如果有来自上一个CNI插件的结果，则透明的传递下去
	if conf.PrevResult != nil {
		// If loopback has previous result which passes from previous CNI plugin,
		// loopback should pass it transparently
		result = conf.PrevResult
	} else {
		// 否则，构建一个结果，作为PrevResult
		loopbackInterface := &amp;current.Interface{
			Name: args.IfName,
			// lo不关心L2地址
			Mac: "00:00:00:00:00:00", 
			Sandbox: args.Netns,
		}
		// 将本次添加的IP地址纳入结果
		r := &amp;current.Result{
			CNIVersion: conf.CNIVersion, 
			Interfaces: []*current.Interface{ loopbackInterface },
		}

		if v4Addr != nil {
			r.IPs = append(r.IPs, &amp;current.IPConfig{
				Version:   "4",
				Interface: current.Int(0),
				Address:   *v4Addr,
			})
		}

		if v6Addr != nil {
			r.IPs = append(r.IPs, &amp;current.IPConfig{
				Version:   "6",
				Interface: current.Int(0),
				Address:   *v6Addr,
			})
		}

		result = r
	}

	return types.PrintResult(result, conf.CNIVersion)
}

// 只是把lo关闭，不能删除
func cmdDel(args *skel.CmdArgs) error {
	if args.Netns == "" {
		return nil
	}
	args.IfName = "lo" // ignore config, this only works for loopback
	err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
		link, err := netlink.LinkByName(args.IfName)
		if err != nil {
			return err // not tested
		}

		err = netlink.LinkSetDown(link)
		if err != nil {
			return err // not tested
		}

		return nil
	})
	if err != nil {
		return err // not tested
	}

	return nil
}

// 检查lo接口是否启动
func cmdCheck(args *skel.CmdArgs) error {
	args.IfName = "lo" // ignore config, this only works for loopback

	return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
		link, err := netlink.LinkByName(args.IfName)
		if err != nil {
			return err
		}
		// link属性标记，位域
		if link.Attrs().Flags&amp;net.FlagUp != net.FlagUp {
			return errors.New("loopback interface is down")
		}

		return nil
	})
}



func main() {
	// 调用skel
	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback"))
}</pre>
<div class="blog_h2"><span class="graybg">bridge</span></div>
<div class="blog_h3"><span class="graybg">简介 </span></div>
<p>使用该插件，同一主机上的所有容器被连接到一个网桥上，<span style="background-color: #c0c0c0;">网桥位于宿主机的网络命名空间中</span>。一个veth对，一端接着网桥，另一端接着容器。<span style="background-color: #c0c0c0;">IP地址仅仅在veth对连接容器的那一端分配</span>。 </p>
<p><span style="background-color: #c0c0c0;">网桥自身也可以分配IP地址，这样它可以作为所有容器的网关</span>。如果<span style="background-color: #c0c0c0;">不分配，那么网桥工作在<strong>纯L2模式</strong></span>，如果容器有对外访问需求（也就是不仅仅需要和本机其它容器通信），则<span style="background-color: #c0c0c0;">网桥需要<strong>桥接到宿主机的某个网络接口</strong></span>。</p>
<p>使用此插件时，需要指定网桥的名字。示例配置：</p>
<pre class="crayon-plain-tag">{
    "cniVersion": "0.3.1",
    "name": "mynet",
    "type": "bridge",
    // 使用或创建的网桥的名字，默认cni0
    "bridge": "mynet0",
    // 如果为true则为网桥分配IP地址，默认false
    "isGateway": true,
    // isGateway为true的前提下，如果设置为true，则设置网桥IP为命名空间的默认路由
    "isDefaultGateway": true,
    // 是否需要为网桥设置新的IP地址（如果地址变化），默认false
    "forceAddress": false,
    // 在宿主机上，为来自此容器，目标是主机之外的流量设置IP遮掩（SNAT），默认值由内核选择
    "ipMasq": true,
    // 为网桥上的接口设置hairpin模式，默认false
    // 默认情况下，网桥不会将从端口A发来的封包，再发回A去，这会导致容器无法访问自己
    "hairpinMode": true,
    // 使用的IPAM插件
    "ipam": {
        "type": "host-local",
        "subnet": "10.10.0.0/16"
    }
    // 将网桥设置为混杂模式，默认false
    // 混杂模式下，网络接口监听任何封包，不管它的目的地（MAC）是不是自己
    promiscMode: false
    // 分配VLAN tag，默认不分配
    // 如果指定，则在veth对的宿主机端设置VLAN tag，并且启用网桥的vlan_filtering特性
    vlan: 0
}</pre>
<p>纯L2模式配置：</p>
<pre class="crayon-plain-tag">{
    "cniVersion": "0.3.1",
    "name": "mynet",
    "type": "bridge",
    "bridge": "mynet0",
    "ipam": {}
}</pre>
<div class="blog_h3"><span class="graybg">核心代码</span></div>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"runtime"
	"syscall"
	"time"

	"github.com/j-keck/arping"
	"github.com/vishvananda/netlink"

	"github.com/containernetworking/cni/pkg/skel"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/types/current"
	"github.com/containernetworking/cni/pkg/version"
	"github.com/containernetworking/plugins/pkg/ip"
	"github.com/containernetworking/plugins/pkg/ipam"
	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containernetworking/plugins/pkg/utils"
	bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
	"github.com/containernetworking/plugins/pkg/utils/sysctl"
)

// For testcases to force an error after IPAM has been performed
var debugPostIPAMError error

const defaultBrName = "cni0"

type NetConf struct {
	types.NetConf
	// 在标准NetConf之上新增的字段
	BrName       string `json:"bridge"`
	IsGW         bool   `json:"isGateway"`
	IsDefaultGW  bool   `json:"isDefaultGateway"`
	ForceAddress bool   `json:"forceAddress"`
	IPMasq       bool   `json:"ipMasq"`
	MTU          int    `json:"mtu"`
	HairpinMode  bool   `json:"hairpinMode"`
	PromiscMode  bool   `json:"promiscMode"`
	Vlan         int    `json:"vlan"`
}


// 网关信息
type gwInfo struct {
	// IP地址和子网掩码
	gws               []net.IPNet
	// IPv4还是IPv6
	family            int
	// 在网关列表中，到此网关为止，是否发现默认路由
	defaultRouteFound bool
}

func init() {
	// 确保main函数在主线程（线程组Leader）上运行
	// 命名空间操作（unshare、setns）为单个线程设计，我们需要确保main函数
	// 不会跳转到其它OS线程上
	runtime.LockOSThread()
}

func loadNetConf(bytes []byte) (*NetConf, string, error) {
	n := &amp;NetConf{
		BrName: defaultBrName,
	}
	if err := json.Unmarshal(bytes, n); err != nil {
		return nil, "", fmt.Errorf("failed to load netconf: %v", err)
	}
	if n.Vlan &lt; 0 || n.Vlan &gt; 4094 {
		return nil, "", fmt.Errorf("invalid VLAN ID %d (must be between 0 and 4094)", n.Vlan)
	}
	return n, n.CNIVersion, nil
}

// 处理IPAM插件的结果，为每个地址族：
// 1. 计算、编译网关地址列表
// 2. 如果需要，添加一个默认路由
func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) {

	gwsV4 := &amp;gwInfo{}
	gwsV6 := &amp;gwInfo{}

	// 便利IPAM返回的每一个IPConfig
	for _, ipc := range result.IPs {

		// Determine if this config is IPv4 or IPv6
		var gws *gwInfo
		// 代表一个IP网络信息（网络号+掩码）
		defaultNet := &amp;net.IPNet{}
		switch {
		case ipc.Address.IP.To4() != nil:
			gws = gwsV4
			// 地址族信息
			gws.family = netlink.FAMILY_V4
			defaultNet.IP = net.IPv4zero
		case len(ipc.Address.IP) == net.IPv6len:
			gws = gwsV6
			gws.family = netlink.FAMILY_V6
			defaultNet.IP = net.IPv6zero
		default:
			return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc)
		}
		// 计算掩码
		defaultNet.Mask = net.IPMask(defaultNet.IP)

		// 这里仅仅是为了获得*int，以便后续设置网络接口索引
		ipc.Interface = current.Int(2)

		// 如果IPAM没有提供网关，则计算出网关地址
		if ipc.Gateway == nil &amp;&amp; n.IsGW {
			// 使用网络的第1个地址作为网关
			ipc.Gateway = calcGatewayIP(&amp;ipc.Address)
		}

		// 如有必要（此网桥作为默认路由），为当前地址族添加默认路由（使用当前网关地址）
		if n.IsDefaultGW &amp;&amp; !gws.defaultRouteFound {
			for _, route := range result.Routes {
				if route.GW != nil &amp;&amp; defaultNet.String() == route.Dst.String() {
					gws.defaultRouteFound = true
					break
				}
			}
			if !gws.defaultRouteFound {
				// 添加一条默认路由，可能使用计算出的网关地址
				result.Routes = append(
					result.Routes,
					&amp;types.Route{Dst: *defaultNet, GW: ipc.Gateway},
				)
				gws.defaultRouteFound = true
			}
		}

		// 如果当前网络作为网关，则加入到网关列表
		if n.IsGW {
			gw := net.IPNet{
				IP:   ipc.Gateway,
				Mask: ipc.Address.Mask,
			}
			gws.gws = append(gws.gws, gw)
		}
	}
	return gwsV4, gwsV6, nil
}

// 确保某个（IPAM分配的）IP地址被设置到网桥上
func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool) error {
	// 获得网络接口现有地址列表
	addrs, err := netlink.AddrList(br, family)
	if err != nil &amp;&amp; err != syscall.ENOENT {
		return fmt.Errorf("could not get list of IP addresses: %v", err)
	}

	// 需要被设置的IP地址
	ipnStr := ipn.String()
	for _, a := range addrs {

		// 已经被设置了，不做操作
		if a.IPNet.String() == ipnStr {
			return nil
		}

		// 如果对应的子网不重叠，多个IPv6地址可以存在于网桥上
		// 对于IPv4，或者子网重叠的Ipv6，如果forceAddress为true，重新配置IP地址
		if family == netlink.FAMILY_V4 || a.IPNet.Contains(ipn.IP) || ipn.Contains(a.IPNet.IP) {
			if forceAddress {
				// 删除现有IP地址
				if err = deleteAddr(br, a.IPNet); err != nil {
					return err
				}
			} else {
				return fmt.Errorf("%q already has an IP address different from %v", br.Attrs().Name, ipnStr)
			}
		}
	}

	// 为网桥添加新IP地址
	addr := &amp;netlink.Addr{IPNet: ipn, Label: ""}
	if err := netlink.AddrAdd(br, addr); err != nil &amp;&amp; err != syscall.EEXIST {
		return fmt.Errorf("could not add IP address to %q: %v", br.Attrs().Name, err)
	}

	// 将网桥的MAC地址设置给自己
	// otherwise, the bridge will take the
	// lowest-numbered mac on the bridge, and will change as ifs churn
	if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil {
		return fmt.Errorf("could not set bridge's mac: %v", err)
	}

	return nil
}

// 将IP地址从网络接口上删除
func deleteAddr(br netlink.Link, ipn *net.IPNet) error {
	addr := &amp;netlink.Addr{IPNet: ipn, Label: ""}

	if err := netlink.AddrDel(br, addr); err != nil {
		return fmt.Errorf("could not remove IP address from %q: %v", br.Attrs().Name, err)
	}

	return nil
}

// 根据名称获取网桥
func bridgeByName(name string) (*netlink.Bridge, error) {
	l, err := netlink.LinkByName(name)
	if err != nil {
		return nil, fmt.Errorf("could not lookup %q: %v", name, err)
	}
	// 通过CAST判断网络接口是不是网桥
	br, ok := l.(*netlink.Bridge)
	if !ok {
		return nil, fmt.Errorf("%q already exists but is not a bridge", name)
	}
	return br, nil
}

// 确保网桥存在、并且已经配置好
func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
	// 网桥对象
	br := &amp;netlink.Bridge{
		LinkAttrs: netlink.LinkAttrs{
			Name: brName,
			MTU:  mtu,
			// 让内核使用默认的发送队列长度（txqueuelen）; leaving it unset
			// 如果不设置 ，则为0，也就是使用0长的发送队列长度，这会导致FIFO流量塑形器（traffic shapers）
			// 不能正常工作，因为它使用发送队列长度作为默认的封包限制
			TxQLen: -1,
		},
	}
	if vlanFiltering {
		// 启用VLAN过滤，使用一个网桥即可管理所有VLAN，而不是为每个VLAN创建独立网桥，3.8开始支持
		br.VlanFiltering = &amp;vlanFiltering
	}

	// 添加网桥
	err := netlink.LinkAdd(br)
	if err != nil &amp;&amp; err != syscall.EEXIST {
		return nil, fmt.Errorf("could not add %q: %v", brName, err)
	}

	// 设置混杂模式
	if promiscMode {
		if err := netlink.SetPromiscOn(br); err != nil {
			return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err)
		}
	}

	// 重新读取网桥配置
	br, err = bridgeByName(brName)
	if err != nil {
		return nil, err
	}

	// 不接受IPv6路由通告，自己管理关于此接口的路由
	_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", brName), "0")

	// 启动网桥
	if err := netlink.LinkSetUp(br); err != nil {
		return nil, err
	}

	return br, nil
}

// 确保VLAN接口就绪
func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) {
	name := fmt.Sprintf("%s.%d", br.Name, vlanId)
	// 检查VLAN接口是否已经存在
	brGatewayVeth, err := netlink.LinkByName(name)
	if err != nil {
		if err.Error() != "Link not found" { // 根据字符串判断靠谱么？
			return nil, fmt.Errorf("failed to find interface %q: %v", name, err)
		}
		hostNS, err := ns.GetCurrentNS()
		if err != nil {
			return nil, fmt.Errorf("faild to find host namespace: %v", err)
		}

		// 使用veth将网桥和VLAN接口连接起来（和容器网络接口一样）
		// 创建veth对，一端连在网桥，另外一端连接在hostNS的VLAN网络接口
		_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId)
		if err != nil {
			return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err)
		}
		// 重新查找，确保OK
		brGatewayVeth, err = netlink.LinkByName(brGatewayIface.Name)
		if err != nil {
			return nil, fmt.Errorf("failed to lookup %q: %v", brGatewayIface.Name, err)
		}
	}

	return brGatewayVeth, nil
}

// 创建veth对
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, 
        hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) {
	contIface := &amp;current.Interface{}
	hostIface := &amp;current.Interface{}
	//     容器网络命名空间
	err := netns.Do(func(hostNS ns.NetNS) error {
		// 创建veth对，在容器网络命名空间中调用，会自动将对端移动到hostNS
		hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
		if err != nil {
			return err
		}
		// 从veth收集容器端网络接口的名字、MAC地址
		contIface.Name = containerVeth.Name
		contIface.Mac = containerVeth.HardwareAddr.String()
		contIface.Sandbox = netns.Path()
		hostIface.Name = hostVeth.Name
		return nil
	})
	if err != nil {
		return nil, nil, err
	}

	// 由于移动到了根网络命名空间，hostVeth的index已经变了，需要重新获取网络接口对象
	hostVeth, err := netlink.LinkByName(hostIface.Name)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
	}
	// 设置宿主端的veth的L2地址。如果这个地址不正确，则无法进行L2通信，这个veth是容器和外部的桥梁
	hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()

	// 将hostVeth对接到网桥上
	if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
		return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", 
                hostVeth.Attrs().Name, br.Attrs().Name, err)
	}

	// 为宿主端的veth设置Hairpin模式
	if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
		return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
	}

	// 如果指定了VLAN，则在网桥上（实际上是veth的宿主机端，桥的一个端口）添加VLAN filter entry
	if vlanID != 0 {
	    // bridge vlan add dev DEV vid VID [ pvid ] [ untagged ] [ self ] [ master ]
		err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err)
		}
	}

	return hostIface, contIface, nil
}

// 网关IP，简单的+1
func calcGatewayIP(ipn *net.IPNet) net.IP {
	nid := ipn.IP.Mask(ipn.Mask)
	return ip.NextIP(nid)
}

// 创建网桥
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
	vlanFiltering := false
	if n.Vlan != 0 {
		vlanFiltering = true
	}
	// 如果需要，创建并配置网桥
	br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
	}

	return br, &amp;current.Interface{
		Name: br.Attrs().Name,
		Mac:  br.Attrs().HardwareAddr.String(),
	}, nil
}

// 禁用IP6的地址冲突检测（Duplicate Address Detection，DAD）
// 禁用的原因是Hairpin模式会导致网络接口看到自己的DAD封包
func disableIPV6DAD(ifName string) error {
	// ehanced_dad sends a nonce with the DAD packets, so that we can safely
	// ignore ourselves
	enh, err := ioutil.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/enhanced_dad", ifName))
	if err == nil &amp;&amp; string(enh) == "1\n" {
		return nil
	}
	f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName)
	return ioutil.WriteFile(f, []byte("0"), 0644)
}

// 启用IP转发
func enableIPForward(family int) error {
	if family == netlink.FAMILY_V4 {
		// 启用IPv4转发
		return ip.EnableIP4Forward()
	}
	// 启用IPv6转发
	return ip.EnableIP6Forward()
}

func cmdAdd(args *skel.CmdArgs) error {
	var success bool = false

	// 解析配置
	n, cniVersion, err := loadNetConf(args.StdinData)
	if err != nil {
		return err
	}

	if n.IsDefaultGW {
		n.IsGW = true
	}
	// 不支持同时设置Hairpin和Promisc模式
	if n.HairpinMode &amp;&amp; n.PromiscMode {
		return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.")
	}

	// 如有必要，创建网桥，并进行配置
	br, brInterface, err := setupBridge(n)
	if err != nil {
		return err
	}

	// 当前处理的容器网络命名空间
	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	defer netns.Close()

	// 为容器网络命名空间创建VETH对
	// 创建VETH对，一端插入容器，成为containerInterface
	// 另外一端插到网桥上，即hostInterface
	hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, 
            n.MTU, n.HairpinMode, n.Vlan)
	if err != nil {
		return err
	}

	// 返回接口列表：网桥、宿主机端veth、容器命名空间内eth
	result := &amp;current.Result{
		CNIVersion: cniVersion, 
		Interfaces: []*current.Interface{
			brInterface, hostInterface, containerInterface,
		},
	}

	isLayer3 := n.IPAM.Type != ""

	// 如果是L3模式，向IPAM申请IP地址
	if isLayer3 {
		// 调用IPAM插件，传递自己的配置（标准输入）给它
		r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
		if err != nil {
			return err
		}

		// 如果调用失败，释放掉可能已经分配的IP地址
		defer func() {
			if !success {
				ipam.ExecDel(n.IPAM.Type, args.StdinData)
			}
		}()

		// 转换Result为当前CNI版本
		ipamResult, err := current.NewResultFromResult(r)
		if err != nil {
			return err
		}
		// 获取IPAM分配的地址、路由
		result.IPs = ipamResult.IPs
		result.Routes = ipamResult.Routes

		if len(result.IPs) == 0 {
			return errors.New("IPAM plugin returned missing IP config")
		}

		// 为每个地址族收集路由信息
		gwsV4, gwsV6, err := calcGateways(result, n)
		if err != nil {
			return err
		}

		// 在容器网络命名空间配置IP地址和路由
		if err := netns.Do(func(_ ns.NetNS) error {
			// 为了应对网桥开启hairpin的情况，禁用DAD
			// Hairpin会导致回响（echo）neighbor solicitation packets, 进而导致DAD错误
			for _, ipc := range result.IPs {
				if ipc.Version == "6" &amp;&amp; (n.HairpinMode || n.PromiscMode) {
					// 禁用IPv6 Duplicate Address Detection
					if err := disableIPV6DAD(args.IfName); err != nil {
						return err
					}
					break
				}
			}

			// 使用IPAM的调用结果来分配IP地址给容器接口，并且添加路由
			if err := ipam.ConfigureIface(args.IfName, result); err != nil {
				return err
			}
			return nil
		}); err != nil {
			return err
		}

		// 检查网桥Port状态
		retries := []int{0, 50, 500, 1000, 1000}
		for idx, sleep := range retries {
			time.Sleep(time.Duration(sleep) * time.Millisecond)
			// 宿主机端（连接在网桥上）的VETH端
			hostVeth, err := netlink.LinkByName(hostInterface.Name)
			if err != nil {
				return err
			}
			// 应当处于Up状态（可以发封包了）
			if hostVeth.Attrs().OperState == netlink.OperUp {
				break
			}

			if idx == len(retries)-1 {
				return fmt.Errorf("bridge port in error state: %s", hostVeth.Attrs().OperState)
			}
		}

		// 发送无故ARP（也叫免费ARP）请求（gratuitous arp） —— 请求容器接口自身IP地址对应
		// 的MAC，价值是：
		//   1. 验证IP是否冲突，如果此ARP收到应答，则提示网络中有人用了和自己相同的IP
		//   2. 如果当前接口改变了MAC地址，可以通知网络中其它主机及时更新ARP缓存
		if err := netns.Do(func(_ ns.NetNS) error {
			contVeth, err := net.InterfaceByName(args.IfName)
			if err != nil {
				return err
			}

			for _, ipc := range result.IPs {
				if ipc.Version == "4" {
					// 发送免费ARP
					_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
				}
			}
			return nil
		}); err != nil {
			return err
		}

		// （此网桥作为容器的一个）网关模式下，为网桥配置IP地址，并启用IP转发
		if n.IsGW {
			var firstV4Addr net.IP
			var vlanInterface *current.Interface
			// 遍历IPAM分配的网关信息
			for _, gws := range []*gwInfo{gwsV4, gwsV6} {
				for _, gw := range gws.gws {
					if gw.IP.To4() != nil &amp;&amp; firstV4Addr == nil {
						firstV4Addr = gw.IP
					}
					if n.Vlan != 0 {
						// 如有必要，创建名为brname.vlanid的、网桥的VLAN子接口
						vlanIface, err := ensureVlanInterface(br, n.Vlan)
						if err != nil {
							return fmt.Errorf("failed to create vlan interface: %v", err)
						}

						if vlanInterface == nil {
							vlanInterface = &amp;current.Interface{Name: vlanIface.Attrs().Name,
								Mac: vlanIface.Attrs().HardwareAddr.String()}
							// VLAN子接口添加到结果中
							result.Interfaces = append(result.Interfaces, vlanInterface)
						}
						// 将网关IP地址分配给网桥的VLAN子接口
						err = ensureAddr(vlanIface, gws.family, &amp;gw, n.ForceAddress)
						if err != nil {
							return fmt.Errorf("failed to set vlan interface for bridge with addr: %v", err)
						}
					} else {
						// 将IP分配给网桥
						err = ensureAddr(br, gws.family, &amp;gw, n.ForceAddress)
						if err != nil {
							return fmt.Errorf("failed to set bridge addr: %v", err)
						}
					}
				}

				if gws.gws != nil {
					// 启用宿主机的IP转发功能
					if err = enableIPForward(gws.family); err != nil {
						return fmt.Errorf("failed to enable forwarding: %v", err)
					}
				}
			}
		}

		if n.IPMasq {
			// 设置iptables规则，以实现MASQ
			// 生成一个自定义链
			chain := utils.FormatChainName(n.Name, args.ContainerID)
			comment := utils.FormatComment(n.Name, args.ContainerID)
			for _, ipc := range result.IPs {
				// 安装MASQ到chain，此china将位于NAT表作为POSTROUTING的子链
				if err = ip.SetupIPMasq(&amp;ipc.Address, chain, comment); err != nil {
					return err
				}
			}
		}
	}

	// 在第一个VETH添加之后，或者设置IP地址之后，网桥的MAC可能变化
	// 因此重新获取网桥
	br, err = bridgeByName(n.BrName)
	if err != nil {
		return err
	}
	brInterface.Mac = br.Attrs().HardwareAddr.String()

	result.DNS = n.DNS

	// Return an error requested by testcases, if any
	if debugPostIPAMError != nil {
		return debugPostIPAMError
	}

	success = true

	return types.PrintResult(result, cniVersion)
}

func cmdDel(args *skel.CmdArgs) error {
	n, _, err := loadNetConf(args.StdinData)
	if err != nil {
		return err
	}

	isLayer3 := n.IPAM.Type != ""

	if isLayer3 {
		// 调用IPAM插件，回收IP地址
		if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
			return err
		}
	}

	if args.Netns == "" {
		return nil
	}

	// 网络命名空间存在，尝试清理其中的网络接口，由于可能调用多次
	// 因此需要实现幂等操作，如果网络接口已经被移除过，不要返回错误
	var ipnets []*net.IPNet
	err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
		var err error
		// 删除容器网络接口，导致VETH对被删除
		ipnets, err = ip.DelLinkByNameAddr(args.IfName)
		if err != nil &amp;&amp; err == ip.ErrLinkNotFound {
			return nil
		}
		return err
	})

	if err != nil {
		return err
	}

	// 清理MASQ规则
	if isLayer3 &amp;&amp; n.IPMasq {
		chain := utils.FormatChainName(n.Name, args.ContainerID)
		comment := utils.FormatComment(n.Name, args.ContainerID)
		for _, ipn := range ipnets {
			if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil {
				return err
			}
		}
	}

	return err
}

// 这个插件就是基于skel做的，如果自己从头开发CNI插件，这是很好的参考
func main() {
	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))
}

type cniBridgeIf struct {
	Name        string
	ifIndex     int
	peerIndex   int
	masterIndex int
	found       bool
}


// CHECK接口
func cmdCheck(args *skel.CmdArgs) error {
	// 检查网络配置
	n, _, err := loadNetConf(args.StdinData)
	if err != nil {
		return err
	}
	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	defer netns.Close()

	// 调用IPAM插件进行检查
	err = ipam.ExecCheck(n.IPAM.Type, args.StdinData)
	if err != nil {
		return err
	}

	// 解析并转换上一次ADD调用的结果
	if n.NetConf.RawPrevResult == nil {
		return fmt.Errorf("Required prevResult missing")
	}
	if err := version.ParsePrevResult(&amp;n.NetConf); err != nil {
		return err
	}

	result, err := current.NewResultFromResult(n.PrevResult)
	if err != nil {
		return err
	}

	var errLink error
	var contCNI, vethCNI cniBridgeIf
	var brMap, contMap current.Interface

	// 遍历上次调用结果中的网络接口列表，找到容器网络接口、网桥接口
	for _, intf := range result.Interfaces {
		if n.BrName == intf.Name {
			brMap = *intf
			continue
		} else if args.IfName == intf.Name {
			if args.Netns == intf.Sandbox {
				contMap = *intf
				continue
			}
		}
	}
	// 检查网桥
	brCNI, err := validateCniBrInterface(brMap, n)
	if err != nil {
		return err
	}

	// 检查容器的沙盒是否就是参数中的网络命名空间
	if args.Netns != contMap.Sandbox {
		return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
			contMap.Sandbox, args.Netns)
	}

	// 检查容器网络接口
	if err := netns.Do(func(_ ns.NetNS) error {
		contCNI, errLink = validateCniContainerInterface(contMap)
		if errLink != nil {
			return errLink
		}
		return nil
	}); err != nil {
		return err
	}

	// 检查容器veth的对端
	for _, intf := range result.Interfaces {
		// Skip this result if name is the same as cni bridge
		// It's either the cni bridge we dealt with above, or something with the
		// same name in a different namespace.  We just skip since it's not ours
		if brMap.Name == intf.Name {
			continue
		}

		// same here for container name
		if contMap.Name == intf.Name {
			continue
		}

		vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI)
		if errLink != nil {
			return errLink
		}

		if vethCNI.found {
			// veth with container interface as peer and bridge as master found
			break
		}
	}

	if !brCNI.found {
		return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName)
	}
	if !contCNI.found {
		return fmt.Errorf("CNI created interface in container %s not found", args.IfName)
	}
	if !vethCNI.found {
		return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName)
	}

	// 检查IP地址、路由、DNS，是否和容器中的状态匹配
	if err := netns.Do(func(_ ns.NetNS) error {
		// 检查容器IP是否符合期望
		err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
		if err != nil {
			return err
		}
		// 检查路由是否匹配
		err = ip.ValidateExpectedRoute(result.Routes)
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		return err
	}

	return nil
}

// 下面的若干函数都是由CHECK接口调用，实现了各方面的检查

// 检查网络接口
func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) {

	ifFound := cniBridgeIf{found: false}
	if intf.Name == "" {
		return ifFound, nil, fmt.Errorf("Interface name missing ")
	}
	// 确保接口存在
	link, err := netlink.LinkByName(intf.Name)
	if err != nil {
		return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name)
	}
	// 确保接口在/不在网络沙盒中
	if expectInSb {
		if intf.Sandbox == "" {
			return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name)
		}
	} else {
		if intf.Sandbox != "" {
			return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name)
		}
	}

	return ifFound, link, err
}

// 检查网桥
func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) {

	// 检查网桥接口本身
	brFound, link, err := validateInterface(intf, false)
	if err != nil {
		return brFound, err
	}
	// 断言其是一个网桥
	_, isBridge := link.(*netlink.Bridge)
	if !isBridge {
		return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name)
	}
	// 如果网桥有MAC，断言它和结果中的MAC一致
	if intf.Mac != "" {
		if intf.Mac != link.Attrs().HardwareAddr.String() {
			return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac)
		}
	}
	// 混杂模式断言
	linkPromisc := link.Attrs().Promisc != 0
	if linkPromisc != n.PromiscMode {
		return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ",
			intf.Name, n.PromiscMode, linkPromisc)
	}

	brFound.found = true
	brFound.Name = link.Attrs().Name
	brFound.ifIndex = link.Attrs().Index
	brFound.masterIndex = link.Attrs().MasterIndex

	return brFound, nil
}

// 检查容器中的VETH接口
func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) {
	// 同样的，首先进行接口通用检查
	vethFound, link, err := validateInterface(*intf, false)
	if err != nil {
		return vethFound, err
	}
	// 断言其是一个VETH
	_, isVeth := link.(*netlink.Veth)
	if !isVeth {
		// just skip it, it's not what CNI created
		return vethFound, nil
	}
	// 断言对端的序号符合预期
	_, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
	if err != nil {
		return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
	}
	vethFound.ifIndex = link.Attrs().Index
	vethFound.masterIndex = link.Attrs().MasterIndex

	if vethFound.ifIndex != contIf.peerIndex {
		return vethFound, nil
	}

	if contIf.ifIndex != vethFound.peerIndex {
		return vethFound, nil
	}

	if vethFound.masterIndex != brIf.ifIndex {
		return vethFound, nil
	}
	// 断言MAC地址符合预期
	if intf.Mac != "" {
		if intf.Mac != link.Attrs().HardwareAddr.String() {
			return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac)
		}
	}

	vethFound.found = true
	vethFound.Name = link.Attrs().Name

	return vethFound, nil
}

func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) {

	vethFound, link, err := validateInterface(intf, true)
	if err != nil {
		return vethFound, err
	}

	_, isVeth := link.(*netlink.Veth)
	if !isVeth {
		return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name)
	}
	_, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
	if err != nil {
		return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
	}
	vethFound.ifIndex = link.Attrs().Index

	if intf.Mac != "" {
		if intf.Mac != link.Attrs().HardwareAddr.String() {
			return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
		}
	}

	vethFound.found = true
	vethFound.Name = link.Attrs().Name

	return vethFound, nil
}</pre>
<div class="blog_h3"><span class="graybg">VETH对创建</span></div>
<p>创建VETH对的过程如下：</p>
<pre class="crayon-plain-tag">// 从容器命名空间中调用此函数
// 自动将一端移动到“宿主机”命名空间（实际上是任何NS，不一定是真正的宿主机NS）
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth net.Interface, containerVeth net.Interface, error) {
	return SetupVethWithName(contVethName, "", mtu, hostNS)
}

func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
	// 宿主端      容器端
	hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu)
	if err != nil {
		return net.Interface{}, net.Interface{}, err
	}
	// 启动容器端
	if err = netlink.LinkSetUp(contVeth); err != nil {
		return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err)
	}

	hostVeth, err := netlink.LinkByName(hostVethName)
	if err != nil {
		return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
	}

	// 移动对端到宿主机命名空间
	if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
		return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err)
	}

	// 在宿主机命名空间启动对端
	err = hostNS.Do(func(_ ns.NetNS) error {
		hostVeth, err = netlink.LinkByName(hostVethName)
		if err != nil {
			return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
		}

		if err = netlink.LinkSetUp(hostVeth); err != nil {
			return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
		}

		// we want to own the routes for this interface
		_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", hostVethName), "0")
		return nil
	})
	if err != nil {
		return net.Interface{}, net.Interface{}, err
	}
	return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil
}

func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink.Link, err error) {
	for i := 0; i &lt; 10; i++ {
		if vethPeerName != "" {
			peerName = vethPeerName
		} else {
			// 给对端随机分配名称
			peerName, err = RandomVethName()
			if err != nil {
				return
			}
		}

		veth, err = makeVethPair(name, peerName, mtu)
		switch {
		case err == nil:
			return

		case os.IsExist(err):
			if peerExists(peerName) &amp;&amp; vethPeerName == "" {
				continue
			}
			err = fmt.Errorf("container veth name provided (%v) already exists", name)
			return

		default:
			err = fmt.Errorf("failed to make veth pair: %v", err)
			return
		}
	}

	// should really never be hit
	err = fmt.Errorf("failed to find a unique veth name")
	return
}


func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
	// 创建VETH结构
	veth := &amp;netlink.Veth{
		LinkAttrs: netlink.LinkAttrs{
			Name:  name,
			Flags: net.FlagUp,
			MTU:   mtu,
		},
		// 对端（需要移动到宿主机的）名称，随机，例如veth99350820
		PeerName: peer,
	}
	// 添加连接
	if err := netlink.LinkAdd(veth); err != nil {
		return nil, err
	}
	// 创建连接后需要重新获取
	// 因为索引、MAC等信息可能更新
	veth2, err := netlink.LinkByName(name)
	if err != nil {
		netlink.LinkDel(veth) // 出错则删除设备并清理
		return nil, err
	}

	return veth2, nil
}</pre>
<div class="blog_h3"><span class="graybg">网络拓扑</span></div>
<p>使用配置：</p>
<pre class="crayon-plain-tag">{
	"cniVersion": "0.3.1",
	"name": "testConfig",
	"type": "bridge",
	"bridge": "bridge0",
	"vlan": 100,
	"ipam": {}
}</pre>
<p>获得应答：</p>
<pre class="crayon-plain-tag">{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "bridge0",
            "mac": "ce:d7:70:c7:97:08"
        },
        {
            "name": "veth99350820",
            "mac": "ce:d7:70:c7:97:08"
        },
        {
            "name": "eth0",
            "mac": "ce:87:b0:d4:94:c3",
            "sandbox": "/var/run/netns/cnitest-cdf173b0-fa2a-66ab-e5d6-bba23a0708f5"
        }
    ],
    "dns": {}
}</pre>
<p>产生的网络拓扑如下： </p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge-vlan.png"><img class=" wp-image-33017 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge-vlan.png" alt="bridge-vlan" width="463" height="581" /></a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/cni-study-note">CNI学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/cni-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Flannel学习笔记</title>
		<link>https://blog.gmem.cc/flannel</link>
		<comments>https://blog.gmem.cc/flannel#comments</comments>
		<pubDate>Sun, 03 Mar 2019 12:58:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=36559</guid>
		<description><![CDATA[<p>简介 Flannel是一个老牌的容器网络插件，支持CNI规范，不支持网络策略。 Flannel会在每个节点上运行一个守护进程flanneld，这个守护进程负责从一个大的地址空间（K8S Pod CIDR）分配子网给节点。Flannel支持使用K8S API，或者直接使用etcd来存储网络配置、分配的子网信息、其它任何辅助数据（例如节点的公共IP地址）。  Flannel后端 封包转发的工作由Flannel后端机制负责。 VXLAN 使用内核中的VXLAN功能，对容器封包进行封装，通过UDP发送到其它节点。 host-gw 在所有节点上配置适当的、针对容器IP子网的路由规则。要求所有运行Flannel的节点在L2相互联通。 UDP 仅仅用于调试目的，性能很差。 安装 K8S 对于K8S 1.17+可以使用如下清单： --- # 支持PSP apiVersion: <a class="read-more" href="https://blog.gmem.cc/flannel">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/flannel">Flannel学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Flannel是一个老牌的容器网络插件，支持CNI规范，不支持网络策略。</p>
<p>Flannel会在每个节点上运行一个守护进程flanneld，这个守护进程负责从一个大的地址空间（K8S Pod CIDR）分配子网给节点。Flannel支持使用K8S API，或者直接使用etcd来存储网络配置、分配的子网信息、其它任何辅助数据（例如节点的公共IP地址）。 </p>
<div class="blog_h2"><span class="graybg">Flannel后端</span></div>
<p>封包转发的工作由Flannel后端机制负责。</p>
<div class="blog_h3"><span class="graybg">VXLAN</span></div>
<p>使用内核中的VXLAN功能，对容器封包进行封装，通过UDP发送到其它节点。</p>
<div class="blog_h3"><span class="graybg">host-gw</span></div>
<p>在所有节点上配置适当的、针对容器IP子网的路由规则。要求所有运行Flannel的节点在L2相互联通。</p>
<div class="blog_h3"><span class="graybg">UDP</span></div>
<p>仅仅用于调试目的，性能很差。</p>
<div class="blog_h1"><span class="graybg"><a id="install"></a>安装</span></div>
<div class="blog_h2"><span class="graybg">K8S</span></div>
<p>对于K8S 1.17+可以使用如下清单：</p>
<pre class="lang:yaml decode:true ">---

# 支持PSP
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp.flannel.unprivileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
    seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
    apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
    apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
spec:
  privileged: false
  volumes:
  - configMap
  - secret
  - emptyDir
  - hostPath
  allowedHostPaths:
  - pathPrefix: "/etc/cni/net.d"
  - pathPrefix: "/etc/kube-flannel"
  - pathPrefix: "/run/flannel"
  readOnlyRootFilesystem: false
  # Users and groups
  runAsUser:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  # Privilege Escalation
  allowPrivilegeEscalation: false
  defaultAllowPrivilegeEscalation: false
  # Capabilities
  allowedCapabilities: ['NET_ADMIN', 'NET_RAW']
  defaultAddCapabilities: []
  requiredDropCapabilities: []
  # Host namespaces
  hostPID: false
  hostIPC: false
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  # SELinux
  seLinux:
    # SELinux is unused in CaaSP
    rule: 'RunAsAny'

---

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: flannel
rules:
- apiGroups: ['extensions']
  resources: ['podsecuritypolicies']
  verbs: ['use']
  resourceNames: ['psp.flannel.unprivileged']
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - nodes/status
  verbs:
  - patch

---

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: flannel
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flannel
subjects:
- kind: ServiceAccount
  name: flannel
  namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: flannel
  namespace: kube-system

---

kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  # 会写入到宿主机的CNI配置
  cni-conf.json: |
    {
      "name": "cbr0",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  # Flannel配置
  net-conf.json: |
    {
      # 一般填写Pod CIDR
      "Network": "10.244.0.0/16",
      # 使用的Flannel后端
      "Backend": {
        "Type": "vxlan"
      }
    }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/os
                operator: In
                values:
                - linux
      hostNetwork: true
      # 标注调度优先级
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.13.1-rc2
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.13.1-rc2
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
      - name: run
        hostPath:
          path: /run/flannel
      - name: cni
        hostPath:
          path: /etc/cni/net.d
      - name: flannel-cfg
        configMap:
          name: kube-flannel-cfg
</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/flannel">Flannel学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/flannel/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于Calico的CNI</title>
		<link>https://blog.gmem.cc/calico</link>
		<comments>https://blog.gmem.cc/calico#comments</comments>
		<pubDate>Mon, 12 Feb 2018 07:42:30 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=18489</guid>
		<description><![CDATA[<p>什么是Calico Calico为容器或虚拟机提供安全的网络连接，它创建一个扁平化的第3层网络，为每个节点分配一个可路由的IP地址。网络中的节点不需要NAT或IP隧道就可以相互通信，因此性能很好，接近于物理网络，不使用网络策略的情况下，可能引入0.01毫秒级别的延迟，带宽影响不明显）。 在需要Overlay的应用场景下，Calico可以支持IP-in-IP隧道，也可以与其它Overlay网络（例如Flannel）配合，IP-in-IP隧道会带来较小的性能下降。 Calico支持动态的网络安全策略（NetPolicy），你可以细粒度的控制容器、虚拟机、物理机端点之间的网络通信。 Calico在每个节点运行一个虚拟路由器（vRouter），vRouter利用Linux内核自带的IP转发功能，工作负载依赖于此路由器和外部通信。节点上的代理组件Felix负责根据分配到节点上的工作负载的IP地址信息为vRouter提供L3转发规则。vRouter基于BIRD实现了边界网关协议（BGP）。通过vRouter，工作负载直接基于物理网络进行通信，甚至可以被分配外网IP并直接暴露到互联网上。 BGP术语 术语 说明 BGP 边界网关协议（Border Gateway Protocol）是互联网上一个核心的去中心化自治路由协议，它通过维护IP路由表或前缀表来实现自治系统（AS）之间的可达性 大多数ISP使用BGP来与其他ISP创建路由连接，特大型的私有IP网络也可以使用BGP BGP的通信对端（对等实体，Peer）通过TCP（端口179）会话交换数据，BGP路由器会周期地发送19字节的保活消息来维护连接。在路由协议中，只有BGP使用TCP作为传输层协议 IBGP 内部边界网关协议。同一个AS内部的两个或多个对等实体之间运行的BGP被称为IBGP IGP 内部网关协议。同一AS内部的对等实体（路由器）之间使用的协议，它存在可扩容性问题： 一个IGP内部应该仅有数十（最多小几百）个对等实体 对于端点数，也存在限制，一般在数百（最多上千）个Endpoint级别 IBGP和IGP都是处理AS内部路由的，仍然需要IGP的原因是： IBGP之间是TCP连接，也就意味着IBGP邻居采用的是逻辑连接的方式，两个IBGP连接不一定存在实际的物理链路。所以需要有IGP来提供路由，以完成BGP路由的递归查找 <a class="read-more" href="https://blog.gmem.cc/calico">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/calico">基于Calico的CNI</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">什么是Calico</span></div>
<p>Calico为容器或虚拟机提供安全的网络连接，它创建一个扁平化的第3层网络，为每个节点分配一个可路由的IP地址。网络中的节点不需要NAT或IP隧道就可以相互通信，因此性能很好，接近于物理网络，不使用网络策略的情况下，<span style="background-color: #c0c0c0;">可能引入0.01毫秒级别的延迟，带宽影响不明显</span>）。 在需要Overlay的应用场景下，Calico可以支持IP-in-IP隧道，也可以与其它Overlay网络（例如Flannel）配合，IP-in-IP隧道会带来<span style="background-color: #c0c0c0;">较小的性能下降</span>。</p>
<p>Calico支持动态的网络安全策略（NetPolicy），你可以细粒度的控制容器、虚拟机、物理机端点之间的网络通信。</p>
<p>Calico在<span style="background-color: #c0c0c0;">每个节点</span>运行一个虚拟路由器（vRouter），<span style="background-color: #c0c0c0;">vRouter利用Linux内核自带的IP转发功能，工作负载依赖于此路由器和外部通信</span>。节点上的代理组件Felix负责根据分配到节点上的工作负载的IP地址信息为vRouter提供L3转发规则。vRouter基于BIRD实现了边界网关协议（BGP）。通过vRouter，工作负载直接基于物理网络进行通信，甚至可以被分配外网IP并直接暴露到互联网上。</p>
<div class="blog_h1"><span class="graybg">BGP术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>BGP</td>
<td>
<p>边界网关协议（Border Gateway Protocol）是互联网上一个核心的<span style="background-color: #c0c0c0;">去中心化</span>自治路由协议，它通过维护IP路由表或前缀表来实现自治系统（AS）之间的可达性</p>
<p>大多数ISP使用BGP来与其他ISP创建路由连接，特大型的私有IP网络也可以使用BGP</p>
<p>BGP的<span style="background-color: #c0c0c0;">通信对端（对等实体，Peer）</span>通过TCP（端口179）会话交换数据，BGP路由器会周期地发送19字节的保活消息来维护连接。在路由协议中，只有BGP使用TCP作为传输层协议</p>
</td>
</tr>
<tr>
<td>IBGP</td>
<td>
<p>内部边界网关协议。同一个AS内部的两个或多个对等实体之间运行的BGP被称为IBGP</p>
</td>
</tr>
<tr>
<td>IGP</td>
<td>
<p>内部网关协议。同一AS内部的对等实体（路由器）之间使用的协议，它存在可扩容性问题：</p>
<ol>
<li>一个IGP内部应该仅有数十（最多小几百）个对等实体</li>
<li>对于端点数，也存在限制，一般在数百（最多上千）个Endpoint级别</li>
</ol>
<p>IBGP和IGP都是处理AS内部路由的，仍然需要IGP的原因是：</p>
<ol>
<li>IBGP之间是TCP连接，也就意味着IBGP邻居采用的是逻辑连接的方式，两个IBGP连接不一定存在实际的物理链路。所以需要有IGP来提供路由，以完成BGP路由的递归查找</li>
<li>BGP协议本身实际上并不发现路由，BGP将路由发现的工作全部移交给了IGP协议，它本身着重于路由的控制</li>
</ol>
</td>
</tr>
<tr>
<td>EBGP</td>
<td>
<p>外部边界网关协议。归属不同的AS的对等实体之间运行的BGP称为EBGP</p>
</td>
</tr>
<tr>
<td>Border Router</td>
<td>
<p>边界路由器，在AS边界上与其他AS交换信息的路由器</p>
</td>
</tr>
<tr>
<td>AS </td>
<td>
<p>自治系统（Autonomous system），一个组织（例如ISP）管辖下的所有IP网络和路由器的整体</p>
<p>参与BGP路由的每个AS都被分配一个唯一的自治系统编号（ASN）。对BGP来说ASN是区别整个相互连接的网络中的各个网络的唯一标识。64512到65535之间的ASN编号保留给专用网络使用</p>
</td>
</tr>
<tr>
<td>Route Reflector</td>
<td>
<p>同一AS内如果有<span style="background-color: #c0c0c0;">多个路由器参与BGP路由</span>，则它们之间必须配置成<span style="background-color: #c0c0c0;">全连通的网状结构</span>——任意两个路由器之间都必须配置成对等实体。由于所需要TCP连接数是路由器数量的平方，这就导致了巨大的TCP连接数</p>
<p>为了缓解这种问题，BGP支持两种方案：Route Reflector、Confederations</p>
<p>路由反射器（Route Reflector）是AS内的一台路由器，其它所有路由器都和RR直接连接，作为RR的客户机。RR和客户机之间建立BGP连接，而客户机之间则不需要相互通信</p>
<p>RR的工作步骤如下：</p>
<ol>
<li>从非客户机IBGP对等实体学到的路由，发布给此RR的所有客户机</li>
<li>从客户机学到的路由，发布给此RR的所有非客户机和客户机</li>
<li>从EBGP对等实体学到的路由，发布给所有的非客户机和客户机</li>
</ol>
<p>RR的一个优势是配置方便，因为只需要在反射器上配置</p>
</td>
</tr>
<tr>
<td>工作负载</td>
<td>Workload，即运行在Calico节点上的虚机或容器</td>
</tr>
<tr>
<td>全互联</td>
<td>全互联网络（Full node-to-node Mesh）是指任何两个Calico节点都进行配对的L3连接模式</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">集成K8S</span></div>
<div class="blog_h2"><span class="graybg">安装Calico</span></div>
<p>关于如何快速启用基于Calico的CNI网络连接，参考<a href="/kubernetes-study-note#with-calico">Kubernetes学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">网络策略</span></div>
<p>Calico支持K8S的NetworkPolicy，下面是一个示例。</p>
<div class="blog_h3"><span class="graybg">准备</span></div>
<p>首先创建一个名字空间：</p>
<pre class="crayon-plain-tag">kubectl create ns dev
# 为新名字空间的默认账户添加imagePullSecrets
kubectl --namespace=dev edit serviceaccount default</pre>
<p>然后创建一个5实例的部署：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev run media-api --replicas=5 --image=docker.gmem.cc/media-api:1.1
kubectl --namespace=dev get pod</pre>
<p>将上述部署暴露为服务：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev expose deployment media-api --port=8800,7700
kubectl --namespace=dev get service</pre>
<p>执行类似下面的命令，确认服务能够正常响应HTTP请求：</p>
<pre class="crayon-plain-tag">curl http://media-api.dev.svc.k8s.gmem.cc:8800/media/newpub/2017-01-01</pre>
<div class="blog_h3"><span class="graybg">隔离访问</span></div>
<p>下面将上述名字空间dev隔离掉，Calico会阻止访问此名字空间中各的Pod：</p>
<pre class="crayon-plain-tag">kubectl create -f - &lt;&lt;EOF
kind: NetworkPolicy
apiVersion: extensions/v1beta1
metadata:
  name: default-deny
  namespace: dev
spec:
  podSelector:
    # 默认拒绝任何访问
    matchLabels: {}
EOF</pre>
<p>这是再次执行curl命令，你无法得到响应。</p>
<div class="blog_h3"><span class="graybg">准许访问</span></div>
<p>下面我们再创建一个网络策略， 允许API网关层访问上面的服务：</p>
<pre class="crayon-plain-tag">kubectl create -f - &lt;&lt;EOF
kind: NetworkPolicy
apiVersion: extensions/v1beta1
metadata:
  name: allow-ingress-nginx
  namespace: dev
spec:
  podSelector:
    matchLabels:
      # 下面的标签是自动添加的
      run: media-api
  # 允许来自apigateway层的入站连接
  ingress:
    - from:
      - podSelector:
          matchLabels:
            tier: apigateway
EOF</pre>
<div class="blog_h3"><span class="graybg">删除策略</span></div>
<p>要删除网络策略，执行：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev delete networkpolicy default-deny</pre>
<div class="blog_h3"><span class="graybg">网络策略列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>禁止所有入站连接</td>
<td>
<pre class="crayon-plain-tag">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: advanced-policy-demo
spec:
  # 下面的选择器匹配所有Pod
  podSelector:
    matchLabels: {}
  # 策略类型：入站
  policyTypes:
  - Ingress</pre>
</td>
</tr>
<tr>
<td>禁止对特定Pod的入站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    # 允许对Nginx的入站访问
    matchLabels:
      run: nginx
  ingress:
    - from:
      - podSelector:
          matchLabels: {}</pre>
</td>
</tr>
<tr>
<td>禁止所有出站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress</pre>
</td>
</tr>
<tr>
<td>允许进行DNS查询的出站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress
  # 允许针对kube-system名字空间中的Pod的53端口进行UDP访问
  egress:
  - to:
    # 名字空间选择器
    - namespaceSelector:
        matchLabels:
          name: kube-system
    # 53端口
    ports:
    - protocol: UDP
      port: 53</pre>
</td>
</tr>
<tr>
<td>允许对Nginx的出站访问</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          run: nginx</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">静态IP</span></div>
<p>通常你应该使用Service（具有集群静态IP）而不是尝试为Pod指定静态IP。</p>
<p>如果非要这么做，Calico为Pod提供了一些注解：</p>
<pre class="crayon-plain-tag"># 分配给当前Pod的IP地址列表，请求的地址必须位于Calico的IPAM的地址池中
annotations:
    "cni.projectcalico.org/ipAddrs": "[\"192.168.0.1\"]"

# 分配给当前Pod的IP地址列表，绕过IPAM
# 你可以分配任何地址给Pod，但是仅仅当地址位于Calico的地址空间中时，Calico才负责分配路由
# 注意IP冲突问题
annotations:
    "cni.projectcalico.org/ipAddrsNoIpam": "[\"10.0.0.1\"]"</pre>
<div class="blog_h2"><span class="graybg">高可用</span></div>
<p>Calico依赖于Etcd来存放配置信息，缺省情况下它会容器化安装单实例的Etcd。你可以修改其Configmap，指定使用外部的、高可用的Etcd：</p>
<pre class="crayon-plain-tag">kind: ConfigMap
apiVersion: v1
metadata:
  name: calico-config
  namespace: kube-system
data:
  # 可以和K8S共享Etcd集群
  etcd_endpoints: "http://10.0.1.1:2379,http://10.0.2.1:2379,http://10.0.3.1:2379" </pre>
<div class="blog_h1"><span class="graybg">外部连接</span></div>
<div class="blog_h2"><span class="graybg">出站连接</span></div>
<p>所谓出站外部连接，是指从Calico端点到位于Calico集群外部的目的主机的连接。</p>
<p>最简单的实现外部出站连接的方式是为Calico池开启NAT：</p>
<pre class="crayon-plain-tag"># 查看默认IP池的配置
# calicoctl get ipPool default-ipv4-ippool -o yaml
# 输出：
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  creationTimestamp: 2018-02-12T11:33:04Z
  name: default-ipv4-ippool
  resourceVersion: "5"
  uid: 7d7a9461-0fe8-11e8-a715-deadbeef00a0
spec:
  cidr: 192.168.0.0/16
  ipipMode: Always
  # 已经启用出站NAT
  natOutgoing: true</pre>
<p>你可以针对某个IP池进行NAT设置。</p>
<div class="blog_h2"><span class="graybg">入站连接</span></div>
<p>所谓入站外部连接，是指从Calico集群外部主机发起的，针对Calico端点的连接。实现入站外部连接的方式主要由两种。</p>
<div class="blog_h3"><span class="graybg">BGP Peering</span></div>
<p>利用BGP配对，将<span style="background-color: #c0c0c0;">外部网络的基础设施配对到Calico集群中的某些节点</span>。这需要外部网络中包含支持BGP协议的交换机或者路由器，该路由器作为访问Calico集群内部端点的网关。</p>
<p>如果网络规模较小，你可以让外部路由器和所有Calico节点之间建立BGP会话；如果网络规模较大，可能需要利用RR来创建一个第三层拓扑。 </p>
<p>下面是一个基于BIRD（支持BGP协议的软路由）的示例：Calico集群使用AS号65000，外部网络使用AS号65001，Calico集群内部节点10.0.0.100配对到外部路由器（BIRD）10.0.0.1，Calico内部使用节点全互联模式：</p>
<ol>
<li>Calico BGP Peer配置文件：<br />
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-xenial-100-zircon
spec:
  peerIP: 10.0.0.1
  node: xenial-100
  asNumber: 65001 </pre>
</li>
<li>BIRD配置文件：<br />
<pre class="crayon-plain-tag">protocol bgp xenial100 {
    description "10.0.0.100";
    local as 65001;
    neighbor 10.0.0.100 as 65000;
    source address 10.0.0.1;
    graceful restart;
    import all;
    export all;
}</pre>
</li>
</ol>
<p>按照上面方式配置后，可以在BIRD上看到学习到的路由：</p>
<pre class="crayon-plain-tag"># sudo birdc show route
172.27.154.192/26  via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.187.192/26  via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.97.64/26    via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.121.64/26   via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.41.128/26   via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.61.64/26    via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]

# 同步的内核路由表
# route
172.27.41.128   xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.61.64    xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.97.64    xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.121.64   xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.154.192  xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.187.192  xenial-100      255.255.255.192 UG    0      0        0 virbr0 </pre>
<p>此外，下一章的RR示例也可以支持入站外部链接。</p>
<div class="blog_h3"><span class="graybg">编排器特定方式</span></div>
<p>Calico支持多种容器编排器（Orchestrator）特有的入站连接方式，例如Kubernetes的Service IP。</p>
<div class="blog_h3"><span class="graybg">外部访问K8S服务IP</span></div>
<p>本节内容和Calico无直接关系，Calico仅仅为K8S提供容器连接性，它不知道K8S的服务（Service）是何物。</p>
<p>K8S中Service IP仅仅在集群内部可以访问，集群内节点或者Pod依赖Kube Proxy来访问Service IP。<span style="background-color: #c0c0c0;">外部访问Service的规范化方式</span>是，使用NodePort（可以配合LoadBalancer），向外部暴露服务。</p>
<p>但是，要直接暴露Service的IP地址到集群外部也是可以实现的。你只需要配置适当的路由规则，将Service IP子网的网关设置为K8S集群的任意节点即可：</p>
<pre class="crayon-plain-tag"># K8S 服务网段为10.96.0.0/12，10.0.0.100为集群中一个节点
route add -net 10.96.0.0 netmask 255.240.0.0 gw 10.0.0.100

# 设置完路由后，尝试连接集群的DNS服务：telnet 10.96.0.10 53，不要ping，不支持</pre>
<div class="blog_h1"><span class="graybg">使用RR</span></div>
<p>较大规模的网络拓扑中，启用节点全互联需要大量的TCP连接，可以考虑引入Router Refactor并禁用全互联。实践中<span style="background-color: #c0c0c0;">“较大规模”网络拓扑包含50+节点</span>，但是也有超过100节点的正常运作的全互联网络。</p>
<p>BIRD是一个BGP实现，支持完全的动态路由，本章以BIRD软路由器作为RR。</p>
<div class="blog_h2"><span class="graybg">安装BIRD</span></div>
<pre class="crayon-plain-tag">sudo add-apt-repository ppa:cz.nic-labs/bird
sudo apt-get update
sudo apt-get install bird</pre>
<div class="blog_h2"><span class="graybg">配置BIRD</span></div>
<div class="blog_h3"><span class="graybg">IPv4</span></div>
<pre class="crayon-plain-tag"># 日志配置
log syslog { debug, trace, info, remote, warning, error, auth, fatal, bug };
log stderr all;

# 当前路由器（RR）全局唯一标识，通常设置为路由器的IP地址
router id 10.0.0.1;

filter import_kernel {
    if ( net != 0.0.0.0/0 ) then {
        accept;
    }
    reject;
}

# 所有协议的全局性调试开关
debug protocols all;

# 伪协议，不是和网络中其它路由器通信，而是每60秒将BIRD的路由表同步到OS内核
protocol kernel {
    scan time 60;
    import none;  # 从内核路由表导入路由条目
    export all;   # 将BIRD的路由同步给内核
}
 
# 伪协议，每2秒监控网络接口的信息
protocol device {
	scan time 2;
}


# 为拓扑中每个节点（每个连接到此RR的Peer）添加以下内容
protocol bgp xenial100 {
    # 可选的描述
    description "10.0.0.100";

    # 声明本路由器所属的AS
    #       IP地址 如果不使用179 AS号
    # local [ip] [port number] [as number]
    local as 65000;

    # BGP实例，指定当前路由器与自通信的邻居路由器的信息
    #                                            如果和当前路由器AS一样则自动切换为IBGP
    #                                                        可以不指定AS号而指定internal
    #                指定网络前缀而非精确的IP，则启用动态BGP行为，当前路由器会在BGP端口监听，当匹配
    #                前缀的BGP连接到来后，spawn一个BGP实例处理它。普通BGP实例/动态BGP实例可混合使用
    # neighbor [ip | range prefix] [port number] [as number] [internal|external]
    # neighbor range 10.0.3.0/24 as 65000;
    neighbor 10.0.0.100 as 65000;
    # 提示此邻居和本路由器（的某个接口）是直接相连的
    direct;
    # 本路由器作为路由反射器，邻居作为RR客户端
    rr client;
    # 当一个BGP speaker重启/崩溃后，邻居会丢弃从它接收到的所有路径。即使该speaker的转发平面仍然
    # 继续工作，也会出现封包转发被干扰的情况。该选项用于缓和这个问题
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial101 {
    description "10.0.0.101";
    local as 65000;
    neighbor 10.0.0.101 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial102 {
    description "10.0.0.102";
    local as 65000;
    neighbor 10.0.0.102 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial103 {
    description "10.0.0.103";
    local as 65000;
    neighbor 10.0.0.103 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial104 {
    description "10.0.0.104";
    local as 65000;
    neighbor 10.0.0.104 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial105 {
    description "10.0.0.105";
    local as 65000;
    neighbor 10.0.0.105 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}</pre>
<p>配置完毕后，重启BIRD：<pre class="crayon-plain-tag">sudo service bird restart</pre></p>
<p>很快，你就可以看到RR从Calico节点同步了路由信息过来：</p>
<pre class="crayon-plain-tag">Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         localhost       0.0.0.0         UG    0      0        0 wlan1
10.0.0.0        *               255.255.0.0     U     0      0        0 virbr0
172.17.0.0      *               255.255.0.0     U     0      0        0 docker0
172.18.0.0      *               255.255.0.0     U     0      0        0 docker_gwbridge
172.21.0.0      *               255.255.0.0     U     0      0        0 br-29f4509ebfd6
172.27.0.0      xenial-101      255.255.255.192 UG    0      0        0 virbr0
172.27.0.0      xenial-101      255.255.0.0     UG    0      0        0 virbr0
172.27.0.0      xenial-100      255.255.0.0     UG    0      0        0 virbr0
192.168.142.0   *               255.255.254.0   U     9      0        0 wlan1</pre>
<div class="blog_h2"><span class="graybg">配置Calico节点</span></div>
<p>首先禁用全互联，查看Calico的BGP选项，如果启用全互联则禁用之：</p>
<pre class="crayon-plain-tag"># calicoctl get bgpconfig -o yaml &gt; /tmp/bgp.yaml

apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: BGPConfiguration
  metadata:
    creationTimestamp: 2018-02-14T02:58:56Z
    name: default
    resourceVersion: "790"
    uid: ff92b98a-1132-11e8-937a-deadbeef00a0
  spec:
    asNumber: 65000
    logSeverityScreen: Info
    # 这里设置为false
    nodeToNodeMeshEnabled: false   
kind: BGPConfigurationList
metadata:
  resourceVersion: "826"
# calicoctl replace -f /tmp/bgp.yaml</pre>
<p>然后，添加一个全局的Peer，指向先前创建的RR：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-global-1
spec:
  peerIP: 10.0.0.1
  asNumber: 65000
EOF

# calicoctl get BGPPeer -o yaml &gt; /tmp/bgpeer.yaml
# calicoctl replace -f /tmp/bgpeer.yaml</pre>
<p>现在，登陆到某个Calico节点，查看其配对情况，应该仅仅和10.0.0.1进行了配对：</p>
<pre class="crayon-plain-tag"># calicoctl node status

Calico process is running.

IPv4 BGP status
+--------------+-----------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE |  SINCE   |    INFO     |
+--------------+-----------+-------+----------+-------------+
| 10.0.0.1     | global    | up    | 03:43:38 | Established |
+--------------+-----------+-------+----------+-------------+

IPv6 BGP status
No IPv6 peers found.</pre>
<p>可以尝试在集群内连接某个Pod，如果可以连接，说明配置成功。 </p>
<div class="blog_h2"><span class="graybg">更简单的方法</span></div>
<p>现在，你可以在任何物理节点上，执行calico node run命令，运行一个容器，将节点加入Calico CNI网络：</p>
<pre class="crayon-plain-tag">sudo calicoctl node run --ip-autodetection-method interface=virbr0</pre>
<p>这样的节点可以直接作为路由反射器使用，也可以直连Pod。</p>
<div class="blog_h2"><span class="graybg">集群内RR</span></div>
<p>从3.3开始，任何Calico节点均可以作为RR来运行，不需要基础设施或外部的专用RR节点。</p>
<div class="blog_h3"><span class="graybg">节点作为RR</span></div>
<p>最简单的配置，为Node加上routeReflectorClusterID字段即可：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Node
metadata:
  name: node-hostname
  # 标签影响配对
  labels:
    routeReflector: 10.0.0.1
spec:
  bgp:
    asNumber: 64512
    ipv4Address: 10.244.0.1/24
    ipv6Address: 2001:db8:85a3::8a2e:370:7334/120
    # 提示此节点是一个RR（同时也是一个普通Calico节点）
    # 将它的BGP Peers看作RR客户端，除非节点具有相同的routeReflectorClusterID，
    # 这会在BGP协议层次上产生一系列影响
    routeReflectorClusterID: 10.0.0.1</pre>
<div class="blog_h3"><span class="graybg">配对设置</span></div>
<p>3.3为BGPPeer资源添加了字段：</p>
<ol>
<li>nodeSelector：配对应该在该选择器匹配的节点上发生。原先只能指定节点名称</li>
<li>peerSelector：配对应该在该选择器匹配的RR上发生。原先只能指定RR的IP</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: cluster1
spec:
  # 所有节点
  nodeSelector: "all()"
  # 所有具有routeReflector的RR
  peerSelector: "has(routeReflector)"</pre>
<p>一种典型的BGP拓扑划分方式：</p>
<ol>
<li>将节点分组，每个组包含一个RR</li>
<li>所有RR进行全互联 </li>
</ol>
<div class="blog_h1"><span class="graybg">配置</span></div>
<div class="blog_h2"><span class="graybg"><a id="BGPPeer"></a>BGP对等实体</span></div>
<div class="blog_h3"><span class="graybg">概念</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">概念</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Node-to-node mesh</td>
<td>
<p>使用full node-to-node mesh选项，可以自动化的配置所有Calico节点之间的对等实体。该选项默认启用，每个Calico节点都自动和任何其它节点进行BGP Peering</p>
<p>适用于几十个节点的规模</p>
</td>
</tr>
<tr>
<td>Global BGP peers</td>
<td>全局BGP Peer是一个BGP代理，它和网络中所有其它Calico节点进行Peering，典型应用场景是中等规模的部署，所有节点运行在同一个L2网络中，每个节点和RR（一个或一组）配对</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AS号配置</span></div>
<p>默认AS号为64512。如果所有节点在一个AS内部但是你需要定制AS号（例如需要同边界路由器进行Peering），执行以下命令：</p>
<pre class="crayon-plain-tag"># 检查默认BGP配置资源是否存在
calicoctl get bgpconfig default

# 如果不存在，则：
cat &lt;&lt; EOF | calicoctl create -f -
 apiVersion: projectcalico.org/v3
 kind: BGPConfiguration
 metadata:
   name: default
 spec:
   logSeverityScreen: Info
   nodeToNodeMeshEnabled: false   # 禁用全节点BGP互联。如果没有进行适当配置，一旦禁用立即无法连通现有的Pod
   asNumber: 65000                # 使用另一个AS号
EOF

# 如果存在，则先Dump在Replace
calicoctl get bgpconfig default -o yaml &gt; bgp.yaml
calicoctl replace -f bgp.yaml</pre>
<div class="blog_h3"><span class="graybg">禁用全互联</span></div>
<p>修改sepc.nodeToNodeMeshEnabled即可。</p>
<p>如果你准备从零开始构建网络，并且不希望使用全互联，可以禁用之。如果你准备从全互联切换到其它方式，则需要预先配置好Peering然后再禁用全互联，以保证系统持续的提供对外服务。</p>
<div class="blog_h3"><span class="graybg">全局对等实体</span></div>
<p>如果你的网络拓扑中存在可以和任意Calico节点配对（Peering）的BGP Speakers，这种BGP Speaker被称为全局对等实体（Global Peer）。一旦配置了全局对等实体，<span style="background-color: #c0c0c0;">Calico就会自动将所有节点与之配对</span>。</p>
<p>全局对等实体以下场景中有价值：</p>
<ol>
<li>添加了一个边界路由器，将其配对到全互联网络中</li>
<li>配置使用一个或两个RR的Calico网络，这种情况下每个节点都应该和RR配对，全互联应该禁用</li>
</ol>
<p>配置示例：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-global-3040
spec:
  peerIP: 192.20.30.40
  asNumber: 64567
EOF

# 移除全局对等实体
calicoctl delete bgppeer bgppeer-global-3040</pre>
<div class="blog_h3"><span class="graybg">节点对等实体</span></div>
<p>BGP网络拓扑更加复杂的情况下，你可能考虑针对每个Calico节点配置对等实体。示例：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-node-aabbff
spec:
  # 将位于AS 64514中的Peer aa:bb::ff 和 Calico节点node1配对
  peerIP: aa:bb::ff
  node: node1
  asNumber: 64514
EOF

# 查看配对
calicoctl get bgpPeer bgppeer-node-aabbff
# 移除配对
calicoctl delete bgppeer bgppeer-node-aabbff</pre>
<div class="blog_h3"><span class="graybg">检查配对情况</span></div>
<p>要检查某个节点的BGP Peer，执行命令：</p>
<pre class="crayon-plain-tag"># SSH到node1，然后执行下面的命令，可以看到所有和node1配对的实体
calicoctl node status

# Calico process is running.

# IPv4 BGP status
# +--------------+-------------------+-------+----------+-------------+
# | PEER ADDRESS |     PEER TYPE     | STATE |  SINCE   |    INFO     |
# +--------------+-------------------+-------+----------+-------------+
# | 10.0.0.101   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.102   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.103   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.104   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.105   | node-to-node mesh | up    | 15:53:18 | Established |
# +--------------+-------------------+-------+----------+-------------+

# IPv6 BGP status
# No IPv6 peers found. </pre>
<div class="blog_h2"><span class="graybg"><a id="BGPConfiguration"></a>BGP集群选项</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  # 资源的唯一性名称
  name: default
spec:
  # 全局日志级别：Debug, Info, Warning, Error, Fatal
  logSeverityScreen: Info
  # 是否启用节点全互联
  nodeToNodeMeshEnabled: true
  # AS号
  asNumber: 65000</pre>
<div class="blog_h2"><span class="graybg"><a id="Felix"></a>Felix配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  # 资源的唯一性名称
  name: default
spec:
  # 指定Felix操控内核顶级Iptable规则链的方式
  # Insert 默认，较安全，可以防止Calico的规则被Bypass
  # Append 需要注意规则链中更前面的规则可能导致Calico的规则被跳过
  chainInsertMode: Insert
  # 从工作负载发送到其所属宿主机（通过端点egress策略之后）的流量的默认处理方式，可选值Drop, Return, Accept
  # 默认情况下Calico阻止从工作负载到其宿主机的流量，实现手段是使用Iptables规则的DROP目标
  # Accept：Calico在处理完工作负载端点的Egress Policy之后，无条件的允许这种流量通过
  # Return：使用INPUT链中的其它规则进行处理。Calico默认在INPUT链顶部插入规则，使用该选项后，Calico在处理
  #         完工作负载端点的Egress Policy之后，即把封包归还给INPUT链下一条规则处理
  defaultEndpointToHostAction: Drop
  # UDP/TCP的协议端口对，Felix总是允许这些宿主机端点上这些端口的入站流量
  # 这可以防止意外的错误配置导致宿主机断开和外部的连接，默认允许SSH、etcd、BGP、DHCP
  failsafeInboundHostPorts:
  - protocol: tcp
    port: 22
  - protocol: udp
    port: 68
  - protocol: tcp
    port: 179
  - protocol: tcp
    port: 2379
  - protocol: tcp
    port: 2380
  - protocol: tcp
    port: 6666
  - protocol: tcp
    port: 6667
  # UDP/TCP的协议端口对，Felix总是允许这些宿主机端点上针对这些端口的出站流量
  failsafeOutboundHostPorts:
  - protocol: udp
    port: 53
  - protocol: udp
    port: 67
  - protocol: tcp
    port: 179
  - protocol: tcp
    port: 2379
  - protocol: tcp
    port: 2380
  - protocol: tcp
    port: 6666
  - protocol: tcp
    port: 6667
  # 设置为true则允许Felix运行在具有RPF的系统上
  ignoreLooseRPF: false
  # Felix解析宿主机端点时，需要排除的网络接口列表，逗号分隔
  # 默认值是排除K8S内部使用的设备kube-ipvs0
  interfaceExclude: kube-ipvs0
  # 用于区分工作负载端点和宿主机端点的网络接口名前缀
  interfacePrefix: cali
  # 是否在宿主机上配置一个IPinIP网络接口
  # 如果你通过calico/node或calicoctl配置IPIP-enabled pool会自动设置为true
  ipipEnabled：false
  # 上述隧道接口的MTU
  ipipMTU:1440
  # 是否启用IPv6支持
  ipv6Support: false</pre>
<div class="blog_h2"><span class="graybg"><a id="HostEndpoint"></a>HostEndpoint配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: HostEndpoint
metadata:
  # 宿主机端点名称
  name: some.name
  # 标签，用于应用对应的策略
  labels:
    type: production
spec:
  # 端点对应的网络接口
  interfaceName: eth0
  # 端点所在的宿主机
  node: myhost
  # 期望和此网络接口对应的IP地址
  expectedIPs:
  - 192.168.0.1
  - 192.168.0.2
  # 配置，用于应用对应的策略
  profiles:
  - profile1
  - profile2
  # 命名端口，可以在Policy Rule中引用
  ports:
  - name: some-port
    port: 1234
    protocol: TCP
  - name: another-port
    port: 5432
    protocol: UDP</pre>
<div class="blog_h2"><span class="graybg"><a id="WorkloadEndpoint"></a>WorkloadEndpoint</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: WorkloadEndpoint
metadata:
  # 资源名称
  name: node1-k8s-frontend--5gs43-eth0
  # 资源所属名字空间
  namespace: default
  # 资源的标签
  labels:
    app: frontend
    projectcalico.org/namespace: default
    projectcalico.org/orchestrator: k8s
spec:
  # 此端点所属的工作负载的名字
  workload: nginx
  # 工作负载所属的节点
  node: node1
  # 创建此端点的orchestrator
  orchestrator: k8s
  # 容器网络接口名
  endpoint: eth0
  # 工作负载端点的CNI容器ID
  containerID: 1337495556942031415926535
  # 此工作负载端点所在的Pod名称
  pod: my-nginx-b1337a
  # 在宿主机那一端，对接到工作负载的网络接口名
  interfaceName: cali0ef24ba
  # 网络接口的MAC地址
  mac: ca:fe:1d:52:bb:e9
  # 分配到网络接口的CIDR
  ipNetworks:
  - 192.168.0.0/16
  # 此工作负载的出站流量的网关
  ipv4Gateway: 192.168.0.1
  # 分配到此端点的Calico Profile
  profiles:
  - profile1
  # 命名端口列表
  ports:
  - name: some-port
    port: 1234
    protocol: tcp
  - name: another-port
    port: 5432
    protocol: udp
  # 此端点的NAT规则
  ipNATs:
    # 内部IP地址
    internalIP:
    # 外部IP地址
    externalIP:</pre>
<div class="blog_h2"><span class="graybg"><a id="IPPool"></a>IPPool配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: my.ippool-1
spec:
  # IP地址范围，端点IP从中分配
  cidr: 10.1.0.0/16
  # 何时使用IPinIP模式，Always, CrossSubnet, Never
  ipipMode: CrossSubnet
  # 如果启用，则利用该池中IP的容器的出站流量被IP遮掩
  natOutgoing: true
  # 如果禁用，则Calico IPAM不会从该池中分配IP地址
  disabled: false</pre>
<div class="blog_h2"><span class="graybg"><a id="Node"></a>Node配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Node
metadata:
  name: node-hostname
spec:
  bgp:
    # 此节点所属的AS，不指定则使用全局默认AS号
    asNumber: 64512
    # 此节点的IP和子网，会导出，作为该节点上的Calico端点的next-hop
    ipv4Address: 10.244.0.1/24
    ipv6Address: 2001:db8:85a3::8a2e:370:7334/120
    # IP-in-IP隧道中此节点的IP地址
    ipv4IPIPTunnelAddr: 192.168.0.1
  OrchRefs:
    - nodeName: node-hostname
      orchestrator: k8s</pre>
<div class="blog_h2"><span class="graybg"><a id="NetworkPolicy"></a>NetworkPolicy </span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-tcp-6379
  namespace: production
spec:
  # 匹配的端点
  selector: role == 'database'
  # 此策略针对的流量方向
  types:
  - Ingress
  - Egress
  # 入站规则列表（有序）
  ingress:
  # 允许来自frontend的TCP/6379流量
    # 匹配规则时的行为。可选值：Allow, Deny, Log, Pass
  - action: Allow
    # 匹配协议列表，可选值TCP, UDP, ICMP, ICMPv6, SCTP, UDPLite, 1-255
    protocol: TCP
    # 不匹配协议列表，可选值TCP, UDP, ICMP, ICMPv6, SCTP, UDPLite, 1-255
    notProtocol: UDP
    # 源匹配参数
    source:
      # 根据端点选择器匹配
      selector: role == 'frontend'
      # 根据端点所属CIDR匹配
      nets: 172.21.0.0/16
      # 不匹配的CIDR
      notNets: 172.21.0.0/16
      # 根据名字空间匹配，如果指定，则仅仅目标名字空间中的工作负载端点才能匹配
      namespaceSelector: env = 'dev'
      # 根据端点进行匹配
      ports: 0
      notPorts: 0
    # 目的匹配参数
    destination:
      ports:
      - 6379
    # ICMP匹配规则
    icmp:
      type: 0
      code: 0
    # ICMP不匹配规则
    notICMP:
      # ICMP类型， 0-254
      type: 0
      # ICMP代码， 0-255
      code: 0
  # 出站规则列表
  egress:
  - action: Allow</pre>
<p>Calico选择器语法：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">语法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>all()</td>
<td>匹配所有资源</td>
</tr>
<tr>
<td>k == ‘v’</td>
<td>匹配标签k的值为v的资源</td>
</tr>
<tr>
<td>k != ‘v’</td>
<td>匹配具有标签k，且其值不为v的资源</td>
</tr>
<tr>
<td>has(k)</td>
<td>匹配具有标签k的资源</td>
</tr>
<tr>
<td>!has(k)</td>
<td>匹配不具有标签k的资源</td>
</tr>
<tr>
<td>k in { ‘v1’, ‘v2’ }</td>
<td>匹配标签k的值为v1或v2的资源</td>
</tr>
<tr>
<td>k not in { ‘v1’, ‘v2’ }</td>
<td>匹配标签k的值不为v1或v2的资源，或者没有标签k的资源</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"> <a id="Profile"></a>Profile</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Profile
metadata:
  name: profile1
  # 使用此Profile的端点，自动添加如下标签
  labels:
    profile: profile1
spec:
  # 此Profile的网络策略
  ingress:
  - action: Deny
    source:
      nets:
      - 10.0.20.0/24
  - action: Allow
    source:
      selector: profile == 'profile1'
  egress:
  - action: Allow</pre>
<div class="blog_h1"><span class="graybg">calicoctl</span></div>
<p>calicoctl是一个命令行工具，使用它可以创建、修改、删除Calico对象。你可以在任何能够访问Calico数据库的主机上使用该命令。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">cd /usr/local/bin
curl -O -L https://github.com/projectcalico/calicoctl/releases/download/v2.0.0/calicoctl
chmod +x calicoctl</pre>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>很多calicoctl命令都需要访问Calico数据库，你需要通过配置文件来指定数据库的信息：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
  datastoreType: "etcdv3"
  etcdEndpoints: "http://10.96.232.136:6666"</pre>
<p>注意：在Kubeadm部署方式下，Calico不会使用K8S的etcd，而是自己创建一个，执行下面的命令可以查看：</p>
<pre class="crayon-plain-tag">kubectl --namespace=kube-system get service
# calico-etcd ClusterIP 10.96.232.136 &lt;none&gt; 6666/TCP 1h</pre>
<p>如果配置正确，下面的命令将会返回节点列表：</p>
<pre class="crayon-plain-tag">calicoctl get nodes
# NAME         
# xenial-100   
# xenial-101   
# xenial-102   
# xenial-103   
# xenial-104   
# xenial-105</pre>
<div class="blog_h2"><span class="graybg">子命令</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">create</td>
<td>根据指定的文件名或者从标准输入来创建资源</td>
</tr>
<tr>
<td class="blog_h3">replace</td>
<td>替换资源</td>
</tr>
<tr>
<td class="blog_h3">apply</td>
<td>应用资源：如果资源不存在则创建之，如果存在则替换之</td>
</tr>
<tr>
<td class="blog_h3">delete</td>
<td>删除资源</td>
</tr>
<tr>
<td class="blog_h3">get</td>
<td>
<p>获取资源信息，支持的资源包括：</p>
<p style="padding-left: 30px;">bgpConfiguration<br />bgpPeer<br />felixConfiguration<br />globalNetworkPolicy<br />hostEndpoint<br />ipPool<br />networkPolicy<br />node<br />profile<br />workloadEndpoint</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 把默认IP池的配置输出为yaml
calicoctl get ippool -o yaml</pre>
</td>
</tr>
<tr>
<td class="blog_h3">convert</td>
<td>转换不同API版本的配置文件</td>
</tr>
<tr>
<td class="blog_h3">ipam</td>
<td>IP地址管理器相关命令：<br />
<pre class="crayon-plain-tag"># 释放一个原先被IPAM分配的IP地址，释放完毕后此IP地址可以被重用
calicoctl ipam release --ip=192.168.1.2 

# 显示一个IP地址的信息，例如是否被分配，是否被用户或IPAM保留
calicoctl ipam show --ip=192.168.1.1</pre>
</td>
</tr>
<tr>
<td class="blog_h3">node run</td>
<td>
<p>注意：节点的很多配置可以<a href="https://docs.projectcalico.org/v3.2/reference/node/configuration#ip-setting">通过环境变量提供</a>
<p>在当前节点上启动一个calico/node容器实例，以提供Calico网络、网络策略的支持：</p>
<pre class="crayon-plain-tag"># ip/ip6 当前节点的路由IP地址，如果不指定，使用node资源上配置的值
#        如果没有对应的node资源，则尝试自动检测IP地址，如果设置为autodetect则在
#        节点每次启动时强制检测
# as     使用的AS号，如果不指定则使用全局AS号
calicoctl node run [--ip=&lt;IP&gt;] [--ip6=&lt;IP6&gt;] [--as=&lt;AS_NUM&gt;]
# 节点名称，默认为主机名
                     [--name=&lt;NAME&gt;]
# IP地址自动检测方法
# first-found 使用第一个发现的IP地址，如果具有多网卡不建议使用。默认
# can-reach=ip/hostname 使用能到达目标的网络接口
# interface=regrex 网络接口名字正则式
# skip-interface=regrex 排除的网络接口名字正则式
# 示例：
#   --ip-autodetection-method interface=eth.*
#   --ip-autodetection-method interface=eth0
                     [--ip-autodetection-method=&lt;IP_AUTODETECTION_METHOD&gt;]
                     [--ip6-autodetection-method=&lt;IP6_AUTODETECTION_METHOD&gt;]
# 日志存储路径，默认/var/log/calico
                     [--log-dir=&lt;LOG_DIR&gt;]
                     [--node-image=&lt;DOCKER_IMAGE_NAME&gt;]
# BGP后端，gobgp目前试验阶段
                     [--backend=(bird|gobgp|none)]
# 指定配置文件，默认/etc/calico/calicoctl.cfg
                     [--config=&lt;CONFIG&gt;]
# 启动时不创建默认IP池
                     [--no-default-ippools]
                     [--dryrun]
# 执行适当的命令，以配合init system
                     [--init-system]
# 禁用Docker网络
                     [--disable-docker-networking]
# Docker容器中网络接口的名字前缀
                     [--docker-networking-ifprefix=&lt;IFPREFIX&gt;]
                     [--use-docker-networking-container-labels]


# 示例
calicoctl node run --node-image=docker.gmem.cc/calico/node:latest --ip-autodetection-method interface=virbr0</pre>
</td>
</tr>
<tr>
<td>node status</td>
<td>检查节点的状态</td>
</tr>
<tr>
<td>node diags</td>
<td>收集Calico节点的诊断信息</td>
</tr>
<tr>
<td>node checksystem</td>
<td>检查节点内核，是否可以作为Calico节点</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">资源类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">资源类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3"><a href="#Node">node</a></td>
<td>
<p>表示运行Calico的节点，当添加一个主机到Calico集群中后，你需要创建相应的Node资源
<p>当启动一个Calico节点时，它的名称必须和此资源的名称一致</p>
<p>默认情况下，启动一个calico/node实例会自动创建Node资源，使用机器的hostname作为节点名称</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#BGPPeer">bgpPeer</a></td>
<td>
<p>BGP Peer资源代表Calico集群中某个节点与之配对的远程BGP Peer</p>
<p>使用BGP Peer你可以将Calico Newtwork和数据中心结构（例如ToR）连接起来</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#BGPConfiguration">bgpConfiguration</a></td>
<td>
<p>表示集群的BGP相关的选项</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#Felix">felixConfiguration</a></td>
<td>
<p>表示集群的Felix相关的选项</p>
<p>Felix是运行在任何提供端点（Endpoint）的机器（运行VM或容器的节点）上的守护程序，它负责编程式的路由和ACL，以及为机器上端点提供预期连接性所需的任何东西</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#HostEndpoint">hostEndpoint</a></td>
<td>
<p>表示运行Calico的主机上的一个网络接口</p>
<p>每个HostEndpoint可以包含一系列的标签、一个Profile列表，Calico基于这些来应用策略到网络接口。如果没有任何标签、Profile则Calico不会应用任何策略</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#WorkloadEndpoint">workloadEndpoint</a></td>
<td>
<p>表示将基于Calico实现网络连接的VM、容器，连接到它们的宿主机的网络接口</p>
<p>WorkloadEndpoint是具有名字空间的资源，只有相同名字空间内定义的networkPolicy才能应用到其上</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#IPPool">ipPool</a></td>
<td>
<p>表示一个IP集合，工作负载端点的IP从此集合中分配</p>
<p>IP-in-IP隧道可以用在网络结构（network fabric ）强制进行Src/Dest地址检查、并丢弃无法识别的地址的流量的场景下。在某些公有云环境下，你可能无法完全控制网络，特别是<span style="background-color: #c0c0c0;">无法进行网络路由器和Calico节点之间的BGP配对、各Calico节点也不能L2直连</span>，使用IP-in-IP封装可以确保跨工作负载（Inter-workload）流量正常传输</p>
<p>当启用IP-in-IP模式时，Calico在路由到IP池范围内的工作负载IP时，会对IP封包进行一次包装</p>
<p>设置ipipMode=Always，则<span style="background-color: #c0c0c0;">从任何启用Calico的主机发往任何基于Calico网络的容器、虚拟机的流量</span>，都会进行包装</p>
<p>设置ipipMode=CrossSubnet，则可以在下面的场景下优化性能：</p>
<ol>
<li>AWS multi-AZ部署</li>
<li>多组L2直连的Node，通过路由器建立L3连接</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">globalNetworkPolicy</td>
<td>全局性的网络策略，不具有名字空间，应用到任何名字空间中的工作负载端点、主机端点</td>
</tr>
<tr>
<td class="blog_h3"><a href="#NetworkPolicy">networkPolicy</a></td>
<td>
<p>应用到匹配标签选择器的端点的网络策略，网络策略是命名空间内部的资源</p>
<p>网络策略是一个有序的规则的集合，它应用到标签选择器所匹配的端点</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#Profile">profile</a></td>
<td>包含应用到分配了Profile的单个的端点的网络策略</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg"> 常见问题</span></div>
<div class="blog_h2"><span class="graybg">node run后网络不通</span></div>
<p>某次操作意外删除了节点上的calico_node容器，重新执行calicoctl node run恢复容器后，发现此节点无法连接到K8S容器网络，无法ping通任何容器地址。</p>
<p>最终发现，是此节点的配置出现问题（可能是calicoctl node  run的参数不对引发），ipv4Address的掩码错误：</p>
<pre class="crayon-plain-tag"># 应该是 10.0.0.1/16，32明显错误
ipv4Address: 10.0.0.1/32</pre>
<p>除了此节点以外，其上运行的虚拟机出现同样的问题。修复上述掩码后问题消失。</p>
<p>需要注意一点，正常的路由，出口应该是calicoctl node run时检测到的网络接口：</p>
<pre class="crayon-plain-tag">172.27.252.128  radon           255.255.255.192 UG    0      0        0 virbr0</pre>
<p>而出现问题时，该节点上的出口从期望的virbr0变成了tunl0。  </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/calico">基于Calico的CNI</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/calico/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
