Come preservare l'IP sorgente delle richieste dopo il bilanciamento del carico in un cluster K8s

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:

  1. 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.
  2. 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.

Limitazioni del percorso di accesso

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 campo X-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: Local può 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: Local può preservare l’IP sorgente e mantenere la capacità di bilanciamento del carico.

Riferimenti