# 理解 K8s 如何處理不同類型 service 的封包來源 IP
在 Kubernetes 叢集中執行的應用程式會透過服務抽象(Service abstraction)來尋找彼此並與外界溝通。 本文件將說明傳送至不同類型服務的封包的來源 IP 會如何處理,以及您如何根據需求切換此行為。
在 Kubernetes 叢集中運行的應用程式,透過 Service(服務)的抽象概念來找到彼此並進行溝通,也可以和外部世界連接。這份文件將解釋,當封包發送到不同類型的 Service 時,來源 IP(Source IP)會發生什麼變化,以及如何根據需求調整這種行為。
## 術語
[**NAT**](https://en.wikipedia.org/wiki/Network_address_translation)
網路位址轉換
[**Source NAT**](https://en.wikipedia.org/wiki/Network_address_translation#SNAT)
取代封包上的來源 IP;在本頁中,通常是指取代為 node 的 IP 位址。
[**Destination NAT**](https://en.wikipedia.org/wiki/Network_address_translation#DNAT)
取代封包上的目的地 IP;在本頁中,這通常是指取代為 Pod 的 IP 位址
[**VIP**](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies)
虛擬 IP 位址,例如指定給 Kubernetes 中每個 Service 的虛擬 IP 位址
[**kube-proxy**](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies)
一個網路背景程序,負責在每個 node 上協調管理 Service VIP(虛擬 IP)。
## 目標
- 透過各種不同類型的 Service 將簡單應用程式對外公開
- 瞭解每種 Service 類型如何處理來源 IP 的 NAT(網路地址轉換)
- 瞭解在保留來源 IP 時涉及的權衡取捨
## 建立測試的應用程式
```!
$ kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10
```
執行結果 :
```
deployment.apps/source-ip-app created
```
## Source IP for Services with Type=ClusterIP
如果您在叢集中運行 kube-proxy 並使用預設的 **iptables mode**,從叢集內部發送到 **ClusterIP** 的封包,不會進行來源 NAT(Source NAT)。您可以透過查詢 kube-proxy 所在節點上的網址 `http://localhost:10249/proxyMode`,來確認 kube-proxy 的運行模式。
```
$ curl http://localhost:10249/proxyMode
```
執行結果 :
```
ipvs
```
> 如果使用 [Cilium 取代 kube-proxy](https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#kubernetes-without-kube-proxy),則此命令將不適用。
您可以透過為來源 IP 測試應用程式建立一個 Service,來測試來源 IP 的保留功能 :
```!
$ kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080
```
執行結果 :
```
service/clusterip exposed
```
查看 Service 資訊
```
$ kubectl get svc clusterip
```
執行結果 :
```
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
clusterip ClusterIP 172.100.53.4 <none> 80/TCP 28s
```
在同一個 K8s 叢集中,從某個 Pod 發送請求到 ClusterIP 的 Service
```!
$ kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm
```
輸出與此類似:
```
If you don't see a command prompt, try pressing enter.
/ #
```
然後,您可以在 Pod 內執行指令:
```
# Run this inside the terminal from "kubectl run"
ip addr
```
執行結果 :
```
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
3: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1480 qdisc noqueue qlen 1000
link/ether 2e:2b:01:66:08:1b brd ff:ff:ff:ff:ff:ff
inet 172.99.236.23/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::2c2b:1ff:fe66:81b/64 scope link
valid_lft forever preferred_lft forever
```
...然後使用 `wget` 查詢本機 webserver
```!
# Replace "172.100.53.4" with the IPv4 address of the Service named "clusterip"
wget -qO - 172.100.53.4
```
執行結果 :
<pre>
...
Request Information:
<font color=red>client_address=172.99.236.23</font>
method=GET
...
</pre>
**無論客戶端 Pod 和 server 端 Pod 是否位於同一個節點,`client_address`(客戶端位址)始終會顯示為客戶端 Pod 的 IP 地址。這表示,當您從叢集中的一個 Pod 向 ClusterIP 發送請求時,來源 IP 並未被修改**。
在 node 使用 `wget` 查詢本機 webserver
```
curl 172.100.53.4
```
執行結果 :
```
...
Request Information:
client_address=172.20.1.11
method=GET
...
```
---
## Source IP for Services with Type=NodePort
傳送至 `Type=NodePort` 服務的封包預設為來源 NAT。 您可以建立一個 NodePort 服務來測試:
```!
$ kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort
```
執行結果 :
```
service/nodeport exposed
```
現在您可以嘗試從**群集外部**透過上述分配的節點連接埠連線到服務。
```!
$ NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
$ NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')
$ for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done
```
輸出類似於:
```
client_address=172.20.22.11
```
請注意,這些不是正確的用戶端 IP,而是群集內部 IP。 這就是所發生的事:
* 用戶端傳送封包到 `node2:nodePort`
* 節點 2 以自己的 IP 位址取代封包中的來源 IP 位址 (SNAT)
* 節點 2 以 pod IP 取代封包上的目的地 IP
* 封包會傳送到節點 1,然後傳送到端點
* pod 的回覆會傳回到節點 2
* pod 的回覆會傳回到用戶端

為了避免這種情況,Kubernetes 有一個保留 Client 端 source IP 的功能。 如果您將 `service.spec.externalTrafficPolicy` 設定為 `Local` 值,kube-proxy 只會 proxies proxy requests to local endpoints,而不會將流量轉送至其他節點。 此方法可保留原始來源 IP 位址。 如果沒有 local endpoints,傳送到節點的封包會被丟棄,因此您可以在任何封包處理規則中依賴正確的原始 IP 位址,這些規則可能會應用於傳送到端點的封包。
設定 `service.spec.externalTrafficPolicy` 欄位如下:
```bash!
$ kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'
```
輸出類似於:
```
service/nodeport patched
```
現在,重新執行測試:
```bash!
$ for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done
```
輸出類似於:
```
client_address=172.20.22.11
```
請注意,您只從端點 Pod 執行的節點得到一個回覆,並且是正確的 Client IP。
這就是發生的事情:
* 客戶端傳送封包到 `node2:nodePort`,而 `node2:nodePort` 沒有任何 endpoints
* 封包被丟棄
* 用戶端傳送封包到 `node1:nodePort`,而 `node1:nodePort` 確實有 endpoints
* node1 路由封包到具有正確來源 IP 的端點

---
## Source IP for Services with Type=LoadBalancer
傳送至 Type=LoadBalancer 的服務的封包,預設會進行來源 NAT,因為所有處於 Ready 狀態的可排程 Kubernetes 節點,都符合負載平衡流量的資格。 因此,如果封包到達沒有端點的節點,系統會將其代理到有端點的節點,並以節點的 IP 取代封包上的來源 IP (如上一節所述)。
您可以透過負載平衡器暴露來源 IP 應用程式來測試:
```bash!
$ kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer
```
輸出為 :
```
service/loadbalancer exposed
```
列印服務的 IP 位址:
```
$ kubectl get svc loadbalancer
```
輸出與此類似 :
```
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
loadbalancer LoadBalancer 10.43.205.52 172.20.22.199 80:30729/TCP 8m24s
```
接下來,傳送一個要求到此服務的 external-ip:
```
curl 172.20.22.199
```
輸出與此類似:
```
client_address=172.20.22.11
```
但是,如果您在 Google Kubernetes Engine/GCE 上執行,將相同的 `service.spec.externalTrafficPolicy` 欄位設定為 `Local`,就會強制沒有 Service 端點的節點,透過故意使健康檢查失敗的方式,將自己從符合負載平衡流量資格的節點清單中移除。

您可以透過設定註解來測試:
```bash!
$ kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'
```
您應該會立即看到 Kubernetes 分配的 `service.spec.healthCheckNodePort` 欄位:
```bash!
$ kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort
```
輸出與此類似:
```
healthCheckNodePort: 30865
```
`service.spec.healthCheckNodePort` 欄位會指向每個節點上為 `/healthz` 提供健康檢查服務的連接埠。 您可以測試這一點:
```bash!
$ kubectl get pod -o wide -l app=source-ip-app
```
輸出與此類似:
```
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
source-ip-app-75c58c94c6-m57pb 1/1 Running 0 99m 10.42.0.132 antony-rancher <none> <none>
```
使用 `curl` 在不同節點上取得 `/healthz` endpoint :
```
# Run this locally on a node you choose
$ curl localhost:30865/healthz
```
輸出與此類似:
```
{
"service": {
"namespace": "default",
"name": "loadbalancer"
},
"localEndpoints": 1,
"serviceProxyHealthy": true
}
```
在不同的節點上,您可能會得到不同的結果:
```
# Run this locally on a node you choose
$ curl localhost:30865/healthz
```
輸出與此類似:
```
No Service Endpoints Found
```
## Cleaning up
```bash!
# Delete the Services:
$ kubectl delete svc -l app=source-ip-app
# Delete the Deployment, ReplicaSet and Pod:
$ kubectl delete deployment source-ip-app
```