在 Kubernetes 內取得使用者 IP - HTTP Loadbalancer

介紹如何在 GKE 上透過 HTTP LB 取得使用者真實 IP

ta-ching chen

2 minute read

 文章目錄

前言 

對於提供 HTTP 服務的系統來說,取得來源 IP 方式有兩種:

  • 利用封包標頭取得來源 IP

此方案是直接讀取封包的來源 IP,但由於容器和外界溝通不像傳統 Linux 主機有實體網卡對接,而是透過一系列的 NAT 規則置換封包標頭後才傳進容器內 (Understand container communication),導致取得錯誤的使用者 IP。

此方案則是利用 PROXY Protocol,此方案是讓 Proxy Server 將 IP 附加在 HTTP 標頭 X-Forwarded-For 內,因此該標頭內的第一個位址即是使用者的真實 IP。

X-Forwarded-For:[61.219.125.41, 10.140.0.2]

下面會介紹在 GKE (Google Container Engine) 上透過 L7 HTTP Load Balancer 取得使用者真實 IP。

架構說明 

下圖為目前 Google 支援三種 LB

GCP LB Type

在 GKE 上新增 spec.type=LoadBalancerService,Kubernetes 會協助建立一組擁有獨立 IP 位址的 L4 TCP Load Balancer,因此無法支援 L7 應用層的 PROXY Protocol。

L4 TCP LB

為此我們必須建立 Layer 7 的 HTTP Load Balancer,將其先連接到 NGINX instance group 再導向後方的 Kubernetes 叢集內。由於 HTTP 請求會先經過 NGINX reverse proxy,此時使用者 IP 會被紀錄在 X-Forwarded-For 內。

L7 HTTP LB

在 GCP 上建立 HTTP Load Balancer 的方式請參考 Setting Up HTTP(S) Load Balancing

下面附上 NGINX 設定檔以及應用端取得 IP 的程式。

NGINX 設定 

## /etc/nginx/nginx.conf

worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;

    keepalive_timeout  65;

    gzip  on;
    client_max_body_size 5M;
    include /etc/nginx/conf.d/*.conf;

    #additional config
    include /etc/nginx/extra-conf.d/*;
}
## /etc/nginx/conf.d/realip.conf

upstream realip {
  server server <k8s service LB ip>;
}

server {
  server_name _;
  listen 80;

  location / {
    proxy_set_header           Host $host;
    proxy_set_header           X-Real-IP $remote_addr;
    proxy_set_header           X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header           X-Forwarded-Proto $scheme;
    proxy_pass                 http://realip;
    proxy_connect_timeout      10;
    proxy_send_timeout         10;
    proxy_read_timeout         10;
    proxy_buffer_size          4k;
    proxy_buffers              4 32k;
    proxy_busy_buffers_size    64k;
    proxy_temp_file_write_size 64k;
    access_log on;
  }
}

應用端 

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/tomasen/realip"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        host := os.Getenv("HOSTNAME")
        reply := fmt.Sprintf("Hostname:\n%s\n\nUser-Agent:\n%v\n\nHeader:\n%v\n\nIP:\n%v", host, r.UserAgent(), r.Header, realip.RealIP(r))
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(reply))
    })
    http.ListenAndServe(":80", nil)
}

延伸閱讀 

comments powered by Disqus