0

Proxy Network — Mạng Proxy toàn tập Phần 10

Kiểm chứng — chạy thật, đừng suy đoán

Muốn xác nhận Lệnh
Chuỗi chứng chỉ và ngày hết hạn openssl s_client -connect example.com:443 -servername example.com -showcerts
Đã thoả thuận được HTTP/2 chưa openssl s_client -connect example.com:443 -servername example.com -alpn h2
Phiên bản TLS thực dùng curl -sv https://example.com/ 2>&1 \| grep -i "SSL connection"
Host lạ bị từ chối curl -sk -H 'Host: khong-ton-tai.example' https://IP/ -o /dev/null -w '%{http_code}\n'
Giả mạo XFF bị chặn curl -s -H 'X-Forwarded-For: 1.2.3.4' https://example.com/whoami
Có phải open proxy không curl -sx http://example.com:8080 http://example.org/ -o /dev/null -w '%{http_code}\n'
Upstream chậm sinh 504 đúng lúc curl -s -o /dev/null -w 'http=%{http_code} t=%{time_total}\n' https://example.com/slow
Response có bị cache không curl -sI https://example.com/ \| grep -iE 'age\|cache-control\|vary\|x-cache'
Nén có hoạt động curl -sI -H 'Accept-Encoding: gzip' https://example.com/ \| grep -i content-encoding
Đo tách bạch từng pha curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer}\n' https://example.com/
SSE có bị buffer ở proxy không curl -N https://example.com/events

-w với %{time_appconnect} là công cụ chẩn đoán tốt nhất trong danh sách này.

Nó tách một con số "chậm" mơ hồ thành bốn giai đoạn cụ thể. DNS chậm, bắt tay TCP chậm, bắt tay TLS chậm và upstream chậm là bốn sự cố khác nhau, cần bốn cách sửa khác nhau — nhưng trên dashboard chúng trông y hệt nhau.

15.3 Thực hành 30 phút

Mục tiêu: dựng nginx trước cmd/api, rồi tự tay chứng minh ba điều — client IP đúng, XFF giả bị chặn, upstream chậm trả 504. Toàn bộ chạy trên Docker, không đụng vào máy thật.

Bước 1 — endpoint để quan sát (5 phút)

Thêm hai route tạm vào router chi của cmd/api. Chúng chỉ tồn tại trong bài thực hành này — xoá đi khi xong.

// GET /whoami trả về đúng những gì tầng dưới NHÌN THẤY, không suy diễn.
// Đây là công cụ chẩn đoán quan trọng nhất khi có proxy ở giữa.
r.Get("/whoami", func(w http.ResponseWriter, req *http.Request) {
	out := map[string]any{
		"remote_addr": req.RemoteAddr,
		// Values chứ không phải Get: client gửi được NHIỀU dòng XFF.
		"x_forwarded_for":   req.Header.Values("X-Forwarded-For"),
		"x_forwarded_proto": req.Header.Get("X-Forwarded-Proto"),
		"host":              req.Host,
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(out)
})

// GET /slow cố tình chậm hơn proxy_read_timeout để ép ra 504.
r.Get("/slow", func(w http.ResponseWriter, req *http.Request) {
	select {
	case <-time.After(10 * time.Second):
		_, _ = w.Write([]byte("xong\n"))
	case <-req.Context().Done():
		// nginx đã bỏ đi. Ghi lại để về sau phân biệt được
		// "upstream chậm" với "upstream lỗi" khi đọc log.
		slog.Warn("client huỷ request", "path", req.URL.Path)
	}
})

Bước 2 — cấu hình nginx (10 phút)

deployments/proxy-lab/nginx.conf:

worker_processes 1;
events { worker_connections 1024; }

http {
    # ut = thời gian upstream xử lý, rt = thời gian tổng tính cả client.
    # Chênh lệch giữa hai con số này là phần mạng phía client — tách được
    # ra thì mới trả lời được câu "chậm ở đâu".
    log_format edge '$remote_addr -> $upstream_addr '
                    'status=$status ut=$upstream_response_time rt=$request_time '
                    'rid=$request_id';
    access_log /dev/stdout edge;

    upstream api {
        server api:8000;
        keepalive 32;          # giữ sẵn connection, tránh bắt tay TCP mỗi request
    }

    server {
        listen 8080 default_server;
        server_name _;

        location / {
            proxy_pass http://api;

            # Hai dòng này đi CÙNG NHAU với keepalive ở khối trên.
            # Thiếu proxy_http_version 1.1 thì nginx nói HTTP/1.0 với upstream
            # và toàn bộ khai báo keepalive trở thành vô nghĩa.
            proxy_http_version 1.1;
            proxy_set_header Connection "";

            proxy_set_header Host              $host;
            # $remote_addr chứ KHÔNG phải $proxy_add_x_forwarded_for:
            # đây là hop ngoài cùng, mọi giá trị client gửi lên đều phải bỏ.
            proxy_set_header X-Forwarded-For   $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Request-Id      $request_id;

            proxy_connect_timeout 2s;
            proxy_send_timeout    5s;
            proxy_read_timeout    3s;   # cố tình ngắn để bước 5 quan sát được

            proxy_next_upstream error timeout;   # KHÔNG thêm non_idempotent
        }
    }
}

deployments/proxy-lab/compose.yml:

name: proxy-lab
services:
  api:
    build:
      context: ../..
      dockerfile: deployments/proxy-lab/Dockerfile
    expose: ["8000"]          # expose, KHÔNG ports — chỉ nginx vào được

  edge:
    image: nginx:1.27-alpine
    depends_on: [api]
    ports: ["8080:8080"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

Vì sao expose chứ không phải ports cho api?

ports mở cổng ra máy host, và lúc đó ứng dụng có hai đường vào: qua nginx và trực tiếp. Mọi thứ bạn cấu hình ở nginx — allowlist Host, rate limit, xoá header nội bộ — đều bị đường thứ hai đi vòng qua. Đây chính là bẫy số 12 ở dạng thu nhỏ, và trên production nó lộ ra dưới dạng "sao có traffic không xuất hiện trong log của LB".

Khởi động:

docker compose -f deployments/proxy-lab/compose.yml up -d --build

Bước 3 — xác nhận client IP đúng (5 phút)

curl -s http://localhost:8080/whoami

Kết quả trên một môi trường Docker mặc định:

{
  "host": "localhost",
  "remote_addr": "172.20.0.3:59836",
  "x_forwarded_for": ["172.20.0.1"],
  "x_forwarded_proto": "http"
}

Hai chi tiết cần đọc kỹ:

  • remote_addr là container nginx (172.20.0.3), không phải bạn. Đây là bằng chứng cụ thể cho việc r.RemoteAddr vô dụng khi có proxy ở giữa.
  • x_forwarded_for là 172.20.0.1 — gateway của bridge network, tức máy host. Đó mới là "client".

Địa chỉ cụ thể sẽ khác trên máy bạn vì Docker cấp subnet khác nhau; thứ cần khớp là quan hệ giữa hai giá trị, không phải con số.

Bước 4 — xác nhận giả mạo XFF bị chặn (5 phút)

curl -s -H "X-Forwarded-For: 1.2.3.4" http://localhost:8080/whoami

x_forwarded_for vẫn phải là ["172.20.0.1"]. Giá trị 1.2.3.4 biến mất hoàn toàn — đúng như thiết kế.

Giờ đổi một dòng trong nginx.conf để tận mắt thấy phía sai:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
docker compose -f deployments/proxy-lab/compose.yml restart edge
curl -s -H "X-Forwarded-For: 1.2.3.4" http://localhost:8080/whoami
# {"x_forwarded_for":["1.2.3.4, 172.20.0.1"], ...}

Giá trị giả nằm ở vị trí đầu tiên. Mọi đoạn code lấy strings.Split(xff, ",")[0] — và có rất nhiều thư viện làm đúng như vậy — sẽ tin 1.2.3.4. Đổi lại dòng cũ trước khi sang bước tiếp theo.

Bước 5 — xác nhận 504 khi upstream chậm (5 phút)

curl -s -o /dev/null -w "http=%{http_code} time=%{time_total}\n" http://localhost:8080/slow
# http=504 time=3.016747

proxy_read_timeout 3s cắt request ở giây thứ 3, dù handler cần 10 giây. Log của nginx nói rõ nguyên nhân:

[error] upstream timed out (110: Connection timed out) while reading response header
        from upstream, client: 172.20.0.1, request: "GET /slow HTTP/1.1"
172.20.0.1 -> 172.20.0.2:8000 status=504 ut=3.003 rt=3.003 rid=1f0b0f7c5f5f4a2e9a6a3d4b8c1e7f02

Ba điều rút ra:

  1. 504 là kết luận của proxy, không phải của ứng dụng. Ứng dụng không hề trả 504 — nó vẫn đang chạy bình thường. Đi tìm "chỗ nào trả 504" trong code Go là đi tìm thứ không tồn tại.
  2. ut=3.003 trùng khít proxy_read_timeout. Khi upstream_response_time bám sát một giá trị tròn trịa nào đó, thủ phạm gần như luôn là timeout chứ không phải upstream.
  3. Handler nhận được r.Context().Done() và ghi log huỷ. Không xử lý tín hiệu này thì công vẫn tiếp tục tiêu tài nguyên cho một response không ai còn chờ — và dưới tải cao, đó là cách một sự cố nhỏ biến thành sập dây chuyền.

Dọn dẹp:

docker compose -f deployments/proxy-lab/compose.yml down -v

15.4 Phụ lục A — Thuật ngữ Anh–Việt

Thuật ngữ Nghĩa trong ngữ cảnh proxy
forward proxy Proxy đứng về phía client, client cấu hình để đi qua nó; server đích không biết client thật
reverse proxy Proxy đứng về phía server, client không biết nó tồn tại và tưởng đang nói chuyện thẳng với ứng dụng
upstream Bên mà proxy chuyển request tới — theo từ vựng nginx/HAProxy, chính là backend
downstream Bên mà proxy nhận request từ đó — client hoặc proxy phía trước
origin Máy chủ giữ bản gốc của nội dung, nằm ở cuối chuỗi proxy/CDN
edge Tầng ngoài cùng, nơi client chạm vào hệ thống lần đầu
PoP (Point of Presence) Một cụm máy của CDN đặt tại một vị trí địa lý, phục vụ vùng lân cận
hop Một chặng trung gian trên đường đi của request; mỗi proxy là một hop
hop-by-hop header Header chỉ có nghĩa giữa hai hop kề nhau (Connection, Upgrade, TE…), phải bị gỡ trước khi chuyển tiếp
end-to-end header Header dành cho đích cuối; mọi hop phải giữ nguyên và chuyển tiếp
tunnel Đường ống byte trong suốt do proxy mở, thường qua method CONNECT; proxy không đọc được nội dung
TLS termination Proxy giải mã TLS, xử lý HTTP dạng rõ rồi nói tiếp với upstream; nếu mã hoá lại ở chặng sau thì gọi là re-encryption / bridging
passthrough Proxy chuyển thẳng byte TLS mà không giải mã; định tuyến dựa vào SNI
SNI (Server Name Indication) Tên miền client khai trong bắt tay TLS, cho phép một IP phục vụ nhiều chứng chỉ
ALPN Cơ chế thoả thuận giao thức tầng ứng dụng (h2, http/1.1) ngay trong bắt tay TLS
sticky session / session affinity Ràng buộc một client luôn về cùng một upstream, thường qua cookie hoặc hash IP
drain / graceful shutdown Ngừng nhận request mới nhưng để request đang chạy hoàn tất; tắt theo trình tự có kiểm soát thay vì cắt ngang kết nối đang phục vụ
backpressure Tín hiệu "chậm lại" truyền ngược từ tầng đang quá tải về tầng gửi
hairpin / NAT loopback Traffic đi ra rồi vòng lại chính mạng nội bộ; hay hỏng khi thiết bị NAT không hỗ trợ
split-horizon DNS Cùng một tên miền trả về IP khác nhau tuỳ người hỏi ở trong hay ngoài mạng
health check (active / passive) Chủ động gửi probe định kỳ, hoặc thụ động suy ra sức khoẻ từ lỗi của traffic thật
circuit breaker / outlier detection Ngắt traffic tới upstream đang hỏng một khoảng thời gian thay vì tiếp tục thử; dạng tự động là loại upstream có tỉ lệ lỗi lệch hẳn phần còn lại của pool
round-robin / least connections Hai thuật toán chia tải cơ bản: lần lượt theo vòng, hoặc ưu tiên upstream đang giữ ít kết nối nhất
connection pool Tập kết nối giữ sẵn để tái dùng, tránh trả giá bắt tay cho mỗi request
keepalive Giữ kết nối TCP mở sau khi response xong để dùng lại cho request kế tiếp
multiplexing / head-of-line blocking Nhiều request chạy đồng thời trên một kết nối (HTTP/2, HTTP/3); khi một request chậm chặn các request xếp sau trên cùng kênh thì gọi là head-of-line blocking
buffering Proxy đọc trọn response vào bộ đệm rồi mới gửi cho client; phải tắt cho streaming
chunked transfer encoding Cách gửi body theo từng khối khi chưa biết trước tổng độ dài
cache key Khoá định danh một entry trong cache; sai cache key là nguồn gốc của rò dữ liệu người dùng
cache hit ratio Tỉ lệ request được cache phục vụ mà không phải hỏi origin
stale-while-revalidate Trả bản cũ ngay cho client, đồng thời làm mới ngầm ở phía sau
purge / invalidation Chủ động xoá hoặc đánh dấu hết hạn một entry cache trước thời hạn
origin shield Một tầng cache trung gian giữa các PoP và origin, giảm số request đánh vào origin
anycast Nhiều máy ở nhiều nơi cùng công bố một địa chỉ IP; mạng tự chọn đích gần nhất
rate limiting Giới hạn số request trong một đơn vị thời gian, thường theo thuật toán token bucket
WAF (Web Application Firewall) Lớp lọc request theo luật, chặn các mẫu tấn công ở tầng ứng dụng
mTLS TLS hai chiều — cả client lẫn server đều trình chứng chỉ để xác thực lẫn nhau
sidecar Proxy chạy cạnh mỗi instance ứng dụng, chặn toàn bộ traffic vào/ra của instance đó
data plane / control plane Tầng chuyển gói tin thật / tầng quyết định cấu hình rồi đẩy xuống cho data plane — với Envoy, đẩy qua nhóm API gọi là xDS
service discovery Cơ chế tìm ra địa chỉ hiện tại của một service thay vì ghi cứng IP
canary / blue-green Hai kiểu triển khai từng phần: chia phần trăm traffic, hoặc chuyển hẳn giữa hai môi trường
request id / correlation id Định danh gắn vào một request và giữ nguyên qua mọi tầng, để nối log lại với nhau
trace context Chuẩn truyền thông tin trace giữa các service, mang qua header traceparent
open proxy Proxy chuyển tiếp tới đích tuỳ ý cho bất kỳ ai — gần như luôn là cấu hình sai
transparent proxy Proxy chặn traffic ở tầng mạng mà client không hề cấu hình gì
MITM proxy Proxy giải mã TLS bằng CA riêng để đọc nội dung; dùng để gỡ lỗi hoặc để giám sát
request smuggling Tấn công lợi dụng việc proxy và upstream hiểu ranh giới request khác nhau
slowloris Tấn công giữ nhiều kết nối mở bằng cách gửi header nhỏ giọt, làm cạn tài nguyên server
residential / datacenter / rotating proxy Proxy dùng IP thuê bao dân dụng hoặc IP trung tâm dữ liệu; loại rotating tự đổi IP ra theo mỗi request hay mỗi phiên, lấy từ một pool

15.5 Phụ lục B — RFC và spec liên quan

Số hiệu Tên Vì sao liên quan tới proxy
RFC 9110 HTTP Semantics Định nghĩa method, status code và header hop-by-hop — nền tảng của mọi hành vi chuyển tiếp
RFC 9111 HTTP Caching Ngữ nghĩa Cache-Control, Vary, Age; luật quyết định cái gì shared cache được phép giữ
RFC 9112 HTTP/1.1 Cú pháp trên dây, Content-Length với Transfer-Encoding — nguồn gốc của request smuggling
RFC 9113 HTTP/2 Multiplexing, HPACK, stream — vì sao proxy phải xử lý HTTP/2 khác hẳn HTTP/1.1
RFC 9114 HTTP/3 HTTP trên QUIC/UDP; ảnh hưởng tới cách LB tầng 4 và firewall xử lý traffic
RFC 7239 Forwarded HTTP Extension Chuẩn hoá header Forwarded — bản có cấu trúc thay cho X-Forwarded-* vốn chỉ là quy ước
RFC 1928 SOCKS Protocol Version 5 Proxy ở tầng phiên: client bắt tay với proxy, proxy mở TCP tới đích hộ (CONNECT) hoặc chuyển tiếp UDP (UDP ASSOCIATE) — không phụ thuộc HTTP
RFC 1929 Username/Password Authentication for SOCKS V5 Cơ chế xác thực đơn giản đi kèm SOCKS5
RFC 3986 URI Generic Syntax Chuẩn phân tích URI; proxy và upstream hiểu URI khác nhau là chỗ sinh lỗ hổng
RFC 6455 The WebSocket Protocol Cơ chế Upgrade; proxy phải xử lý riêng vì kết nối sống rất lâu
RFC 8446 TLS 1.3 Phiên bản TLS hiện hành; ảnh hưởng tới bắt tay, resumption và cấu hình termination
RFC 6265 HTTP State Management Mechanism Cookie — thứ khiến response trở nên riêng tư và do đó không được cache chung
RFC 7838 HTTP Alternative Services Alt-Svc; cách server báo còn endpoint khác, liên quan tới việc nâng cấp lên HTTP/3
W3C Trace Context Trace Context Chuẩn header traceparent/tracestate để nối trace xuyên qua mọi hop
PROXY protocol Đặc tả của HAProxy Cách LB tầng 4 truyền IP nguồn thật cho backend; không có xác thực, xem bẫy số 6

15.6 Phụ lục C — Công cụ

Công cụ Vai trò Khi nào chọn Ghi chú
nginx Reverse proxy, cache, static server Mặc định hợp lý cho hầu hết dự án Tài liệu tốt, cộng đồng lớn; cấu hình động cần bản thương mại hoặc module bên ngoài
HAProxy Load balancer tầng 4 và tầng 7 Cần cân bằng tải tinh vi, health check phong phú, thống kê chi tiết Trang stats có sẵn rất mạnh khi chẩn đoán
Envoy Data plane lập trình được Làm sidecar trong mesh, hoặc cần cấu hình động qua API Mạnh nhất và cũng khó nhất; cấu hình dài, thường cần control plane đi kèm
Caddy Reverse proxy có HTTPS tự động Muốn TLS tự cấp và tự gia hạn mà không phải dựng thêm gì Cấu hình ngắn nhất trong nhóm; hợp dự án nhỏ và môi trường dev
Traefik Reverse proxy hướng container Chạy trên Docker/Kubernetes, muốn route sinh ra từ label/annotation Tự phát hiện service; đổi lại là thêm một tầng "ma thuật" khi debug
Squid Forward proxy có cache Proxy chiều ra cho mạng nội bộ, cần lọc và cache Nghiêng về bài toán mạng doanh nghiệp hơn là phục vụ web hiện đại
mitmproxy Proxy gỡ lỗi, xem và sửa traffic Muốn nhìn tận mắt request/response thật, kể cả HTTPS Chỉ dùng khi gỡ lỗi; cài CA riêng nên không bao giờ để chạy thường trực
Cloudflare / Fastly CDN và reverse proxy toàn cầu Người dùng phân tán địa lý, cần chống DDoS và cache ở biên Thêm một hop bạn không kiểm soát — xem §8
pgbouncer Connection pooler cho PostgreSQL Nhiều tiến trình cùng nối vào một Postgres, cạn max_connections "Proxy cho database" — cùng tư duy, khác giao thức
net/http/httputil ReverseProxy trong thư viện chuẩn Go Cần proxy nhúng trong ứng dụng, định tuyến theo logic nghiệp vụ Dùng Rewrite, không dùng Director; xem §10
golang.org/x/net/proxy Dialer đi qua proxy (SOCKS5) Client Go cần đi ra ngoài qua SOCKS5 Cung cấp proxy.Dialer; khác hẳn httputil vốn đứng ở phía server
elazarl/goproxy Thư viện dựng forward proxy có MITM Cần forward proxy tuỳ biến viết bằng Go Chỉ dùng khi thật sự cần can thiệp vào nội dung

15.7 Phụ lục D — Đọc thêm

Chỉ những nguồn chính chủ. Với tài liệu sống, hãy đọc bản trên trang gốc thay vì bài blog chép lại — directive và giá trị mặc định thay đổi theo phiên bản.

Nguồn Đường dẫn gốc Đọc để làm gì
nginx documentation nginx.org/en/docs/ Tra directive theo module; mỗi trang ghi rõ ngữ cảnh (http/server/location) và giá trị mặc định
HAProxy haproxy.org Tài liệu cấu hình đầy đủ theo từng phiên bản; phần health check và retry rất đáng đọc
Envoy documentation envoyproxy.io/docs Kiến trúc listener/filter/cluster và toàn bộ API cấu hình
Caddy documentation caddyserver.com/docs Cú pháp Caddyfile và hành vi cấp chứng chỉ tự động
Traefik documentation doc.traefik.io Provider, router, middleware trong mô hình hướng container
MDN — HTTP developer.mozilla.org/en-US/docs/Web/HTTP Giải thích từng header và status code kèm ví dụ; tra nhanh hơn đọc thẳng RFC
Go — net/http/httputil pkg.go.dev/net/http/httputil ReverseProxy, ProxyRequest, và ghi chú vì sao Director bị coi là không an toàn
Go — net/http pkg.go.dev/net/http Server, Transport, ResponseController — nơi tra giá trị mặc định thay vì đoán
RFC Editor rfc-editor.org Bản gốc của mọi RFC liệt kê ở §15.5
W3C Trace Context w3.org/TR/trace-context/ Định dạng chính xác của traceparent và tracestate

Một thói quen đáng hình thành

Mỗi khi định viết một directive vào file cấu hình production, mở trang tài liệu của chính directive đó và đọc dòng "Default". Phần lớn sự cố proxy không đến từ thứ bạn cấu hình sai, mà từ thứ bạn không cấu hình và mặc định lại không phải cái bạn tưởng — bẫy số 2 và số 10 ở §15.1 đều thuộc dạng đó.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí