Come preservare l'IP sorgente delle richieste dopo il bilanciamento del carico in un cluster K8s
Categories:
Introduzione
Il distribuzione delle applicazioni non è sempre un semplice installazione e esecuzione, a volte è necessario considerare anche i problemi di rete. Questo articolo introdurrà come far sì che i servizi in un cluster k8s possano ottenere l’IP sorgente della richiesta.
I servizi delle applicazioni dipendono generalmente dalle informazioni in ingresso; se le informazioni in ingresso non dipendono dalla quintupla (IP sorgente, porta sorgente, IP destinazione, porta destinazione, protocollo), allora quel servizio ha una bassa accoppiamento con la rete e non ha bisogno di preoccuparsi dei dettagli di rete.
Pertanto, per la maggior parte delle persone non è necessario leggere questo articolo; se sei interessato alla rete o desideri ampliare un po’ i tuoi orizzonti, puoi continuare a leggere il resto, per conoscere più scenari di servizio.
Questo articolo è basato su k8s v1.29.4; alcune descrizioni nel testo mescolano pod ed endpoint, in questo scenario possono essere considerati equivalenti.
Se ci sono errori, benvenuti i suggerimenti di correzione, li aggiornerò tempestivamente.
Perché l’informazione sull’IP sorgente viene persa?
Chiariremo prima cos’è l’IP sorgente: quando A invia una richiesta a B e B inoltra la richiesta a C, sebbene C veda l’IP sorgente del protocollo IP come l’IP di B, in questo articolo l’IP di A è considerato l’IP sorgente.
Ci sono principalmente due tipi di comportamenti che causano la perdita delle informazioni sorgente:
- Network Address Translation (NAT), lo scopo è risparmiare IPv4 pubblici, bilanciamento del carico, ecc. Questo farà sì che il server veda l’IP del dispositivo NAT come IP sorgente, non l’IP sorgente reale.
- Proxy, reverse proxy (RP, Reverse Proxy) e load balancer (LB, Load Balancer) appartengono a questa categoria, di seguito chiamati uniformemente server proxy. Questi servizi proxy inoltreranno la richiesta al servizio backend, ma sostituiranno l’IP sorgente con il proprio IP.
- Il NAT, in breve, è scambiare spazio porte con spazio IP; gli indirizzi IPv4 sono limitati, un IP può mappare 65535 porte, nella maggior parte dei casi queste porte non sono esaurite, quindi più subnet IP possono condividere un IP pubblico, distinguendo i servizi diversi sulle porte. La forma d’uso è:
public IP:public port -> private IP_1:private port, per maggiori dettagli consulta Network Address Translation - I servizi proxy sono per nascondere o esporre; i servizi proxy inoltreranno la richiesta al servizio backend, sostituendo contemporaneamente l’IP sorgente con il proprio IP, per nascondere l’IP reale del servizio backend e proteggere la sicurezza del servizio backend. La forma d’uso dei servizi proxy è:
client IP -> proxy IP -> server IP, per maggiori dettagli consulta Proxy
NAT e server proxy sono molto comuni, la maggior parte dei servizi non può ottenere l’IP sorgente della richiesta.
Queste sono le due comuni vie per modificare l’IP sorgente; benvenuti integrazioni se ce ne sono altre.
Come preservare l’IP sorgente?
Ecco un esempio di richiesta HTTP:
| Campo | Lunghezza (byte) | Offset bit | Descrizione |
|---|---|---|---|
| Intestazione IP | |||
IP sorgente | 4 | 0-31 | Indirizzo IP del mittente |
| IP destinazione | 4 | 32-63 | Indirizzo IP del ricevente |
| Intestazione TCP | |||
| Porta sorgente | 2 | 0-15 | Numero porta mittente |
| Porta destinazione | 2 | 16-31 | Numero porta ricevente |
| Numero di sequenza | 4 | 32-63 | Per identificare il flusso di byte inviati dal mittente |
| Numero di conferma | 4 | 64-95 | Se impostato il flag ACK, è il numero di sequenza successivo atteso |
| Offset dati | 4 | 96-103 | Numero di byte della posizione di inizio dati rispetto all’intestazione TCP |
| Riservato | 4 | 104-111 | Campo riservato, non utilizzato, impostato a 0 |
| Flag | 2 | 112-127 | Vari flag di controllo, come SYN, ACK, FIN, ecc. |
| Dimensione finestra | 2 | 128-143 | Quantità di dati che il ricevente può ricevere |
| Checksum | 2 | 144-159 | Per rilevare se i dati hanno errori durante la trasmissione |
| Puntatore urgente | 2 | 160-175 | Posizione dei dati urgenti che il mittente spera il ricevente processi rapidamente |
| Opzioni | Variabile | 176-… | Potrebbe includere timestamp, lunghezza massima segmento, ecc. |
| Intestazione HTTP | |||
| Riga di richiesta | Variabile | …-… | Include metodo richiesta, URI e versione HTTP |
Campi intestazione | Variabile | …-… | Contiene vari campi intestazione, come Host, User-Agent, ecc. |
| Riga vuota | 2 | …-… | Per separare intestazione e corpo |
| Corpo | Variabile | …-… | Corpo opzionale della richiesta o risposta |
Esaminando la struttura della richiesta HTTP sopra, si nota che opzioni TCP, riga di richiesta, campi intestazione, corpo sono variabili; lo spazio opzioni TCP è limitato e generalmente non usato per trasmettere IP sorgente, la riga di richiesta ha informazioni fisse non estendibili, il corpo HTTP crittografato non può essere modificato, solo i campi intestazione HTTP sono adatti per estensione e trasmissione dell’IP sorgente.
Nell’header HTTP si può aggiungere il campo X-REAL-IP per trasmettere l’IP sorgente; questa operazione è solitamente sul server proxy, poi il server proxy invierà la richiesta al servizio backend, e il servizio backend potrà ottenere l’informazione IP sorgente tramite questo campo.
Nota: è necessario garantire che il server proxy sia prima del dispositivo NAT, in modo da ottenere il whoami sorgente reale della richiesta. Nei prodotti Alibaba Cloud possiamo vedere il prodotto Load Balancer come categoria separata, la sua posizione in rete è diversa da quella di un server applicativo ordinario.
Guida operativa K8S
Usando il progetto whoami come esempio per il deployment.
Creare Deployment
Creare prima il servizio:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: docker.io/traefik/whoami:latest
ports:
- containerPort: 8080
Questo creerà un Deployment contenente 3 Pod, ogni pod contiene un contenitore che esegue il servizio whoami.
Creare Service
Si può creare un servizio di tipo NodePort o LoadBalancer per accesso esterno, o un servizio di tipo ClusterIP solo per accesso interno al cluster, poi aggiungere un servizio Ingress per esporre l’accesso esterno.
NodePort può essere accessibile sia tramite NodeIP:NodePort che tramite servizio Ingress, comodo per test; questa sezione usa il servizio NodePort.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
Dopo aver creato il servizio, accedendo con curl whoami.example.com:30002, si vede che l’IP restituito è il NodeIP, non il whoami sorgente della richiesta.
Nota: questo non è il corretto IP cliente, sono IP interni del cluster. Ecco cosa succede:
- Il cliente invia il pacchetto a node2:nodePort
- node2 sostituisce l’IP sorgente del pacchetto con il proprio indirizzo IP (SNAT)
- node2 sostituisce l’IP destinazione del pacchetto con l’IP Pod
- Il pacchetto viene instradato a node1, poi all’endpoint
- La risposta del Pod viene instradata indietro a node2
- La risposta del Pod viene inviata al cliente
Rappresentato in figura:

Configurare externalTrafficPolicy: Local
Per evitare questa situazione, Kubernetes ha una funzionalità per preservare l’IP sorgente cliente. Se si imposta service.spec.externalTrafficPolicy su Local, kube-proxy proxyerà le richieste solo agli endpoint locali, senza inoltrare il traffico ad altri nodi.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
Testando con curl whoami.example.com:30002, quando whoami.example.com mappa su IP di più nodi del cluster, c’è una certa probabilità di non poter accedere. È necessario confermare che il record DNS contenga solo l’IP del nodo dove si trova l’endpoint (pod).
Questa configurazione ha un costo, ovvero la perdita della capacità di bilanciamento del carico nel cluster; il cliente ottiene risposta solo accedendo al nodo dove è deployato l’endpoint.

Quando il cliente accede al Nodo 2, non ci sarà risposta.
Creare Ingress
La maggior parte dei servizi forniti agli utenti usa http/https, la forma https://ip:port potrebbe sembrare strana agli utenti. Generalmente si usa Ingress per bilanciare il servizio NodePort creato sopra su porta 80/443 di un dominio.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
namespace: default
spec:
ingressClassName: external-lb-default
rules:
- host: whoami.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami-service
port:
number: 80
Dopo l’applicazione, testando con curl whoami.example.com, si vede che ClientIP è sempre l’IP del Pod Ingress Controller sul nodo dell’endpoint.
root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...
root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7 1/1 Running 0 3d2h 10.42.1.10 k3s-agent-1 <none> <none>
Usando Ingress come reverse proxy per il servizio NodePort, ovvero due layer di service prima dell’endpoint; il diagramma seguente mostra la differenza tra i due.
graph LR
A[Client] -->|whoami.example.com:80| B(Ingress)
B -->|10.43.38.129:32123| C[Service]
C -->|10.42.1.1:8080| D[Endpoint]graph LR
A[Client] -->|whoami.example.com:30001| B(Service)
B -->|10.42.1.1:8080| C[Endpoint]Nel percorso 1, quando si accede esternamente a Ingress, il primo endpoint raggiunto è Ingress Controller, poi l’endpoint whoami.
E Ingress Controller è sostanzialmente un servizio LoadBalancer,
kubectl -n ingress-nginx get svc
NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
default echoip-ingress nginx ip.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 18h
default whoami-ingress nginx whoami.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 16h
Pertanto, impostando externalTrafficPolicy menzionato prima sul Controller Ingress si può preservare l’IP sorgente.
Inoltre, è necessario impostare use-forwarded-headers su true nel configmap di ingress-nginx-controller, in modo che Ingress Controller possa riconoscere i campi X-Forwarded-For o X-REAL-IP.
apiVersion: v1
data:
allow-snippet-annotations: "false"
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
La differenza principale tra servizio NodePort e servizio ingress-nginx-controller è che il backend di NodePort di solito non è deployato su ogni nodo, mentre il backend di ingress-nginx-controller è di solito deployato su ogni nodo esposto esternamente.
Diversamente dal servizio NodePort dove impostare externalTrafficPolicy causa mancata risposta per richieste cross-node, Ingress può impostare prima l’HEADER e poi proxyare/inoltrare, realizzando sia la preservazione IP sorgente che il bilanciamento del carico.
Conclusione
- Network Address Translation (NAT), Proxy, Reverse Proxy, Load Balancer causeranno la perdita dell’IP sorgente.
- Per prevenire la perdita dell’IP sorgente, durante il forwarding del server proxy impostare l’IP reale nel campo intestazione HTTP
X-REAL-IP, trasmesso tramite servizio proxy. Se multi-layer proxy, usare il campoX-Forwarded-For, che registra in forma di stack l’IP sorgente e la lista IP del percorso proxy. - Il servizio NodePort del cluster impostato su
externalTrafficPolicy: Localpuò preservare l’IP sorgente, ma perderà la capacità di bilanciamento del carico. - ingress-nginx-controller deployato in forma daemonset su tutti i nodi con ruolo loadbalancer, impostando
externalTrafficPolicy: Localpuò preservare l’IP sorgente e mantenere la capacità di bilanciamento del carico.