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 |
-wvớ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
exposechứ không phảiportschoapi?
portsmở 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_addrlà container nginx (172.20.0.3), không phải bạn. Đây là bằng chứng cụ thể cho việcr.RemoteAddrvô dụng khi có proxy ở giữa.x_forwarded_forlà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:
- 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.
ut=3.003trùng khítproxy_read_timeout. Khiupstream_response_timebá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.- 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