「IngressNightmareをめざす異世界泥頭車!一、serviceはなに?!」

17k words

借着这次的CVE-2025-1097、CVE-2025-1098、CVE-2025-24514、CVE-2025-1974填一下19年留下的陈年云相关的老坑,docker部分也同样会找机会补全 (K8s的部分因为没丢到当时的博客导致和早年的固件笔记一起丢了) ,从kubernetes相对基础的部分入手,讲一下常见的核心功能,最后以CVE-2025-1974几个漏洞产生的原理和过程为终点,目的是借着机会复习下k8s的一些组件,因为是摸鱼时候才会写写所以会断断续续的更新这个笔记。(全记到blog这样笔记就不会再丢了)

正文


CVE参考wiz文章内容
https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities

这里着重探讨危害相对较高的CVE-2025-1974XD

首先,需要理解的是CVE-2025-24514,因为CVE-2025-1974..等几个漏洞的利用基本都建立在CVE-2025-24514(Ingress NGINX Admission 处理AdmissionReview请求时字段未过滤,导致可通过注释注入 ) 或类似的思路之上。

而在CVE-2025-24514之前,要先了解Ingress NGINX

站在对外暴露服务的映射角度来看,Ingressservice分别对应了常规7层模型里的七层四层,但其二者在kubernetes中实际架构中是上下层关系,后面也会讲到这部分。

(图片偷自 https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress/
alt text

总之,我会从service开始,因为其相对基础;再经过Ingress NGINXadmission controller概念等,直至延申到后面CVE的内容。

tips


文章中的实验我都会用minikube来做,会方便很多,用法参考下文

https://minikube.sigs.k8s.io/docs/start/?arch=%2Flinux%2Fx86-64%2Fstable%2Fbinary+download#take-the-next-step

其实用docker来拉minikube的话不需要安装这么多依赖,不过还是列出来方便其他部署方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#docker
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker

#conntrack
sudo yum install -y conntrack

#crictl
wget https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.32.0/crictl-v1.32.0-linux-amd64.tar.gz
sudo tar zxvf crictl-v1.32.0-linux-amd64.tar.gz -C /usr/sbin

#cri-dockerd
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.16/cri-dockerd-0.3.16.amd64.tgz
tar -zxvf cri-dockerd-0.3.16.amd64.tgz
sudo cp cri-dockerd /usr/local/bin/cri-dockerd
wget https://raw.githubusercontent.com/Mirantis/cri-dockerd/refs/heads/master/packaging/systemd/cri-docker.service
wget https://raw.githubusercontent.com/Mirantis/cri-dockerd/refs/heads/master/packaging/systemd/cri-docker.service
sudo install ./* /etc/systemd/system
sudo sed -i -e 's,/usr/bin/cri-dockerd,/usr/local/bin/cri-dockerd,' /etc/systemd/system/cri-docker.service
sudo systemctl daemon-reload
sudo systemctl restart cri-docker
#这里因为精神洁癖,喜欢用到的时候再手动开服务就不enable了

#containernetworking-plugins
sudo yum install containernetworking-plugins

#containernetworking-plugins
wget https://github.com/containernetworking/plugins/releases/download/v1.6.2/cni-plugins-linux-amd64-v1.6.2.tgz
CNI_PLUGIN_INSTALL_DIR="/opt/cni/bin"
sudo mkdir -p "$CNI_PLUGIN_INSTALL_DIR"
sudo tar -xf cni-plugins-linux-amd64-v1.6.2.tgz -C "$CNI_PLUGIN_INSTALL_DIR"

这里为了方便我选择docker拉服务,测试环境是ubuntu24.10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
curl -LO https://github.com/kubernetes/minikube/releases/latest/download/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube && rm minikube-linux-amd64

# docker
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo sed -i 's/debian/ubuntu/g' /etc/apt/sources.list.d/docker.list

sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io

#当前用户丢到docker组这里已经丢过了就不记录了

minikube start --driver=docker --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers

# 我这里ipv6不支持所以关一下,不然会影响解析
# sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1

#这里我用的docker因为没有梯子所以用国内源了
minikube start --driver=docker --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers --bas
e-image="registry.cn-hangzhou.aliyuncs.com/google_containers/kicbase:v0.0.46" --memory=2066mb

#装一下kubectl
minikube kubectl -- get pods -A

装完确定pod都活着就ok了

1
2
3
4
5
6
7
8
9
10
test@test:~$  minikube kubectl -- get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-668d6bf9bc-82gm2 1/1 Running 0 10m
kube-system coredns-668d6bf9bc-sxmjq 1/1 Running 0 10m
kube-system etcd-minikube 1/1 Running 0 10m
kube-system kube-apiserver-minikube 1/1 Running 0 10m
kube-system kube-controller-manager-minikube 1/1 Running 0 10m
kube-system kube-proxy-n4r7d 1/1 Running 0 10m
kube-system kube-scheduler-minikube 1/1 Running 0 10m
kube-system storage-provisioner 1/1 Running 1 (10m ago) 10m

service


还是以官方文档对service的介绍为主,

官方对其的定义是

将在集群中运行的应用通过同一个面向外界的端点公开出去,即使工作负载分散于多个后端也完全可行

这里的场景通常是指的一个或一组pod,service可以将这样的一个pod集合公开出去(Cluster IP)用于其他pod交互等,同时,service也定义着外部访问到这些pod集合的策略,*官方文档中将每个定义的service视为一个端点(通常是指pod)的逻辑集合*。

官网文档中举了一个比较实际的例子:前端pod与后端pod之间交互

思考一下,在每个后端pod的ip都不相同的情况下,如果没有service将后端pod集合为一个ip点位,前端pod如何和后端pod进行连接交互。

alt text

总不能像图上这样每个前端都绑定一个后端点位的ip。能不能通信先不说,如果对应的后端点位挂了那前端对应的个pod也就没法用了,有些荒谬的同时这也违背了pod高可用属性的设计初衷。

service的出现就解决了这种问题,通过service我们可以将一组同类型的pod作为集合,映射为一个ip,前端pod与后端pod交互时,并不需要在意自己交互的是哪一个后端pod,因为后端pod随时可能发生变更所以也没必要在意,service 会将流量负载均衡到后端 pod

细一点举个例子比如是 servicenodeport 模式: 请求发送到某个 node节点 上的 nodeport 端口走到kube-proxy再转发到集群中service给其对应的 cluster IP 而后kube-proxy 再根据其负载均衡走到某个node的一个后端服务pod中.

alt text

(nodeport形式)

alt text

需要注意Cluster ip和kube-proxy都是通过api server获取更新的,这里图我是用nodeport模式画的,每个node都会给这个pod集合对应的service开个端口监听,然后收到的请求从nodeport进来会经过kube-proxy转到这个service对应的clusterIP然后再通过其在Endpoints表和具体的负载算法,通过cni(Calico、Flannel)直接走到指定node中的pod

kube-proxy更新频率受到pod迭代影响,其而且其也会通过kubelet监控Endpoints动态更新自己的规则表,通常clusterIP在创建后除非对应serivce删了,不然一般不会变动

service支持多种公开方式或者说服务类型,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ClusterIP
通过集群的内部 IP 公开 Service,选择该值时 Service 只能够在集群内部访问。 这也是你没有为 Service 显式指定 type 时使用的默认值。 你可以使用 Ingress 或者 Gateway API 向公共互联网公开服务。

NodePort
通过每个节点上的 IP 和静态端口(NodePort)公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 Service 配置集群 IP 地址, 相当于你请求了 type: ClusterIP 的 Service。

LoadBalancer
使用云平台的负载均衡器向外部公开 Service。Kubernetes 不直接提供负载均衡组件; 你必须提供一个,或者将你的 Kubernetes 集群与某个云平台集成。

ExternalName
将服务映射到 externalName 字段的内容(例如,映射到主机名 api.foo.bar.example)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。

externalIPs
直接在创建svc时指定一个外部ip.

https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/#external-ips

ClusterIP


首先是默认的ClusterIp,这个是service默认的配置类型,也是其他几个类型的实现基础。

虽然是基础,但他也会更方便理解service的特性,或者说是理解pod与service的关系

这里yaml我的创建两个ng的pod,然后起了个service指关联到这组pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
test@test:~/k8s_yaml$ cat Deployment-nginx-ClusterIP.yaml
apiVersion: apps/v1

kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: registry.cn-hangzhou.aliyuncs.com/acs-sample/nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP

然后拉起来

1
2
3
test@test:~/k8s_yaml$ kubectl apply -f Deployment-nginx-ClusterIP.yaml                                              
deployment.apps/nginx-deployment created
service/nginx-service created

查看当前pod状态,只创建了俩,能看到都正常跑起来了

1
2
3
4
test@test:~/k8s_yaml$ kubectl get pods                                                                                  
NAME READY STATUS RESTARTS AGE
nginx-deployment-6c7848d5b5-fxp6d 1/1 Running 0 9s
nginx-deployment-6c7848d5b5-kdw46 1/1 Running 0 9s

然后是service状态,能看到已经定义的nginx-service已经跑起来了,这里ClusterIP的用处一会讲到

1
2
3
4
test@test:~/k8s_yaml$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d21h
nginx-service ClusterIP 10.107.205.193 <none> 80/TCP 1m

然后就是一个问题,我们要访问到服务的话该如何访问?

有两种方式,分别是

  • 1.访问pod端口

  • 2.访问ClusterIP端口

一、访问pod端口

这里访问pod端口指的是:转发指定一个pod的某个端口至某个地址某个端口

可以用port-forward来将服务端口映射出来

比如这里我们的两个ng的pod

1
2
3
4
test@test:~/k8s_yaml$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-6c7848d5b5-fxp6d 1/1 Running 0 144m
nginx-deployment-6c7848d5b5-kdw46 1/1 Running 0 144m

我们用转发pod的方式先看下

这里选定一个pod将其80端口转发出来,至当前的8888口

1
2
3
test@test:~/k8s_yaml$ kubectl port-forward pod/nginx-deployment-6c7848d5b5-fxp6d --address 0.0.0.0 8888:80 
Forwarding from 0.0.0.0:8888 -> 80
Handling connection for 8888

这时可以访问本地8888端口来访问到pod内80口的应用

1
2
3
4
5
6
7
8
9
10
11
curl 192.168.221.130:8888 -is

HTTP/1.1 200 OK
Server: nginx/1.13.3
Date: Tue, 01 Apr 2025 09:16:37 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 11 Jul 2017 13:06:07 GMT
Connection: keep-alive
ETag: "5964cd3f-264"
Accept-Ranges: bytes

或许也会注意到,采用这种方式,这样仅是用了一个pod,但我们有两个pod,用这种方式是没法做到高可用的。

那service基础的ClusterIP模式,就是为了解决这一问题而出现的.

二、访问ClusterIP端口

1
2
3
4
test@test:~/k8s_yaml$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d21h
nginx-service ClusterIP 10.107.205.193 <none> 80/TCP 1m

可以看下,这里刚创建的name为nginx-service的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test@test:~/k8s_yaml$ kubectl describe svc nginx-service
Name: nginx-service
Namespace: default
Labels: app=nginx
Annotations: <none>
Selector: app=nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.107.205.193
IPs: 10.107.205.193
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.0.12:80,10.244.0.13:80
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>

因为我们在创建这个service时将选择器(Selector)连接指向到了标签为app=nginx的pod

通过这个标签再查询相关的pod,这也正是我们刚创建时候打了app=nginx的这俩pod

1
2
3
4
test@test:~/k8s_yaml$ kubectl get pods --selector='app=nginx'
NAME READY STATUS RESTARTS AGE
nginx-deployment-6c7848d5b5-fxp6d 1/1 Running 0 4h58m
nginx-deployment-6c7848d5b5-kdw46 1/1 Running 0 4h58m

或者这样看也行

1
2
3
test@test:~$ kubectl get endpoints nginx-service
NAME ENDPOINTS AGE
nginx-service 10.244.0.12:80,10.244.0.13:80 5h36m

这里如果通过port-forward直接转发service/nginx-service的端口是不行的,会发现只有一个pod的ng能够收到请求,这是因为端口转发时,kubectl会先查询服务对应的所有 pod,然后随机(或按照一定规则)选择其中一个 pod。

所有经由该port-forward的请求都会被固定地转发到这个选中的pod上,后续请求不会在多个 pod 之间做负载均衡。

基础的ClusterIP类型仅是从集群内部访问时才是轮询负载的,如果要实现外部访问也是负载均衡,则需要选择用建立在其之上的Nodeport类型 or 直接给 svc 指定externalIPsLoadBalancer,亦或者是使用与这个漏洞相关联的ingress,之后都会讲到。

为了方便理解这里就不用kubectl proxy走apiserver访问了,这里我选择临时拉个busybox从集群内访问会更直观一下

1
2
3
test@test:~/k8s_yaml$ kubectl run busybox-pod-1 --rm -it --image=docker.m.daocloud.io/library/busybox:latest -- /bin/sh
If you don't see a command prompt, try pressing enter.
/ #

确定一下svc的ip 10.105.131.184

1
nginx-service   ClusterIP   10.105.131.184   <none>        80/TCP    56m

然后从busyboxpod对nginx-serviceClusterIP发起访问

1
/ # for i in `seq 0 60`;do wget  10.105.131.184:80 1>&2 2>/dev/null ;done
1
kubectl logs 两个pod_name

查看两个pod日志,会发现请求的流量是基本均匀分布开的。

alt text

ClusterIP服务本身没有负载均衡功能,这部分通过是kube-proxy来负载到后面pod的,kube-proxy会捕获到至ClusterIP的流量,依照当前ipvs或者iptables的网络规则匹配目的地址和端口,并依据负载规则决定将请求重定向至哪个目的pod。

参考官方文档对于这部分的阐述

https://kubernetes.io/docs/reference/networking/virtual-ips/

这里kube-proxy如果用的iptables是仅支持轮询的负责方式,而ipvs则玩的会比较花,相对的service越多的话iptables的性能会更显著一些。

这里我因为是docker拉的 k8s 所以需要看下kube-proxy-pod的信息才能确认用的是ipvs还是iptables.

1
2
3
4
5
test@test:~/k8s_yaml$ kubectl describe pods kube-proxy-n4r7d -n kube-system|grep -i command -A10
Command:
/usr/local/bin/kube-proxy
--config=/var/lib/kube-proxy/config.conf
--hostname-override=$(NODE_NAME)

这里并没有直接通过--proxy-mode指定模式,所以还要进一步看config.conf文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
test@test:~/k8s_yaml$ kubectl exec -n kube-system kube-proxy-n4r7d --  cat /var/lib/kube-proxy/config.conf
apiVersion: kubeproxy.config.k8s.io/v1alpha1
bindAddress: 0.0.0.0
bindAddressHardFail: false
clientConnection:
acceptContentTypes: ""
burst: 0
contentType: ""
kubeconfig: /var/lib/kube-proxy/kubeconfig.conf
qps: 0
clusterCIDR: 10.244.0.0/16
configSyncPeriod: 0s
conntrack:
maxPerCore: 0
min: null
tcpBeLiberal: false
tcpCloseWaitTimeout: 0s
tcpEstablishedTimeout: 0s
udpStreamTimeout: 0s
udpTimeout: 0s
detectLocal:
bridgeInterface: ""
interfaceNamePrefix: ""
detectLocalMode: ""
enableProfiling: false
healthzBindAddress: ""
hostnameOverride: ""
iptables:
localhostNodePorts: null
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0s
ipvs:
excludeCIDRs: null
minSyncPeriod: 0s
scheduler: ""
strictARP: false
syncPeriod: 0s
tcpFinTimeout: 0s
tcpTimeout: 0s
udpTimeout: 0s
kind: KubeProxyConfiguration
logging:
flushFrequency: 0
options:
json:
infoBufferSize: "0"
text:
infoBufferSize: "0"
verbosity: 0
metricsBindAddress: 0.0.0.0:10249
mode: ""
nftables:
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0s
nodePortAddresses: null
oomScoreAdj: null
portRange: ""
showHiddenMetricsForVersion: ""
winkernel:
enableDSR: false
forwardHealthCheckVip: false
networkName: ""
rootHnsEndpointName: ""

能看到mode:并没有给值,所以是默认的iptables,这里iptabes只支持基础的负载

1) iptables


1
在此模式下,kube-proxy 使用内核 netfilter 子系统的 iptables API 配置数据包转发规则。对于每个端点,它会安装 iptables 规则,默认情况下会随机选择一个后端 Pod

iptables也只能在conf改动一部分规则同步优化的内容,这里面用的相对多的是 minSyncPeriodmasqueradeAllsyncPeriod其他的其实用的比较少,但可见并没有负载相关的配置供修改

这部分可以参考下文

https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/#kubeproxy-config-k8s-io-v1alpha1-KubeProxyIPTablesConfiguration

南无三、此间事于《库伯-普肉瑟斯/挨批忒宝斯》源码中——壹仟陆佰叁拾行间亦有记载

https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go#L1630

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	// Now write loadbalancing rules.
numEndpoints := len(endpoints)
for i, ep := range endpoints {
epInfo, ok := ep.(*endpointInfo)
if !ok {
continue
}
comment := fmt.Sprintf(`"%s -> %s"`, svcPortNameString, epInfo.String())

args = append(args[:0], "-A", string(svcChain))
args = proxier.appendServiceCommentLocked(args, comment)
if i < (numEndpoints - 1) {
// Each rule is a probabilistic match.
args = append(args,
"-m", "statistic",
"--mode", "random", // 这里指定死了是random
"--probability", proxier.probability(numEndpoints-i))
}
// The final (or only if n == 1) rule is a guaranteed match.
natRules.Write(args, "-j", string(epInfo.ChainName))
}
}

那有没有在这部分能弥补处理负载策略的模式呢

有的,兄弟有的

iptables相对的ipvs则可以按照我们需求做出特定负载模式处理。

2025年4月7日留: service部分没写完,下一次摸鱼时候会更新

2) ipvs


官网的文档对于 IPVS 的相关资料并不怎么细 (这一点在其他功能点也是通病,不知道之后会不会补全文档)

https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/#kubeproxy-config-k8s-io-v1alpha1-KubeProxyIPVSConfiguration

不过倒是有收录一篇华为的研究文章,有对选项和处理逻辑之类做出解释

https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/

这里需要简单引入一下 kube-proxyipvs 概念。

我想了下还是通过与 iptables 对比的方式可能会更直观,方便理解一些.

注意:下文中如果出现 VIP 则是代指 ClusterIPexternalIP


首先 IPVS 对比 iptables 比较核心的差异点:

  1. 首当其冲的是允许指定负载算法,不论是 kube-proxy/config.conf 还是直接运行时候命令行给参,都可以通过指定 scheduler字段,来选择负载方式。

    1
    2
    3
    4
    5
    6
    rr:循环
    lc:最少连接
    dh:目标哈希
    sh:源散列
    sed:最短预期延迟
    nq:永不排队
  2. 然后是运作方式的区别,ipvs 不同于 iptables 直接在 workNode 上起给当前节点工作的pod都起一个对应的 iptables 规则,IPVS 采用了对于 svc每个 ClusterIPexternalIP 都单独起一个 LVS,其会关联到这一个 svc IP+端口,并后面也关联到对应的所有 pod IP与服务端口,而后在 LVS 中会建立对应 pod ip端口 的hash表.

    对于 客户端 发起请求匹配的时候不会像 iptables 那样链式匹配工作节点上的 iptables 规则链,所以 iptables 规则数量增多时,匹配效率可能下降;

    ipvsLVS 中采用hash表的匹配模式,当 客户端请求 到了 VIP 其对应的 LVS 会查询的hash表中的 ip+端口 对,再匹配到后端 pod .

    还是以 service 为例,这里文章作者其实给了一个 svc nodePort 的例子,比如起了一个 svc 绑定了一组 pods ,比如pods 里有三个 pod , 并且 svc 指定了 nodePort 模式,那如果 kubeproxyiptables 模式,每个工作节点上的 iptables 表都会新增三条规则. 之后如果又有一个新增的 svc,那每个工作节点会新增这个 svc 对应的 pod 数量至少相等的规则链。

    这也就使得在多服务的大集群下,一个工作节点的iptables可能会由kubeproxy维护并负责转发着上千个pod 对应的规则链,这样业务越大,越会影响到匹配规则链的效率,同时因为kubeproxy要介入请求转发,还会产生性能瓶颈。

    而对应这种面对大量的规则的场景,IPVS 通过使用hash表来缓解了这部分查询的开销,同时 当请求进入 vipipvs 会直接查询对应的hash表,然后进行请求转发,!是的,kube-proxy!在 ipvs 的情况下不需要直接参与到转发工作了,kube-proxy 主要工作回到了监听 api-server 及时关注 ServiceEndpoint ,并更新、创建、检查 IPVS 虚拟服务器的规则。

    但文章中也表述了,

「给还没有吃上饭的 svc 带去! 对应的 虚拟服务器! 」 此间事于库伯普肉瑟斯/挨批威埃斯——proxierL1775行亦有记载

https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/proxier.go#L1775C1-L1783C15

1
2
3
4
5
6
7
8
9
10
func (proxier *Proxier) syncService(svcName string, vs *utilipvs.VirtualServer, bindAddr bool, alreadyBoundAddrs sets.Set[string]) error {
appliedVirtualServer, _ := proxier.ipvs.GetVirtualServer(vs)
if appliedVirtualServer == nil || !appliedVirtualServer.Equal(vs) {
if appliedVirtualServer == nil {
// IPVS service is not found, create a new service
proxier.logger.V(3).Info("Adding new service", "serviceName", svcName, "virtualServer", vs)
if err := proxier.ipvs.AddVirtualServer(vs); err != nil {
proxier.logger.Error(err, "Failed to add IPVS service", "serviceName", svcName)
return err
...

其实能看到他函数名是 syncService(),说明这个其实是在每次同步 service时,如果没有找到 svc 对应的GetVirtualServer 就会去调 AddVirtualServer() 给创建个新的

对于 AddVirtualServer() 他走ipvs.go的接口,最终调用到的这里

https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/util/ipvs_linux.go

那这时候可能会有疑问

「阿达西,你的ipvs,我的挨个雄鹰一样的vip创建的呢,去哪了!」——出自《生姜男人必修课》

这个需要往上走,其实来看谁调用的syncService(),就会发现它有个轮询 svcPortMap

https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/proxier.go#L1010

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//这里会轮询服务映射,其中会包含这个svc的信息,包含 VIP 端口等
for svcPortName, svcPort := range proxier.svcPortMap {
svcInfo, ok := svcPort.(*servicePortInfo)
if !ok {
proxier.logger.Error(nil, "Failed to cast serviceInfo", "servicePortName", svcPortName)
continue
}

protocol := strings.ToLower(string(svcInfo.Protocol()))
// Precompute svcNameString; with many services the many calls
// to ServicePortName.String() show up in CPU profiles.
svcPortNameString := svcPortName.String()

......
// 下面这里调用了 `syncService` 来创建服务
if err := proxier.syncService(svcPortNameString, serv, true, alreadyBoundAddrs); err == nil {
activeIPVSServices.Insert(serv.String())
activeBindAddrs.Insert(serv.Address.String())

那话说回来有没有 ipvs 相较于 iptables 办不到的事?

有的兄弟!有的

https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/#iptables-ipset-in-ipvs-proxier

IPVS is for load balancing and it can’t handle other workarounds in kube-proxy, e.g. packet filtering, hairpin-masquerade tricks, SNAT, etc.

IPVS 主要用于负载均衡,它无法处理 kube-proxy 中的其他功能,例如数据包过滤、hairpin-masquerade 、SNAT 等

hairpin-masquerade :一个 Pod 通过 Service 的 ClusterIP 访问自身时,需要进行特殊的网络地址转换,IPVS 不支持这种处理。

这部分还是由 iptables 来处理的。

tips:

1
2
3
4
5
6
7
ipvsadm -Ln #可以查看当前IPVS规则
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.1:443 rr
-> 192.168.1.101:6443 Masq 1 0 0
-> 192.168.1.102:6443 Masq 1 0 0

最后一次更新于2025年4月14日,service还是没写完,下次摸鱼再更。