0

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

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

Tài liệu tham chiếu, không phải một bước trong chuỗi dựng dự án. Đọc được độc lập, nhưng mọi ví dụ đều gắn vào hệ thống thật đang xây trong repo này.

Kiến trúc tổng thể: ARCHITECTURE.md Liên quan nhiều nhất: Bước 4 — module identity (middleware, HTTP), Bước 1 — hạ tầng (docker-compose) Cách đọc: §1–§5 là nền tảng, nên đọc tuần tự. §6–§9 là bốn thế giới riêng biệt, đọc theo nhu cầu. §10–§15 là phần thực hành và vận hành.

Vì sao có tài liệu này

Trong toàn bộ chuỗi tài liệu của dự án, cmd/api luôn được vẽ như thứ nói chuyện trực tiếp với người dùng. Điều đó chỉ đúng trên máy bạn.

Ngay khi hệ thống rời khỏi localhost, giữa trình duyệt và cmd/api xuất hiện ít nhất ba trung gian: một CDN, một load balancer, một reverse proxy đang giữ chứng chỉ TLS. Mỗi trung gian đó được phép sửa request của bạn — thêm header, bỏ header, gộp kết nối, đệm dữ liệu, cắt request khi quá lâu, trả lời thay khi upstream chết. Không có cái nào trong số đó xuất hiện trong code Go của bạn.

Hậu quả rất cụ thể và rất dễ gặp:

Triệu chứng bạn sẽ gặp Nguyên nhân thật nằm ở đâu
Rate limit chặn nhầm toàn bộ người dùng cùng lúc RemoteAddr giờ là IP của proxy, không phải của người dùng (§4)
Đăng nhập xong bị đá về http://, cookie Secure biến mất Thiếu X-Forwarded-Proto (§4)
Kết nối luôn đứt đúng ở giây thứ 60 Timeout mặc định của proxy, không phải bug trong Go (§5)
Mỗi lần deploy là một loạt lỗi 502 Không rút node khỏi LB trước khi tắt (§5)
Một người dùng nhìn thấy trang cá nhân của người khác Cache ở tầng CDN thiếu Vary/private (§8)
Tính năng "xem trước link" trở thành cửa sau vào mạng nội bộ SSRF — server của bạn vô tình làm proxy cho kẻ tấn công (§13)

Sáu dòng trên đều là lỗi của tầng proxy, nhưng cả sáu đều lộ ra ở tầng ứng dụng — nơi bạn sẽ ngồi đọc code Go hàng giờ mà không tìm ra gì sai. Đó là lý do tài liệu này tồn tại.

Phạm vi

Cụm từ "proxy network" trong tiếng Anh mang hai nghĩa hoàn toàn khác nhau, và trộn lẫn chúng là nguồn gốc của rất nhiều hiểu nhầm:

Nghĩa Là gì Ở mục nào
Mạng lưới proxy trong kiến trúc hệ thống Tập hợp các trung gian mà traffic của bạn đi qua: reverse proxy, load balancer, CDN, API gateway, sidecar Phần lớn tài liệu
Hồ proxy thương mại Dịch vụ cho thuê hàng nghìn IP làm lối ra: datacenter, residential, mobile §6, §7

Tài liệu này bao trùm cả hai, vì chúng là hai mặt của cùng một cơ chế: một bên bạn vận hành proxy, bên kia bạn đi qua proxy — và ở bên thứ ba, bạn phải phòng thủ trước proxy của người khác.

Ba câu hỏi mang theo suốt tài liệu. Gặp bất kỳ proxy nào — của bạn hay của người khác — hãy hỏi đúng ba câu: (1) Nó đại diện cho ai, client hay server? (2) Nó ở tầng nào, L4 hay L7? (3) Nó tin cái gì bạn gửi tới?

Ba câu này giải thích được gần như mọi hành vi kỳ lạ mà bạn sẽ gặp. Câu thứ ba giải thích gần như mọi lỗ hổng bảo mật.

Mục lục

  1. Proxy là gì — mô hình tinh thần đúng
  2. Phân loại proxy đầy đủ
  3. Giao thức và cơ chế ở tầng dây
  4. Danh tính client qua proxy — header, tin cậy và cách lấy IP đúng
  5. Reverse proxy chuyên sâu — cân bằng tải, sức khỏe và độ bền
  6. Proxy Network đúng nghĩa — hồ proxy, xoay vòng và vận hành
  7. Ẩn danh, riêng tư và dấu vân tay — phía nào nhìn thấy gì
  8. CDN — mạng proxy ngược quy mô toàn cầu
  9. API Gateway và Service Mesh — proxy đi vào trong hệ thống
  10. Viết proxy bằng Go — từ 20 dòng tới bản dùng được thật
  11. Cấu hình thực chiến — Nginx, HAProxy, Envoy, Caddy, Traefik
  12. Áp dụng vào dự án community — kiến trúc đề xuất
  13. Bảo mật — proxy là bề mặt tấn công, không chỉ là lá chắn
  14. Vận hành — hiệu năng, quan sát và gỡ lỗi
  15. Bẫy thường gặp, checklist và phụ lục

1. Proxy là gì — mô hình tinh thần đúng

Phần lớn tài liệu về proxy bắt đầu bằng một danh sách: forward proxy, reverse proxy, transparent proxy, SOCKS proxy… Đọc xong bạn nhớ được tên gọi nhưng không quyết định được gì, vì danh sách không nói cho bạn biết cái gì phân biệt chúng.

Phần này không liệt kê. Nó dựng một mô hình tinh thần đủ chắc để mười bốn phần còn lại chỉ là hệ quả. Nếu bạn chỉ đọc một phần của tài liệu này, đọc phần này.

1.1 Định nghĩa ở mức bản chất: proxy là bên ĐẠI DIỆN

Từ proxy trong tiếng Anh không phải thuật ngữ máy tính. Nó là từ pháp lý: giấy uỷ quyền. Cổ đông không đi họp được thì ký giấy cho người khác đi thay — người đó bỏ phiếu nhân danh cổ đông, và bên tổ chức đại hội xử lý lá phiếu ấy như phiếu của cổ đông.

Trong mạng máy tính, nghĩa giữ nguyên:

Proxy là một bên trung gian đứng ra thực hiện giao dịch nhân danh một trong hai đầu, chứ không phải một cái ống dẫn trung lập.

Hai chữ quan trọng nhất là nhân danh. Bỏ chúng đi, proxy trở thành router — và mọi thứ trong tài liệu này mất nghĩa.

Từ định nghĩa đó suy ra ngay một tính chất kỹ thuật, và đây mới là thứ bạn cần nhớ:

Proxy có HAI kết nối, không phải một. Một kết nối với bên nó phục vụ, một kết nối với bên còn lại. Nó đọc xong từ kết nối này, quyết định, rồi tự mình phát sinh yêu cầu trên kết nối kia. Hai kết nối đó có thể khác nhau về địa chỉ nguồn, về phiên bản giao thức, về khoá TLS, về timeout, về cách đóng khung dữ liệu trên dây.

   KHÔNG phải proxy — một kết nối duy nhất đi xuyên qua:

       A ═══════════════════════════════════════► B
                    (router / switch / NAT)
              thiết bị ở giữa chỉ sửa nhãn gói tin

   LÀ proxy — hai kết nối tách biệt, ghép lại bằng phần mềm:

       A ═══════════► P ═══════════► B
         kết nối #1     kết nối #2
         A nói với P    P nói với B, bằng danh nghĩa của chính P

Cái ranh giới ═══► P ═══► đó là nơi mọi thứ thú vị và mọi thứ đau đầu trong tài liệu này xảy ra: nơi header được thêm vào hoặc mất đi, nơi TLS bị kết thúc rồi dựng lại, nơi IP người dùng biến thành IP của một máy chủ trong datacenter, nơi timeout của bạn không còn là timeout mà client thực sự cảm nhận.

1.2 Phép thử một câu

Toàn bộ hệ thống phân loại proxy — cái mà §2 sẽ khai triển — treo trên đúng một câu hỏi:

Cái proxy này đại diện cho CLIENT hay đại diện cho SERVER?

  • Đại diện cho clientforward proxy.
  • Đại diện cho serverreverse proxy.

Không có vai trò thứ ba. Mọi cái tên khác bạn từng nghe (transparent, explicit, SOCKS, MITM, egress gateway, ingress, sidecar, CDN, WAF, API gateway) đều là một trong hai vai trò này cộng thêm một chi tiết về cách triển khai.

Chú ý chữ vai trò. Có đúng một thứ trung gian trả lời được "không bên nào" cho câu hỏi trên: tunnel — và chính vì thế nó không phải proxy. Nó chỉ nối hai kết nối rồi chuyển byte mù, không nhân danh ai. RFC 9110 cũng chia đúng theo lằn ranh đó, xem §1.4.

Câu hỏi trên nghe trừu tượng, nên đây là hai cách trả lời nó bằng sự thật kiểm chứng được:

Cách 1 — ai cấu hình nó, và nếu nó biến mất thì bên nào phải sửa? Bảng dưới mô tả trường hợp phổ biến nhất: forward proxy explicit, tức client được cấu hình để trỏ vào nó. Biến thể transparent (client bị chặn ngầm, không hề biết) làm lệch dòng thứ hai và thứ tư — §2 tách riêng ca đó.

Forward proxy Reverse proxy
Ai cài đặt, ai trả tiền chạy Phía client (người dùng, phòng IT công ty) Phía server (chính bạn, chủ của cmd/api)
Client có phải cấu hình gì không — trỏ trình duyệt/biến môi trường vào proxy Không — client chỉ biết một tên miền
Nếu proxy chết Client mất mạng, phải sửa cấu hình phía client Website chết, phía server phải sửa
Ai biết proxy tồn tại Client biết. Server thường không Server biết. Client thường không

Cách 2 — nhìn thẳng vào dây. HTTP/1.1 có nhiều dạng request-target khác nhau, và dạng được dùng nói thẳng ra vai trò của bên nhận (chi tiết cú pháp ở §3, RFC 9112 là chỗ định nghĩa các dạng này):

GET http://community.example.com/api/v1/posts HTTP/1.1
Host: community.example.com

Dạng absolute-form — có đủ scheme://host ngay trên dòng đầu. Client gửi cái này khi nó biết mình đang nói chuyện với một forward proxy và cần nói cho proxy biết đích đến thật.

GET /api/v1/posts HTTP/1.1
Host: community.example.com

Dạng origin-form — chỉ có path. Client gửi cái này khi nó tin rằng bên kia chính là server. Reverse proxy nhận đúng dạng này, vì với client thì nó server.

CONNECT community.example.com:443 HTTP/1.1
Host: community.example.com:443

Dạng authority-form với method CONNECT — client yêu cầu proxy mở một đường ống byte thô tới host:port rồi tránh ra. Đây là cách HTTPS đi qua forward proxy mà proxy không đọc được nội dung; xem §3.

Vì sao phép thử này quan trọng hơn cái tên gọi

Cùng một binary Nginx, cùng một binary Envoy, có thể là forward proxy ở máy này và reverse proxy ở máy kia. Phần mềm không quyết định vai trò — vị trí trong hệ thống và bên nào được nó phục vụ mới quyết định.

Cho nên khi ai đó nói "cứ đặt một cái proxy ở đó", câu hỏi đầu tiên của bạn phải là "nó đại diện cho ai?". Không trả lời được câu đó thì mọi quyết định sau — đặt chứng chỉ ở đâu, tin header nào, ghi log IP nào — đều là đoán mò.

1.3 Hai chiều, hai sơ đồ

Forward proxy — đại diện cho client. Proxy nằm ở phía đi ra, trong mạng của client.

        ┌──────────── mạng của CLIENT ─────────────┐
        │                                          │
        │   Trình duyệt ────────► Forward Proxy ───┼────► Origin server
        │   (BIẾT có proxy,       (do phía client  │      (chỉ thấy IP của
        │    được cấu hình         cài & vận hành)  │       proxy — không biết
        │    để trỏ vào nó)                        │       client là ai)
        └──────────────────────────────────────────┘

   Ai giấu ai:  proxy giấu CLIENT khỏi server.
   Câu hỏi vận hành:  "ai được phép đi ra ngoài, và đi tới đâu?"

Reverse proxy — đại diện cho server. Proxy nằm ở phía đi vào, trong mạng của server.

                                 ┌────────── mạng của SERVER ──────────┐
                                 │                                     │
   Trình duyệt ──────────────────┼──► Reverse Proxy ────► cmd/api      │
   (KHÔNG biết có proxy —        │    (do phía server     cmd/api #2   │
    nó tin đây LÀ server thật;   │     cài & vận hành)    cmd/api #3   │
    DNS trỏ thẳng vào proxy)     │                                     │
                                 └─────────────────────────────────────┘

   Ai giấu ai:  proxy giấu SERVER (có mấy con, ở đâu, chạy gì) khỏi client.
   Câu hỏi vận hành:  "ai được phép đi vào, và request này về máy nào?"

Đặt cạnh nhau, hai sơ đồ trên là cùng một cơ chế soi gương. Proxy vẫn là hai kết nối ghép lại; chỉ có bên được nó phục vụ là đổi.

Và trong đời thật, một request đi qua cả hai. Đây là hình mà bạn nên giữ trong đầu:

   ── phía client ──────────┐            ┌────────── phía server ──────
                            │            │
   Trình duyệt ──► Forward ─┼── Internet ┼──► Reverse ──► cmd/api
                    proxy   │            │    proxy
                            │            │
   đại diện cho client ─────┘            └───── đại diện cho server

   Ở giữa Internet, hai cái proxy này KHÔNG biết nhau tồn tại.
   Cả hai đều tin rằng mình đang nói chuyện trực tiếp với đầu kia.

Đó là lý do vì sao debug một sự cố xuyên chuỗi lại khó: không hop nào có bức tranh toàn cảnh, kể cả cmd/api của bạn.

1.4 Proxy khác gì gateway, tunnel, NAT, load balancer, firewall, relay

Sáu từ này bị dùng lẫn lộn hằng ngày, kể cả trong tài liệu chính hãng của các nhà cung cấp. Bảng dưới phân định bằng đúng hai tiêu chí đã dựng ở §1.1 và §1.2: có hai kết nối tách biệt không, và đại diện cho ai.

Khái niệm Đại diện cho ai Tầng thường gặp Kết thúc & tạo lại kết nối? Dấu hiệu nhận biết chắc chắn
Proxy (nghĩa hẹp, forward) Client L4 hoặc L7 Lưu lượng đi ra bị hướng vào nó: hoặc client tự cấu hình (explicit), hoặc mạng ép ngầm (transparent)
Gateway (= reverse proxy) Server Thường L7 Với client, nó đóng vai origin server
Tunnel Không bên nào L4 (byte thô) Có hai kết nối TCP, nhưng không diễn giải nội dung Nó nối hai kết nối rồi copy byte; không parse, không sửa, không nhân danh ai
NAT Không bên nào L3/L4 Không — chỉ sửa nhãn IP/port Vẫn là một kết nối TCP xuyên suốt, chỉ đổi địa chỉ
Load balancer Server (thường) L4 hoặc L7 Tuỳ loại Đây là vai trò, không phải tầng: nó trả lời "chọn backend nào"
Firewall Không bên nào L3/L4 (cổ điển) Không Nó chỉ quyết định cho qua hay chặn, không nhân danh ai
Relay Tuỳ ngữ cảnh Tuỳ Từ chung cho "nhận rồi chuyển tiếp", không hàm ý đại diện

Ba chỗ dễ sai nhất, nói kỹ:

Gateway không phải là một thứ khác proxy. RFC 9110 — tài liệu định nghĩa ngữ nghĩa HTTP — phân loại các bên trung gian thành đúng ba dạng: proxy, gatewaytunnel, trong đó gateway chính là tên chuẩn của cái ta quen gọi là reverse proxy. Nói "gateway hay proxy?" là câu hỏi sai; câu đúng vẫn là "nó đại diện cho ai?".

Ba dạng của RFC không mâu thuẫn với hai vai trò ở §1.2: proxy đại diện cho client, gateway đại diện cho server, còn tunnel không đại diện cho ai — nên nó nằm ngoài phép thử, chứ không phải là vai trò thứ ba.

NAT không phải proxy, và đây là nhầm lẫn tốn nhiều thời gian nhất. NAT sửa trường địa chỉ trong header IP/TCP rồi đẩy gói đi tiếp — kết nối TCP vẫn là một kết nối từ đầu tới cuối, không có bên thứ ba nào bắt tay riêng với ai. Hệ quả thực tế: NAT không thể thêm header X-Forwarded-For, không thể retry một request lỗi, không thể cache, và không cần private key TLS. Nếu một thiết bị làm được bất kỳ việc nào trong bốn việc đó, nó là proxy chứ không phải NAT.

Load balancer là vai trò chồng lên proxy, không phải loại proxy. Một L7 load balancer một reverse proxy có thêm thuật toán chọn backend. Một L4 load balancer có thể là proxy TCP thật (hai kết nối) hoặc chỉ là bộ DNAT (một kết nối). Hỏi "cái LB này có phải proxy không" thì phải xem nó thuộc kiểu nào — và câu trả lời quyết định việc backend nhìn thấy IP nào. Chi tiết ở §5.

Cảnh báo về chữ "relay" trong chính dự án này

Dự án community có binary cmd/relay. Nó không phải network relay theo nghĩa của phần này. Nó là outbox relay: một tiến trình đọc bảng outbox trong PostgreSQL rồi publish sang Kafka (xem ARCHITECTURE.md §7.4).

Cùng một chữ, hai thế giới khác nhau. Khi đọc log hay tên service, đừng để chữ "relay" đánh lừa — hỏi lại phép thử một câu: nó đại diện cho ai? cmd/relay không đại diện cho ai cả, nó là một consumer/producer nội bộ.

1.5 Proxy ở tầng nào — L4 hay L7

Sau câu hỏi "đại diện cho ai", câu hỏi thứ hai là "nó nhìn thấy đến đâu?". Câu trả lời chỉ có hai mức thực dụng.

L4 — proxy chỉ biết ip:port. Nó nhận một luồng byte TCP (hoặc datagram UDP) và đẩy sang một luồng khác. Nó không biết bên trong là HTTP, là Postgres wire protocol hay là Kafka protocol. Với nó, tất cả đều là byte.

L7 — proxy hiểu HTTP. Nó parse dòng request, đọc được method, path, header, cookie, và (nếu buffer) cả body. Nó nói được HTTP.

Ranh giới này quyết định tất cả những gì proxy có thể và không thể làm cho bạn:

Tiêu chí L4 (TCP/UDP) L7 (HTTP)
Nhìn thấy 5-tuple: IP nguồn/đích, port nguồn/đích, protocol Toàn bộ request: method, path, header, cookie, body
Route dựa trên IP đích, port, và SNI nếu là TLS Path, Host, header, cookie, method, thậm chí nội dung body
Route theo URL (/api/* → A, /static/* → B) Không thể — nó không thấy path ✅ Được
Cần private key TLS của bạn không ❌ Không — TLS đi xuyên qua nguyên vẹn ⚠️ (nếu muốn đọc nội dung)
Chi phí CPU Thấp — chủ yếu là copy byte Cao hơn: giải mã TLS + parse HTTP mỗi request
Retry một request lỗi ❌ Không — nó không biết đâu là "một request" ✅ Được (chỉ nên retry method idempotent)
Cache ❌ Không ✅ Được — xem §8
Thêm/sửa header (X-Forwarded-For, request ID) ❌ Không ✅ Được — xem §4
Dùng cho PostgreSQL, Kafka, Redis ✅ Được — protocol nào cũng chạy ❌ Không (trừ proxy chuyên biệt cho từng protocol)
Cân bằng tải nhiều request trên một kết nối HTTP/2 ❌ Không — cả kết nối dính chặt vào một backend ✅ Được — cân bằng theo từng request

Bốn hệ quả cần khắc sâu:

1. L4 không đọc được path, nên không route theo URL được. Đây không phải hạn chế của sản phẩm nào cả — thông tin đó nằm trong payload TCP mà L4 cố tình không mở ra. Muốn /api/v1/posts đi về cmd/api còn /static/* đi về CDN, bạn bắt buộc phải có một hop L7.

2. L4 vẫn route được theo tên miền, nhờ SNI. Trong bắt tay TLS, client gửi ClientHello chứa SNI (Server Name Indication) ở dạng , chưa mã hoá — vì server cần biết chọn chứng chỉ nào trước khi có kênh mã hoá. Một L4 proxy có thể đọc mỗi trường đó rồi quyết định backend, mà không cần private key. Đây là mẹo rất hay dùng cho multi-tenant. (Cơ chế Encrypted Client Hello nhằm giấu luôn SNI đang được triển khai dần; khi nó phổ biến, mẹo này mất tác dụng.)

3. L7 phải giải mã TLS, nên phải giữ private key — và đó là một quyết định bảo mật, không phải quyết định kỹ thuật. Chỗ nào kết thúc TLS là chỗ đó có bản rõ của mọi mật khẩu, mọi token, mọi bài viết nháp của người dùng. Đặt L7 proxy ở đâu tức là bạn quyết định ai được nhìn thấy dữ liệu người dùng ở dạng không mã hoá. Với proxy do bạn vận hành thì đó là đánh đổi bình thường; với proxy của người khác thì đó là câu hỏi phải trả lời tỉnh táo. §7§13 đào sâu chuyện này.

4. L4 và HTTP/2 là một cái bẫy ít người biết. HTTP/2 ghép nhiều request vào một kết nối TCP. L4 proxy cân bằng tải ở mức kết nối, nên toàn bộ request của một client sẽ dồn vào đúng một backend cho tới khi kết nối đóng. Bạn thêm 5 pod cmd/api mà tải vẫn lệch hẳn về một pod — không phải thuật toán sai, mà là bạn đang cân bằng nhầm đơn vị.

Vậy chọn L4 hay L7?

Không phải chọn một. Hệ thống thật thường có cả hai, xếp chồng: một lớp L4 ở ngoài cùng để chịu lưu lượng lớn và bảo vệ chống flood, rồi một lớp L7 ở trong để định tuyến theo path và gắn header. Việc cần làm không phải chọn, mà là biết rõ ở mỗi hop mình đang ở tầng nào — vì mọi khả năng trong bảng trên đều theo tầng, không theo sản phẩm.

1.6 Vì sao hệ thống thật gần như luôn có proxy, dù bạn không chủ đích đặt

Câu chuyện phổ biến: "dự án của em nhỏ, chưa cần proxy". Thực tế thì request đầu tiên bạn gửi tới cmd/api đã đi qua ít nhất một lớp trung gian rồi.

Bạn làm gì Lớp trung gian bị chèn vào Tầng Hậu quả bạn sẽ gặp
Một dòng - "8080:8080" dưới ports: trong Docker Compose Chuyển tiếp cổng của Docker (DNAT ở netfilter, và tuỳ cấu hình còn thêm tiến trình userland của Docker) L3/L4 IP nguồn mà app thấy có thể là IP gateway của bridge network, không phải IP máy bạn
Deploy lên Kubernetes Service — kube-proxy lập trình DNAT (iptables/nftables/IPVS) từ ClusterIP về pod IP L3/L4 Kết nối tới Service không hề "đi qua" một tiến trình proxy nào ở chế độ mặc định
Đặt CDN trước website CDN edge — reverse proxy có cache, đặt ở hàng chục vị trí địa lý L7 TLS bị kết thúc ở edge; response cũ có thể sống lâu hơn bạn tưởng
Ngồi làm việc trong mạng công ty Proxy MITM của phòng IT, cài CA riêng vào máy nhân viên L7 go get, docker pull lỗi chứng chỉ; HTTPS của bạn bị đọc hợp pháp
Bật service mesh Sidecar Envoy chen vào giữa mọi kết nối vào/ra của pod L4+L7 Thêm một hop có timeout riêng, có retry riêng, có metric riêng
Không làm gì cả, chỉ viết Go http.DefaultTransport tôn trọng HTTP_PROXY/HTTPS_PROXY/NO_PROXY L7 Binary của bạn tự đi qua proxy trên máy có biến môi trường đó

Cái cuối cùng làm nhiều người ngạc nhiên nhất, nên xem tận mắt:

package main

import (
	"fmt"
	"net/http"
	"net/url"
	"time"
)

// clientTheoMoiTruong: proxy do MÔI TRƯỜNG quyết định.
// http.DefaultTransport đã đặt sẵn Proxy: http.ProxyFromEnvironment, nên MỌI binary Go
// (kể cả cmd/api khi nó gọi ra dịch vụ ngoài) đã tôn trọng HTTP_PROXY/HTTPS_PROXY/NO_PROXY
// mà bạn không viết một dòng nào. Đây là lý do code chạy ở nhà thì được, lên máy công ty thì lỗi.
func clientTheoMoiTruong() *http.Client {
	return &http.Client{
		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
		Timeout:   10 * time.Second,
	}
}

// clientQuaProxyCoDinh: proxy do ỨNG DỤNG quyết định, không phụ thuộc môi trường.
// Dùng khi việc đi qua proxy là một phần của thiết kế (ví dụ bắt buộc mọi egress
// phải qua một gateway để kiểm soát), vì lúc đó nó phải đúng kể cả khi có người
// quên set biến môi trường trên một máy nào đó.
func clientQuaProxyCoDinh(proxyURL string) (*http.Client, error) {
	u, err := url.Parse(proxyURL)
	if err != nil {
		return nil, fmt.Errorf("proxy URL không hợp lệ: %w", err)
	}
	return &http.Client{
		Transport: &http.Transport{
			Proxy: func(*http.Request) (*url.URL, error) { return u, nil },
		},
		Timeout: 10 * time.Second,
	}, nil
}

func main() {
	resp, err := clientTheoMoiTruong().Get("http://example.com/")
	if err != nil {
		fmt.Println("lỗi:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("status:", resp.Status)
}

Thử đổi hành vi của cùng một binary mà không đụng vào code:

# Các biến này áp cho mọi tiến trình con sinh ra từ session PowerShell hiện tại.
# Đây là quyết định của MÔI TRƯỜNG, không phải của ứng dụng — và đó chính là điều
# khiến nó khó truy vết: không có dòng code nào để mà đọc.

# Phải đặt CẢ HAI: ProxyFromEnvironment chọn biến theo scheme của request.
# Request http:// đọc HTTP_PROXY, request https:// đọc HTTPS_PROXY — đặt mỗi
# HTTPS_PROXY rồi gọi http://example.com/ thì proxy KHÔNG được dùng, và bạn sẽ
# ngồi nghi ngờ code trong khi lỗi nằm ở tên biến.
$env:HTTP_PROXY  = "http://127.0.0.1:8080"
$env:HTTPS_PROXY = "http://127.0.0.1:8080"   # giá trị vẫn là http:// — đây là địa
                                             # chỉ của proxy, không phải của đích
$env:NO_PROXY    = "localhost,127.0.0.1,postgres,kafka"

go run ./cmd/smoketest

Lưu ý một trường hợp đặc biệt được ghi rõ trong tài liệu của thư viện chuẩn: ProxyFromEnvironment trả về nil — tức không dùng proxy — khi host của request là localhost (có hay không có port). Bản cài đặt bên dưới cũng bỏ qua proxy với các địa chỉ loopback. Nên nếu chương trình Go của bạn gọi vào http://localhost:8080 mà thấy proxy không hề được dùng dù HTTP_PROXY đã set, đó là thiết kế chứ không phải lỗi — và cũng là lý do bug loại này không tái hiện được trên máy dev, nơi mọi thứ đều là localhost.

Bẫy này đã có sẵn trong dự án community

Mở deployments/docker-compose.yml và nhìn KAFKA_ADVERTISED_LISTENERS. Vì sao Kafka lại cần một biến riêng để quảng bá địa chỉ của chính nó?

Vì có một lớp chuyển tiếp cổng ("9092:29092") giữa client trên máy host và broker trong container. Client kết nối vào localhost:9092, broker trả lời "hãy nói chuyện với tôi ở địa chỉ X" — và nếu X là địa chỉ nội bộ container thì client trên host không tới được. Broker không biết phía trước mình có một lớp dịch địa chỉ.

Đó chính xác là dạng lỗi mà cả tài liệu này xoay quanh: một bên phía sau lớp trung gian tự nói về địa chỉ của mình, trong khi nó không có cách nào biết địa chỉ mà client thực sự dùng để tới được nó. X-Forwarded-Host, X-Forwarded-Proto, redirect sai scheme, cookie sai domain — cùng một gốc bệnh, chỉ khác giao thức. Xem §4.

1.7 Bản đồ toàn cảnh: một request tới cmd/api đi qua bao nhiêu proxy

Sơ đồ dưới là một triển khai production giả định — không phải kiến trúc đang chạy của dự án community, mà là tập hợp những hop bạn sẽ lần lượt gặp khi hệ thống lớn dần. Nêu ra để thấy con đường từ nút "Đăng bài" trong trình duyệt tới dòng đầu tiên trong transport/http/handler.go dài tới mức nào:

   Người dùng bấm "Đăng bài"
        │
        │   với HTTPS, ① chỉ mở CONNECT tunnel — phiên TLS của
        │   trình duyệt đi xuyên qua nó và kết thúc ở ②
        ▼
   ┌────────────────────────────────┐
   │ ① Proxy phía trình duyệt/OS    │  extension hoặc file PAC do IT đẩy xuống
   └───────────────┬────────────────┘  → forward proxy, L7 (HTTPS: chỉ tunnel)
                   │   TLS #1 — chứng chỉ do CA nội bộ công ty ký
                   ▼
   ┌────────────────────────────────┐
   │ ② Proxy công ty (MITM)         │  kết thúc TLS #1 → đọc được toàn bộ nội dung
   └───────────────┬────────────────┘  → forward proxy, L7
                   │   TLS #2
                   ▼
   ══════════════ Internet ══════════════
                   │
                   ▼
   ┌────────────────────────────────┐
   │ ③ CDN edge (anycast)           │  cache, nén, HTTP/3 ra ngoài — HTTP/1.1 vào trong
   └───────────────┬────────────────┘  → reverse proxy, L7
                   │
                   ▼
   ┌────────────────────────────────┐
   │ ④ WAF                          │  có thể cùng tiến trình với ③, có thể là hop riêng
   └───────────────┬────────────────┘  → reverse proxy, L7
                   │   TLS #3 (nếu bạn mã hoá cả chặng trong)
                   ▼
   ┌────────────────────────────────┐
   │ ⑤ Load balancer của cloud      │  L4 hoặc L7 tuỳ bạn chọn khi tạo
   └───────────────┬────────────────┘
                   │
                   ▼
   ┌────────────────────────────────┐
   │ ⑥ Ingress controller (k8s)     │  Nginx / Envoy / Traefik — route theo path & host
   └───────────────┬────────────────┘  → reverse proxy, L7
                   │
                   ▼
   ┌────────────────────────────────┐
   │ ⑦ Sidecar (service mesh)       │  Envoy chạy chung pod, chặn mọi traffic vào/ra
   └───────────────┬────────────────┘  → reverse proxy cho chiều vào, L4+L7
                   │
                   ▼
   ┌────────────────────────────────┐
   │ ⑧ cmd/api                      │  ← dòng code đầu tiên của BẠN chạy ở đây
   └────────────────────────────────┘

Bảy hop trung gian trước khi tới hàm handler đầu tiên. Con số bảy phụ thuộc hoàn toàn vào giả định ở trên — hệ thống của bạn có thể là ba, có thể là mười. Điều không đổi là: trong sơ đồ này không hop nào thừa, mỗi cái được ai đó thêm vào vì một lý do chính đáng, và mỗi hop đều để lại dấu vết lên công việc hằng ngày của bạn:

Thứ bị ảnh hưởng Vì sao con số hop quan trọng
Timeout Timeout hiệu lực là giá trị nhỏ nhất trên chuỗi, không phải giá trị bạn đặt trong Go. Nếu hop ⑥ cắt ở 30s mà cmd/api đặt 60s, client nhận 504 trong khi handler vẫn đang chạy
Retry Retry nhân lên theo cấp số nhân, không phải cấp số cộng. Giả sử ba hop cùng bật retry, mỗi hop tối đa 3 lượt thử (không phải 1 lần đầu + 3 lần thử lại): 3×3×3 = 27 request chạm tới origin cho một cú bấm chuột. Con số này là số học từ giả định đó, không phải đo được — nhưng bậc độ lớn thì đúng, và đây là cách một sự cố nhỏ biến thành sập toàn hệ thống
Giới hạn kích thước Header tối đa, body tối đa, URL tối đa — hiệu lực là hop chặt nhất. Upload ảnh 20 MB hỏng vì một hop nào đó chặn ở 8 MB, và thông báo lỗi thường không nói hop nào
IP người dùng Mỗi hop có thể ghi đè hoặc nối thêm vào X-Forwarded-For. Không có kỷ luật rõ ràng thì IP bạn ghi vào log là IP của một máy chủ nào đó, không phải của người dùng → §4
TLS Trong Go, r.TLS != nil chỉ nói về hop liền trước. Nó không cho biết người dùng có thực sự dùng HTTPS hay không
Phiên bản HTTP Trình duyệt ↔ ③ có thể là HTTP/3, còn ⑦ ↔ ⑧ là HTTP/1.1. r.Proto trong handler chỉ nói về chặng cuối
Cache Bất kỳ hop L7 nào cũng có thể cache. "Lỗi chỉ xảy ra với một số người dùng" thường là một edge node đang giữ bản cũ → §8
Buffering Một hop buffer toàn bộ body sẽ giết chết streaming và Server-Sent Events, dù cả hai đầu đều hỗ trợ

Câu hỏi đầu tiên khi debug không phải "code sai ở đâu"

Với bảy hop ở giữa, câu hỏi đúng là: "lỗi này sinh ra ở hop nào?"

Kỹ thuật rẻ tiền và hiệu quả nhất: sinh một request ID duy nhất ở hop ngoài cùng, bắt mọi hop chuyển tiếp nó nguyên vẹn, và ghi log nó ở mọi hop. Hop đầu tiên không có ID đó trong log chính là nơi request chết. Không có sợi dây này, bạn sẽ đọc log của cmd/api để tìm một request chưa bao giờ tới được cmd/api. Cách làm cụ thể ở §14.

Và một hệ quả rất cụ thể cho kiến trúc event-driven của dự án này:

package main

import (
	"fmt"
	"log/slog"
	"net/http"
	"os"
)

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	http.HandleFunc("/whoami", func(w http.ResponseWriter, r *http.Request) {
		// Mọi trường dưới đây chỉ mô tả HOP LIỀN TRƯỚC, không mô tả người dùng.
		// Từ trong handler KHÔNG có cách nào biết phía trước còn bao nhiêu hop nữa —
		// đó là lý do bản đồ ở trên phải nằm trong đầu bạn, chứ không nằm trong code.
		logger.Info("request",
			"remote_addr", r.RemoteAddr, // IP của hop liền trước, gần như không bao giờ là IP người dùng
			"host", r.Host, // có thể đã bị một hop nào đó viết lại
			"proto", r.Proto, // HTTP/1.1 ở đây không nói gì về HTTP/3 ở vòng ngoài
			"tls", r.TLS != nil, // false KHÔNG có nghĩa là người dùng đi HTTP trần
			"xff", r.Header.Get("X-Forwarded-For"), // đọc trần như thế này là SAI, xem §4
		)
		fmt.Fprintln(w, "xem log")
	})

	// Ví dụ ngắn nên bỏ qua lỗi ở đây; bản dùng thật xem §10.
	_ = http.ListenAndServe(":8080", nil)
}

Ghép với luồng ghi bài ở ARCHITECTURE.md §9.1: POST /api/v1/posts mở transaction, INSERT INTO posts, INSERT INTO outbox, COMMIT, rồi trả 201. Nếu hop ⑤ hoặc ⑥ đã cắt kết nối vì timeout trước khi 201 kịp về, người dùng thấy lỗi và bấm lại — trong khi transaction đã commit và event đã nằm trong outbox. Kết quả: hai bài viết, hai event post.post.created.v1.

Proxy không gây ra lỗi này; nó chỉ làm cho lỗi này xảy ra thường xuyên hơn nhiều. Cách chống nằm ở tầng ứng dụng — idempotency key phía ghi, và bảng consumed_events phía consumer (06-consumer-va-idempotency.md). Bài học của phần này là: mỗi hop bạn thêm vào là một cơ hội nữa để client và server bất đồng về chuyện "request đã thành công hay chưa".

1.8 Ba câu hỏi mang theo khi đọc phần còn lại

Mười bốn phần sau sẽ giới thiệu rất nhiều tên gọi, sản phẩm và cấu hình. Đừng cố nhớ hết. Với mỗi thứ mới gặp, hỏi đúng ba câu — trả lời được ba câu này thì bạn suy ra được phần lớn hành vi của nó mà không cần đọc tài liệu của nhà cung cấp.

# Câu hỏi Nó quyết định điều gì Trả lời sai thì hỏng gì
1 Nó đại diện cho ai — client hay server? Ai cấu hình nó, ai chịu trách nhiệm khi nó chết, nó giấu bên nào khỏi bên nào, header nào được phép tin Tin X-Forwarded-For do client tự đặt → bất kỳ ai cũng giả mạo được IP, rate limit và audit log thành vô nghĩa
2 Nó ở tầng mấy — L4 hay L7? Route được theo cái gì, có cần private key không, có cache/retry/thêm header được không, tốn CPU bao nhiêu Kỳ vọng L4 route theo path → mất một ngày sửa cấu hình cho một việc bất khả thi về nguyên lý
3 Nó có giữ trạng thái không? Có scale ngang thoải mái được không, restart có mất gì không, hai instance có trả lời khác nhau không Sticky session giữ trong bộ nhớ một instance → thêm instance thứ hai là người dùng bị đăng xuất ngẫu nhiên

Về câu thứ ba, "trạng thái" ở đây gồm những thứ rất cụ thể, và mỗi thứ đều có cái giá vận hành riêng:

Loại trạng thái Ví dụ Cái giá phải trả
Session affinity Cookie hoặc hash IP để ghim client vào một backend Backend chết là mất phiên; tải phân bổ lệch
Cache nội dung Response được lưu ở edge Hai người dùng thấy hai phiên bản khác nhau của cùng một URL
Connection pool tới upstream Giữ sẵn kết nối tới cmd/api Restart backend không đủ — kết nối cũ có thể vẫn được tái dùng
Bộ đếm rate limit "100 request/phút cho mỗi IP" Mỗi instance đếm riêng → n instance thì hạn mức thực tế thành n × 100
Kết quả health check "backend #2 đang hỏng" Instance mới khởi động chưa biết gì, có thể gửi request vào máy đã chết
Khoá phiên TLS Session ticket để bắt tay nhanh hơn ở lần sau Không chia sẻ khoá giữa các instance thì client phải bắt tay lại từ đầu

Một proxy stateless thì bạn nhân bản và giết tuỳ ý — đó là lý do người ta thích nó. Một proxy stateful thì bạn phải trả lời câu hỏi "trạng thái đó sống ở đâu" trước khi thêm instance thứ hai. Câu này quay lại trong §5, §6§9 dưới ba hình dạng khác nhau, nhưng vẫn là một câu hỏi.

Ba câu hỏi trên là toàn bộ mô hình tinh thần cần có. Phần §2 bắt đầu bằng cách lấy câu hỏi số 1 và chẻ nó ra tới tận cùng.


2. Phân loại proxy đầy đủ

Hầu hết bài viết trên mạng phân loại proxy thành một danh sách phẳng: "forward proxy, reverse proxy, transparent proxy, SOCKS proxy, anonymous proxy…". Danh sách đó sai về mặt logic, vì các mục trong nó không loại trừ nhau. Một con nginx đứng trước cmd/api vừa là reverse proxy, vừa là L7 proxy, vừa là edge proxy, vừa là TLS terminating proxy — cả bốn cùng lúc, không mâu thuẫn gì.

Cách nghĩ đúng: proxy được mô tả bởi bốn trục độc lập. Mỗi proxy thật ngoài đời là một điểm trong không gian bốn chiều đó. Khi ai đó nói "tôi cần một proxy", câu hỏi đầu tiên của bạn phải là bốn câu hỏi, không phải một.

2.1 Bốn trục — cách đọc toàn bộ mục này

    Trục 1: HƯỚNG ĐẠI DIỆN     forward ──────────────► reverse
            (đại diện cho ai?)  (cho client)          (cho server)

    Trục 2: ĐỘ LỘ DIỆN         explicit ─────────────► transparent
            (client có biết?)   (được cấu hình)       (bị chuyển hướng)

    Trục 3: TẦNG               L4 TCP ──── SOCKS ────► L7 HTTP
            (hiểu tới đâu?)     (byte)     (địa chỉ)   (ngữ nghĩa)

    Trục 4: VỊ TRÍ             edge ─ ingress ─ sidecar ─ egress ─ backconnect
            (đứng ở đâu?)
Trục Câu hỏi quyết định Trả lời bởi
Hướng đại diện Proxy này được đặt ở đó vì lợi ích của client hay của server? Ai trả tiền vận hành nó
Độ lộ diện Nếu tắt proxy đi, client có phải sửa cấu hình không? Cách gói tin tới được proxy
Tầng Proxy có cần đọc nội dung request để làm việc của nó không? Tính năng bạn muốn
Vị trí Proxy nằm ở ranh giới nào trong sơ đồ hạ tầng? Traffic đi vào hay đi ra

Phép thử một câu cho trục 1: nếu tôi gõ sai địa chỉ đích, ai là người chịu hậu quả? Với forward proxy, client tự chọn đích và tự chịu — proxy chỉ chuyển tiếp. Với reverse proxy, danh sách đích do người vận hành proxy khai báo sẵn, client không được chọn.

Trong toàn bộ mục này, "client" nghĩa là bên khởi tạo kết nối, "upstream"/"origin" nghĩa là bên nhận cuối cùng. Cơ chế ở tầng dây (CONNECT, Host, SNI, Via) được nói kỹ ở §3; ở đây chỉ dùng chúng để phân biệt các loại.

2.2 Trục 1 — Hướng đại diện: forward và reverse

    FORWARD                             REVERSE

    Client ─┐                                      ┌─► api-1
    Client ─┼─► Proxy ─► Internet      Client ─► Proxy ─► api-2
    Client ─┘   (do phía client         (một domain)  └─► static
                 sở hữu)

    Client biết đích, proxy không       Client không biết có mấy backend,
    biết trước có những đích nào        proxy biết chính xác danh sách
Tiêu chí Forward proxy Reverse proxy
Đại diện cho Client Server / origin
Ai sở hữu và vận hành Phía người dùng (doanh nghiệp, ISP, cá nhân) Phía chủ dịch vụ
Danh sách đích Mở — bất kỳ host nào client yêu cầu Đóng — khai báo cứng trong cấu hình
Cách chọn upstream Từ request của client (CONNECT host:443, URI tuyệt đối) Từ luật routing của proxy (Host, path, SNI, header)
Ai phân giải DNS Thường là proxy (client chỉ gửi tên) Proxy, theo tên đã cấu hình sẵn
Client cần biết gì Địa chỉ proxy — phải cấu hình hoặc bị ép Chỉ cần một domain duy nhất
Bài toán giải được Kiểm soát, lọc, cache, ẩn IP client, gộp IP đi ra Cân bằng tải, TLS termination, che giấu topology, WAF
Hỏng cấu hình thì sao Client mất Internet Cả dịch vụ mất khả năng phục vụ
Công cụ điển hình Squid, tinyproxy, Envoy (egress), SOCKS5 server nginx, HAProxy, Caddy, Traefik, Envoy, CDN

Vì sao hai chữ này hay bị hiểu ngược?

Vì người ta cố suy ra từ hướng luồng dữ liệu — mà hướng luồng thì giống hệt nhau: client → proxy → server ở cả hai trường hợp. Cái khác nhau không nằm ở hướng gói tin, mà ở chỗ proxy đứng về phía aiai được quyền quyết định đích đến.

Bài kiểm tra nhanh — hỏi ai giữ danh sách đích: mở cấu hình của proxy và tìm tên các host mà nó chuyển tiếp tới. Không có danh sách nào cả, đích đến từ chính request của client → forward proxy. Danh sách nằm cứng trong cấu hình và client không có cách nào thêm một đích mới → reverse proxy.

Đừng dùng phép thử "xoá cấu hình proxy trên máy client" cho trục này — đó là phép thử của trục 2, không phải trục 1. Client vốn không có dòng cấu hình nào trỏ tới một reverse proxy để mà xoá: nó chỉ phân giải một tên miền, và tên miền đó tình cờ trỏ vào proxy.

Điểm dễ bỏ sót: cùng một phần mềm làm được cả hai vai. nginx, Envoy, HAProxy đều chỉ là bộ máy chuyển tiếp có luật; forward hay reverse chỉ là hình dạng của tệp cấu hình.

# Vai reverse proxy: nginx đại diện cho backend, client chỉ biết một domain.
# (Cấu hình rút gọn — chứng chỉ, bộ header đầy đủ và timeout xem §11.)
server {
    listen 443 ssl;
    server_name community.example.com;

    location / {
        # Thiếu dòng này thì nginx gửi "Host: 127.0.0.1:8000" xuống cmd/api,
        # và mọi URL tuyệt đối dựng từ Host (redirect, link trong email) trỏ
        # về localhost của server. Đây là lỗi kinh điển nhất của reverse proxy.
        proxy_set_header Host $host;

        proxy_pass http://127.0.0.1:8000;   # cmd/api
    }
}

Nhưng đối xứng không hoàn toàn: nginx bản core không làm được forward proxy cho HTTPS, vì nó không xử lý method CONNECT (cần module bên thứ ba). Đây là lý do thực tế khiến người ta chọn Squid hoặc Envoy cho hướng egress thay vì dùng lại nginx đã có sẵn.

Một forward proxy mở cho cả thế giới dùng thì gọi là open proxy — loại nguy hiểm nhất trong họ này, chi tiết ở 2.6.

2.3 Trục 2 — Độ lộ diện với client: explicit và transparent

Trục này quyết định gói tin tới được proxy bằng cách nào, và nó là trục có hậu quả bảo mật lớn nhất.

Tiêu chí Explicit proxy Transparent / intercepting proxy
Client có biết proxy tồn tại Có — được cấu hình chỉ đích danh Không — tưởng đang nói chuyện thẳng với server
Cấu hình ở đâu Trình duyệt, biến HTTP_PROXY/HTTPS_PROXY, PAC/WPAD, policy máy Không có gì trên client; nằm ở router/firewall
Gói tin tới proxy bằng cách nào Client chủ động mở kết nối tới proxy Bị bẻ hướng: policy routing, WCCP, iptables REDIRECT/TPROXY
Proxy biết đích từ đâu Client nói thẳng: CONNECT host:443 hoặc URI tuyệt đối Phải tự suy: địa chỉ đích gốc của socket, header Host, hoặc SNI
HTTPS có cần CA riêng không Không, nếu chỉ tạo tunnel , nếu muốn đọc nội dung
Proxy chết thì sao Client báo lỗi rõ ràng "không kết nối được proxy" Traffic biến mất một cách khó hiểu, hoặc rơi vào timeout
Độ khó gỡ lỗi Thấp — nhìn cấu hình là thấy Cao — không có dấu vết nào trên client
Ứng dụng bỏ qua được không Có: nhiều thư viện lờ biến môi trường proxy Không: đã ở tầng mạng thì không trốn được

Explicit: client hợp tác

Client mở TCP tới proxy rồi nói ra ý định của mình. Với HTTP thường, nó gửi URI tuyệt đối. Với HTTPS, nó gửi CONNECT example.com:443 (method CONNECT, định nghĩa trong RFC 9110), proxy mở tiếp một TCP tới đích và từ đó chỉ bơm byte hai chiều. Proxy nhìn thấy tên host, cổng, thời điểm, khối lượng dữ liệu — nhưng không thấy nội dung, không cần chứng chỉ gì cả.

Trong Go, phía client nói chuyện với explicit proxy như sau:

package main

import (
	"fmt"
	"net"
	"net/http"
	"net/url"
	"time"

	"golang.org/x/net/proxy" // go get golang.org/x/net
)

// httpProxyClient tạo client đi qua một explicit HTTP proxy.
//
// Với URL https://, Transport TỰ ĐỘNG dùng method CONNECT để mở tunnel —
// ta không phải viết gì thêm. Đây là lý do explicit proxy không cần CA:
// TLS vẫn được bắt tay end-to-end giữa client và origin, proxy chỉ bơm byte.
func httpProxyClient(rawProxyURL string) (*http.Client, error) {
	u, err := url.Parse(rawProxyURL) // ví dụ "http://squid.internal:3128"
	if err != nil {
		return nil, fmt.Errorf("proxy URL không hợp lệ: %w", err)
	}

	tr := &http.Transport{
		// http.ProxyURL ép MỌI request qua proxy này.
		// Nếu muốn tôn trọng HTTP_PROXY/HTTPS_PROXY/NO_PROXY của môi trường
		// thì dùng http.ProxyFromEnvironment thay cho dòng dưới.
		Proxy: http.ProxyURL(u),

		// Timeout này áp cho chặng client → proxy. Nếu proxy treo, ta muốn
		// biết ngay, chứ không phải chờ hết Timeout tổng của Client.
		DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
	}

	return &http.Client{Transport: tr, Timeout: 30 * time.Second}, nil
}

// socks5Client đi qua SOCKS5 — thấp hơn một tầng, xem 2.4.
func socks5Client(addr, user, pass string) (*http.Client, error) {
	var auth *proxy.Auth
	if user != "" {
		// RFC 1929: xác thực username/password của SOCKS5 là PLAINTEXT.
		// Chỉ dùng trong mạng nội bộ tin cậy, hoặc bọc trong một lớp mã hoá khác.
		auth = &proxy.Auth{User: user, Password: pass}
	}

	d, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
	if err != nil {
		return nil, fmt.Errorf("dựng SOCKS5 dialer thất bại: %w", err)
	}

	// Phải ép về ContextDialer, nếu không ta mất khả năng huỷ theo context —
	// request bị client bỏ vẫn tiếp tục giữ kết nối tới proxy cho tới timeout.
	cd, ok := d.(proxy.ContextDialer)
	if !ok {
		return nil, fmt.Errorf("dialer SOCKS5 không hỗ trợ context")
	}

	// Không set Proxy: ở đây proxy nằm ở tầng dial, Transport không hề biết.
	return &http.Client{
		Transport: &http.Transport{DialContext: cd.DialContext},
		Timeout:   30 * time.Second,
	}, nil
}

Bẫy vận hành số một của explicit proxy: biến môi trường HTTP_PROXY chỉ có tác dụng nếu thư viện chịu đọc nó. net/http của Go có đọc (qua http.ProxyFromEnvironment), nhưng chỉ khi bạn dùng http.DefaultTransport hoặc tự khai báo. Một &http.Transport{} viết tay với Proxy bỏ trống sẽ đi thẳng ra Internet, âm thầm vượt qua mọi chính sách egress mà đội hạ tầng tưởng là đã bịt kín.

Bốn chi tiết nữa về http.ProxyFromEnvironment, tất cả đọc thẳng ra được từ net/httpgolang.org/x/net/http/httpproxy:

  • Nó chỉ đọc ba biến: http_proxy, https_proxy, no_proxy (bản chữ thường được ưu tiên trước bản chữ hoa). ALL_PROXY không nằm trong danh sách. Quen từ curl mang sang là mất proxy mà không có lấy một dòng log. Bản đọc ALL_PROXYproxy.FromEnvironment() của x/net/proxy, thuộc thế giới SOCKS — xem §10.
  • HTTPS_PROXY nghĩa là "proxy dùng cho request tới https://", không phải "proxy mà ta nói chuyện bằng HTTPS". Biến được chọn theo scheme của đích, không theo scheme của proxy.
  • Đích là localhost hoặc bất kỳ địa chỉ loopback nào (127.0.0.0/8, ::1) luôn được miễn proxy, kể cả khi NO_PROXY trống — đây là ngoại lệ cứng trong code, không phải cấu hình. Nhưng IP nội mạng như 10.0.0.0/8 thì không được miễn: quên khai chúng vào NO_PROXY là mọi lời gọi service nội bộ cũng vòng qua egress proxy, chậm đi một chặng và làm ngập log của proxy.
  • Nếu tiến trình có biến REQUEST_METHOD (dấu hiệu đang chạy như CGI), Go từ chối dùng HTTP_PROXY và trả lỗi. Đây là bản vá cho lỗ hổng httpoxy: request từ ngoài mang header Proxy: thì CGI biến nó thành biến môi trường HTTP_PROXY, và kẻ tấn công chọn được nơi server của bạn gửi traffic tới.

Transparent: client bị bẻ hướng

    Không có proxy:
        Client ──TCP SYN tới 93.184.x.x:443──► Router ──► Internet

    Có transparent proxy:
        Client ──TCP SYN tới 93.184.x.x:443──► Router
                                                 │ policy routing / WCCP
                                                 ▼
                                              Proxy  (đích gốc lấy từ socket)
                                                 │
                                                 └──► Internet

Client không hề gửi CONNECT, vì nó tưởng mình đang nói chuyện thẳng với origin. Proxy phải tự khôi phục đích đến từ metadata của socket bị chuyển hướng (trên Linux là tuỳ chọn socket lưu địa chỉ đích gốc), hoặc đoán từ header Host với HTTP thường, hoặc từ trường SNI trong ClientHello với TLS.

Và đây là chỗ HTTPS vỡ.

TLS không chỉ mã hoá; nó xác thực danh tính server. Client mở tới example.com sẽ yêu cầu bên kia xuất trình chứng chỉ hợp lệ cho tên example.com, ký bởi một CA nằm trong trust store của nó. Một transparent proxy muốn đọc nội dung thì phải đóng vai example.com — tức phải xuất trình chứng chỉ cho tên đó. Nó không có, và không thể xin được từ CA công cộng. Nó chỉ có thể tự ký một chứng chỉ giả bằng CA của chính nó.

Kết quả là một trong ba tình huống:

Tình huống Client thấy gì Proxy làm được gì
Không cài CA của proxy Lỗi chứng chỉ đỏ lòm, kết nối đứt Không đọc được gì, chỉ phá
Cài CA của proxy vào trust store Không thấy gì cả, mọi thứ "bình thường" Đọc và sửa được toàn bộ nội dung
Proxy chỉ passthrough theo SNI Không thấy gì, TLS vẫn end-to-end Chỉ thấy tên host, không thấy nội dung

Hàng giữa là điều đáng suy nghĩ nhất. Cài CA của doanh nghiệp vào máy nghĩa là bạn đã trao cho bộ phận IT khả năng đọc mọi phiên HTTPS trên máy đó — không chỉ traffic công việc, mà cả ngân hàng, email cá nhân, y tế. Không có cơ chế kỹ thuật nào giới hạn CA đó chỉ áp cho một vài domain, trừ khi proxy tự nguyện bỏ qua (thường có danh sách bypass cho nhóm domain nhạy cảm — nhưng đó là chính sách, không phải ràng buộc mật mã).

Ba hệ quả kỹ thuật kéo theo:

  1. Certificate pinning làm sập ứng dụng. App mobile ghim chứng chỉ cụ thể sẽ từ chối chứng chỉ giả của proxy — kể cả khi CA đã được cài. Triệu chứng: browser trên máy đó chạy tốt, app thì lỗi mạng không rõ nguyên nhân.
  2. mTLS không đi qua được MITM. Nếu origin yêu cầu client certificate, proxy không có khoá riêng của client để trình ra. Buộc phải passthrough.
  3. Toàn bộ traffic bây giờ tập trung ở một chỗ giải mã. Proxy MITM trở thành mục tiêu giá trị nhất trong hệ thống: nắm nó là nắm mọi thứ. Chi tiết ở §13.

Transparent không bắt buộc phải MITM. Một transparent proxy hoàn toàn có thể chỉ đọc trường SNI trong ClientHello (trường này nằm ngoài phần mã hoá) để quyết định cho đi hay chặn, rồi bơm byte nguyên vẹn. Nó mất khả năng lọc theo URL và theo nội dung, nhưng giữ được TLS end-to-end. Với hầu hết nhu cầu "chặn domain xấu", đây mới là lựa chọn đúng, không phải MITM. Lưu ý xu hướng ngược lại: các cơ chế mã hoá phần đầu bắt tay TLS đang được triển khai, và khi chúng phổ biến thì cách lọc theo SNI sẽ yếu dần — kiểm chứng tình trạng hỗ trợ ở thời điểm bạn đọc.

2.4 Trục 3 — Tầng hoạt động: L4, SOCKS, L7

    ┌──────────────────────────────────────────────┐
    │  L7 HTTP proxy: thấy method, URL, header,    │  nginx, Envoy, HAProxy
    │  body, cookie → định tuyến & sửa được        │  (mode http), Traefik
    ├──────────────────────────────────────────────┤
    │  SOCKS: thấy địa chỉ đích (IP hoặc tên       │  Dante, ssh -D,
    │  miền) + cổng. Không thấy nội dung           │  nhiều SOCKS5 server
    ├──────────────────────────────────────────────┤
    │  L4 TCP proxy: chỉ thấy IP:port hai đầu      │  HAProxy (mode tcp),
    │  và luồng byte đục                           │  Envoy TCP proxy, LVS
    └──────────────────────────────────────────────┘
Tiêu chí L4 TCP proxy SOCKS5 L7 HTTP proxy
Nhìn thấy IP:port nguồn/đích, byte thô Địa chỉ đích do client khai (IPv4/IPv6/domain), cổng Toàn bộ ngữ nghĩa HTTP
Định tuyến dựa trên Cổng lắng nghe, đôi khi SNI Không định tuyến — client đã chỉ đích Host, path, header, cookie, method
Sửa được request không Không Không Có (rewrite path, thêm/bớt header)
Retry một request hỏng Không — mất kết nối là mất Không Có, nếu request idempotent
Cache được không Không Không Có (RFC 9111)
Giao thức chạy được Mọi thứ trên TCP Mọi thứ trên TCP, và UDP qua UDP ASSOCIATE Chỉ HTTP và những gì HTTP bọc được
Chi phí xử lý Thấp nhất Thấp Cao nhất — phải parse
Ảnh hưởng tới TLS Không đụng Không đụng Phải terminate nếu muốn đọc
Chuẩn Không có RFC. Có PROXY protocol (đặc tả của HAProxy) để trả lại IP thật cho upstream — §3.5 RFC 1928, auth RFC 1929 RFC 9110/9112, HTTP/2 RFC 9113, HTTP/3 RFC 9114

L4: nhanh vì ngu

L4 proxy không biết bên trong luồng byte là gì. Đó vừa là điểm mạnh (proxy được Postgres, Redis, SMTP, MQTT, bất cứ thứ gì trên TCP; không cần chứng chỉ; overhead thấp nhất) vừa là điểm yếu (không cân bằng tải theo request được — một kết nối keep-alive gửi 10.000 request sẽ dính chặt vào một backend duy nhất).

# L4: đủ để chuyển tiếp Postgres — không cần hiểu một byte nào của giao thức.
# Cấu hình đầy đủ với health check và timeout xem §11.
frontend pg_in
    bind :5432
    mode tcp
    default_backend pg_pool

backend pg_pool
    mode tcp
    server pg1 10.0.0.11:5432 check

Một điểm hay quên: HTTP/3 chạy trên QUIC, tức là UDP (RFC 9114). Một L4 proxy chỉ lắng nghe TCP sẽ không thấy traffic HTTP/3 nào, và client sẽ âm thầm tụt về HTTP/2 — hoặc treo, nếu bạn quảng cáo HTTP/3 mà không thực sự chuyển tiếp được UDP.

L7: đắt vì hiểu

L7 proxy đọc và dựng lại request. Đổi lại chi phí, bạn có: định tuyến theo path (/api/* sang cmd/api, /static/* sang CDN), retry an toàn, cân bằng tải theo từng request kể cả trên một kết nối keep-alive, chèn header danh tính client (§4), cache, nén, rate limit.

Cái giá thứ hai ít ai nhắc: L7 proxy phải hiểu mọi thứ đi qua nó. WebSocket (RFC 6455) bắt đầu bằng một HTTP request mang Upgrade: websocket, upstream trả 101 Switching Protocols, và từ đó kết nối không còn là HTTP nữa. Rắc rối nằm ở chỗ UpgradeConnection là header hop-by-hop — một proxy L7 đúng chuẩn phải xoá chúng trước khi chuyển tiếp, nên WebSocket qua proxy luôn cần cấu hình riêng.

Hai kiểu hỏng khác hẳn nhau, và chúng bị chẩn đoán nhầm cho nhau nhiều hơn bất kỳ cặp nào khác:

Triệu chứng Nguyên nhân thật Chỗ sửa
Không bao giờ thấy 101; kết nối hỏng ngay giây đầu, client báo lỗi handshake Proxy nuốt Upgrade, hoặc nói HTTP/1.0 với upstream — WebSocket không tồn tại trên HTTP/1.0 (nginx: thiếu proxy_http_version 1.1 cùng cặp Upgrade/Connection) Cho phép upgrade — §3.9, §11
Bắt tay xong, chạy tốt, rồi đứt đúng ở giây thứ 60 mỗi khi kênh im lặng Timeout đọc mặc định của một tầng proxy nào đó trên đường đi (mặc định proxy_read_timeout của nginx là 60s) — không phải lỗi của thư viện WebSocket Nới timeout ở mọi tầng, hoặc gửi ping định kỳ (§5)

Câu "WebSocket chạy ở local, lên staging thì hỏng" là mô tả của cả hai, nên đừng dừng ở đó: hỏi tiếp "nó có bao giờ lên tới 101 không?" — một câu đó tách đôi được hai nguyên nhân.

SOCKS5: ở giữa, và bị hiểu sai nhiều nhất

SOCKS5 có bắt tay riêng: client chào, thoả thuận phương thức xác thực, gửi lệnh CONNECT kèm địa chỉ đích, rồi từ đó là luồng byte trong suốt. Nó hiểu địa chỉ (kể cả tên miền, nên có thể để proxy phân giải DNS thay client — điều này quan trọng với chống rò rỉ DNS, xem §7) nhưng không hiểu nội dung.

SOCKS5 không "ẩn danh hơn" HTTP proxy.

Nó không thêm một chút mã hoá nào. Cảm giác "kín đáo hơn" đến từ chỗ khác: HTTP proxy ở L7 có thể chèn Via, X-Forwarded-For và tự nó để lộ rằng có proxy trong đường đi; SOCKS5 không có khái niệm header nên không chèn gì. Nhưng nếu ứng dụng của bạn gửi HTTP qua SOCKS5, thì header vẫn do chính ứng dụng của bạn sinh ra và server vẫn đọc được nguyên vẹn.

Đúng đắn hơn: SOCKS5 được chọn khi bạn cần proxy giao thức không phải HTTP, hoặc cần một điểm thoát duy nhất cho mọi loại kết nối TCP của một tiến trình.

Điểm thực dụng: http.Transport.Proxy hiểu thẳng scheme socks5socks5h bên cạnh http/https, nên nếu bạn không cần chạm vào dialer thì cả hàm socks5Client ở trên rút lại còn mấy dòng:

// Godoc của http.Transport.Proxy nói rõ: "socks5" được xử lý y hệt "socks5h".
// Nghĩa là Go LUÔN giao nguyên tên miền cho proxy phân giải, không bao giờ
// resolve trước ở phía client — không rò rỉ DNS, kể cả khi bạn gõ "socks5".
// Userinfo trong URL được Go dịch sang xác thực username/password của RFC 1929.
tr := &http.Transport{
	Proxy: http.ProxyURL(&url.URL{
		Scheme: "socks5",
		Host:   "127.0.0.1:1080",
		User:   url.UserPassword(user, pass), // bỏ đi nếu proxy không đòi xác thực
	}),
}

Cái mất khi viết ngắn: bạn không còn chỗ đặt timeout dial riêng cho chặng tới proxy, cũng không cài được Control trên net.Dialer để chặn IP nội bộ khi phòng SSRF. Cần những thứ đó thì quay lại bản proxy.SOCKS5 ở trên.

2.5 Trục 4 — Vị trí triển khai

Trục này không nói gì về cơ chế; nó nói về ranh giới nào trong hạ tầng mà proxy đứng canh. Cùng một Envoy có thể đóng cả năm vai dưới đây, chỉ khác tệp cấu hình.

Vị trí Đứng ở đâu Đại diện cho Hướng traffic Hỏng thì ai chết
Edge proxy Ngoài cùng, đối diện Internet Server Vào Toàn bộ dịch vụ
Ingress Cửa vào của một cụm (Kubernetes, VPC) Server Vào Mọi service trong cụm
Sidecar Cùng pod/host với đúng một tiến trình Cả hai chiều của tiến trình đó Vào và ra Chỉ tiến trình đó
Egress proxy Cửa ra của mạng nội bộ Client (chính hệ thống của bạn) Ra Mọi lời gọi ra bên ngoài
Backconnect gateway Một endpoint duy nhất che một hồ IP Client Ra Toàn bộ hồ proxy
    Internet
        │
        ▼
    ┌─────────────┐
    │ Edge proxy  │  TLS termination, WAF, rate limit
    └─────────────┘
        │
        ▼
    ┌─────────────┐
    │  Ingress    │  routing: /api → api, /assets → static
    └─────────────┘
        │
        ├──────► cmd/api   (3 instance)
        └──────► cmd/worker
                     │  gọi API bên thứ ba
                     ▼
              ┌─────────────┐
              │Egress proxy │  allowlist domain, IP tĩnh, log
              └─────────────┘
                     │
                     ▼
                 Internet

Ranh giới giữa edgeingress hay bị mờ, và trong một hệ thống nhỏ chúng đúng là một. Chúng tách ra khi trách nhiệm tách ra: edge lo những thứ liên quan tới Internet công cộng (chứng chỉ, chống DDoS, chống bot), ingress lo những thứ liên quan tới nội bộ (biết cmd/api đang có mấy instance, ở đâu). Với dự án community§12, gộp làm một là hợp lý.

Backconnect gateway là khái niệm của thế giới proxy thương mại: bạn chỉ được cấp đúng một host:port, mỗi kết nối tới đó được gateway chuyển ra một IP khác nhau trong hồ hàng nghìn tới hàng triệu địa chỉ. Client không quản lý danh sách IP, không xử lý IP chết. Cơ chế xoay vòng, sticky session và vận hành hồ nằm ở §6.

2.6 Các loại có tên riêng

Các tên dưới đây không phải là trục thứ năm — mỗi cái chỉ là một điểm cụ thể trong không gian bốn trục, đã được đặt tên vì hay gặp.

Tên Hướng Lộ diện Tầng Vị trí
Open proxy Forward Explicit L7 hoặc SOCKS Bất kỳ
Anonymous / elite proxy Forward Explicit L7 Egress
Caching proxy Forward (hoặc reverse) Cả hai L7 Edge / egress
MITM / intercepting proxy Forward Cả hai — explicit khi gỡ lỗi, transparent khi cưỡng chế L7 Egress / thiết bị test
TLS terminating proxy Reverse Explicit L7 (terminate rồi mới định tuyến) Edge
WAF Reverse Explicit L7 Edge
API gateway Reverse Explicit L7 Ingress
Service mesh sidecar Cả hai Transparent (trong pod) L7 Sidecar
Egress proxy doanh nghiệp Forward Cả hai L7 / SOCKS Egress
Residential / mobile proxy Forward Explicit L7 / SOCKS Backconnect
Tor Forward Explicit (SOCKS) SOCKS Overlay riêng

Open proxy — và vì sao nó nguy hiểm cho chính người dùng nó

Open proxy là forward proxy không xác thực, ai kết nối tới cũng dùng được. Nguy hiểm theo hai chiều, và chiều thứ hai mới là chiều người ta hay quên.

Nếu bạn vận hành một open proxy (thường là do cấu hình sai, quên http_access deny all), bạn đang cho người lạ mượn danh tính mạng của mình: spam, quét cổng, tấn công đều mang IP của bạn. IP vào blacklist, nhà cung cấp gửi abuse report, và nếu proxy đặt trong VPC thì nó là bàn đạp để đi tới các dịch vụ nội bộ vốn tin tưởng địa chỉ nội mạng.

Nếu bạn dùng một open proxy tìm được trên mạng:

  • Chủ proxy thấy hết những gì không mã hoá — và có thể sửa nó: chèn quảng cáo, chèn script, đổi nội dung trả về. Với HTTP thường thì việc này là tầm thường.
  • Với HTTPS họ vẫn thấy tên host, thời điểm, khối lượng. Đủ để dựng hồ sơ hành vi.
  • Nếu họ dụ được bạn cài CA của họ, họ thấy tất cả. Không open proxy tử tế nào yêu cầu điều đó — nhưng "hướng dẫn cài chứng chỉ để chạy được" là một mồi câu phổ biến.
  • IP đó có tiền sử. Bạn dùng chung với người đã spam từ hôm qua, nên bạn ăn CAPTCHA và HTTP 403 ở khắp nơi mà không hiểu vì sao.
  • Một tỷ lệ đáng kể open proxy là honeypot — dựng lên để thu thập credential. Bất kỳ thứ gì bạn đăng nhập qua kết nối không mã hoá đều nên coi như đã mất.

Quy tắc: proxy miễn phí, không rõ chủ, không có hợp đồng — coi như đối thủ, không phải hạ tầng.

Anonymous và elite — và hai nghĩa của chữ "transparent"

Trong giới scraping, forward proxy được xếp hạng theo lượng thông tin chúng tự khai ra qua header HTTP. Ba tên gọi dưới đây là từ vựng thương mại của các nhà bán proxy, không phải một chuẩn nào cả — không có RFC nào định nghĩa "elite proxy", và hai nhà cung cấp khác nhau hoàn toàn có thể gán nhãn khác nhau cho cùng một hành vi:

Cấp Tên gọi Header đặc trưng Server đích biết gì
1 Transparent Via, có X-Forwarded-For chứa IP thật Biết có proxy, và biết IP thật của bạn
2 Anonymous Via hoặc header proxy, không có IP thật Biết có proxy, không biết bạn là ai
3 Elite / high-anonymity Không header nào tiết lộ Không có dấu hiệu nào từ header

Cảnh báo về từ vựng: chữ "transparent" ở bảng này không cùng nghĩa với "transparent proxy" ở 2.3. Ở 2.3, transparent = client không biết có proxy. Ở đây, transparent = server đích biết thừa có proxy. Hai nghĩa gần như ngược nhau. Khi đọc tài liệu tiếng Anh, luôn hỏi "trong suốt với ai?".

"Elite" chỉ nói về header HTTP — tức là lớp mỏng nhất.

Header sạch không giúp gì nếu TLS fingerprint của thư viện HTTP bạn dùng khác hẳn trình duyệt thật, nếu thứ tự header không giống ai, nếu IP thuộc dải datacenter đã bị gắn nhãn, hoặc nếu nhịp request đều tăm tắp như đồng hồ. Phía nào nhìn thấy gì, và cái gì thực sự tố cáo bạn — §7.

Caching proxy (Squid)

Cache dùng chung cho nhiều client, đặt ở hướng forward. Quy tắc lưu và dùng lại phản hồi thuộc RFC 9111: Cache-Control, thời gian tươi, revalidate, và Vary để tách các biến thể theo header.

# Chỉ mạng worker được đi ra, và chỉ tới các domain đã duyệt.
# Thứ tự dòng http_access CÓ NGHĨA: luật khớp đầu tiên thắng,
# nên "deny all" bắt buộc phải nằm cuối.
http_port 3128

acl worker_net  src      10.20.0.0/16
acl allowed_api dstdomain .github.com .stripe.com

http_access allow worker_net allowed_api
http_access deny all

Sự thật khó chịu về caching proxy hướng forward năm nay: gần như toàn bộ web đã là HTTPS. Proxy chỉ tạo tunnel CONNECT thì không thấy nội dung, nên không cache được gì. Muốn cache lại phải MITM — đánh đổi mà rất hiếm khi bõ. Vì vậy caching ngày nay chủ yếu sống ở hướng ngược: CDN và reverse proxy trước origin, nơi bạn sở hữu chứng chỉ nên terminate TLS là chuyện đương nhiên (§8).

Điểm còn giá trị của Squid trong doanh nghiệp không còn là cache, mà là kiểm soát và ghi log — xem mục egress proxy bên dưới.

MITM / intercepting proxy (mitmproxy, DLP)

Cùng một cơ chế, hai mục đích hoàn toàn khác nhau:

  • Gỡ lỗi. mitmproxy sinh một CA riêng, bạn cài CA đó vào thiết bị test của chính bạn, rồi xem được từng request/response mà app mobile hay SDK bên thứ ba thực sự gửi đi. Đây là công cụ không thể thay thế khi tài liệu API sai hoặc thiếu.
  • DLP doanh nghiệp. Cùng cơ chế nhưng cài CA lên máy nhân viên qua policy, để quét dữ liệu đi ra: chặn upload mã nguồn, chặn gửi dữ liệu khách hàng ra ngoài.

Ranh giới giữa "công cụ" và "giám sát" ở đây chỉ là ai sở hữu thiết bị và ai được thông báo. Về kỹ thuật, MITM proxy không phân biệt được hai trường hợp — và người dùng cuối cũng không, đó chính là vấn đề. Nếu bạn triển khai loại này, việc công bố rõ ràng phạm vi giải mã không phải là thủ tục hình thức mà là điều kiện để hệ thống còn đáng tin.

TLS terminating proxy

Proxy giữ khoá riêng và chứng chỉ, giải mã ở biên, nói HTTP thường (hoặc TLS mới) với backend. Ba chế độ:

Chế độ Proxy làm gì Đọc được nội dung Chặng proxy → backend
Passthrough Bơm byte, tối đa là đọc SNI để định tuyến Không Vẫn mã hoá end-to-end
Termination Giải mã, xử lý, gửi backend bằng HTTP thường Trần — mạng phải tin cậy
Re-encryption (bridging) Giải mã, xử lý, mã hoá lại tới backend Mã hoá, nhưng proxy vẫn thấy hết

Termination là mặc định hợp lý khi proxy và backend nằm trong cùng một host hoặc cùng một mạng riêng bạn kiểm soát. Nó không hợp lý khi chặng đó đi qua Internet hoặc qua mạng của bên thứ ba — lúc đó chọn re-encryption và chấp nhận thêm một lần bắt tay TLS.

Đây cũng là chỗ tập trung việc quản lý chứng chỉ: một nơi gia hạn, một nơi cấu hình phiên bản TLS (RFC 8446 cho TLS 1.3), backend không cần biết gì về TLS. Đổi lại, mất khoá ở proxy là mất tất cả.

WAF — Web Application Firewall

Là reverse proxy L7 gắn thêm bộ luật kiểm tra request (và đôi khi response) trước khi cho đi tiếp. Hai mô hình:

  • Negative model (phổ biến): danh sách mẫu xấu — SQL injection, XSS, path traversal. Bộ luật mở phổ biến nhất là OWASP Core Rule Set, chạy được trên ModSecurity và trên Coraza (bản viết bằng Go, tương thích cú pháp luật ModSecurity — hợp với đội Go vì nhúng thẳng vào middleware được).
  • Positive model: chỉ cho qua những gì khớp đặc tả (schema OpenAPI chẳng hạn). An toàn hơn nhiều, tốn công khai báo hơn nhiều.

WAF không thay được code đúng. Nó mua cho bạn thời gian để vá, và chặn được lớp quét tự động ồn ào. Nhưng bộ luật negative luôn có false positive — một bài viết trên diễn đàn community có đoạn code chứa ' OR 1=1 là nội dung hợp lệ, WAF thì thấy đó là tấn công. Bật WAF ở chế độ chỉ ghi log vài tuần trước khi bật chặn, nếu không bạn sẽ chặn chính người dùng của mình.

API gateway

Reverse proxy L7 cộng thêm những thứ thuộc về chính sách API: xác thực và cấp quyền tập trung, rate limit theo API key, quota, versioning, đôi khi gộp nhiều lời gọi backend thành một response. Ranh giới với reverse proxy thuần là ranh giới về trách nhiệm, không phải về cơ chế: reverse proxy trả lời "gửi request này đi đâu", gateway trả lời "request này có được phép tồn tại không". Chi tiết và chỗ đặt nó trong dự án — §9.

Service mesh sidecar

Một proxy chạy cạnh mỗi tiến trình, chặn cả traffic vào lẫn ra của riêng tiến trình đó. Đổi lại chi phí, bạn có mTLS tự động giữa mọi service, retry/timeout/circuit breaking khai báo bằng cấu hình thay vì code, và metric đồng nhất mà không phải sửa ứng dụng.

Với community ở hình dạng hiện tại, service mesh gần như chắc chắn là thừa. Đây là modular monolith: ba binary, và các module không gọi nhau qua mạng — chúng nói chuyện qua event Kafka. Mesh giải bài toán "hàng chục service gọi nhau qua HTTP nội bộ"; bài toán đó ở đây không tồn tại. Cái giá thì có thật: thêm một hop mạng cho mỗi chiều, thêm bộ nhớ mỗi pod, thêm một control plane phải vận hành và nâng cấp. Xem §9 trước khi cài.

Egress / forward proxy trong doanh nghiệp

Đây là loại liên quan trực tiếp nhất tới dự án này mà lại ít được nói tới. cmd/worker gọi ra ngoài: webhook, dịch vụ ảnh, API bên thứ ba. Một egress proxy đặt ở cửa ra cho bạn bốn thứ:

  1. Allowlist domain. Worker chỉ được nói chuyện với những host đã duyệt. Đây là biện pháp phòng thủ hiệu quả nhất chống SSRF và chống exfiltration sau khi bị chiếm quyền: kẻ tấn công chạy được code trong worker nhưng không gửi dữ liệu ra đâu được.
  2. Một địa chỉ IP đi ra duy nhất và ổn định. Đối tác cần allowlist IP của bạn — không thể đưa họ dải IP thay đổi mỗi lần scale.
  3. Một chỗ duy nhất để ghi log traffic ra. Trả lời được "hệ thống mình gọi ra ngoài những đâu" là câu hỏi mà không ai trả lời được cho tới khi có proxy này.
  4. Một chỗ duy nhất để đặt timeout và rate limit với bên thứ ba, thay vì rải rác trong code.

Cách nối vào ứng dụng Go: đặt HTTPS_PROXY cho tiến trình và dùng http.ProxyFromEnvironment — nhưng nhớ cái bẫy ở 2.3: một http.Transport viết tay quên trường Proxy sẽ đi vòng qua toàn bộ chính sách này mà không báo lỗi gì.

Residential / mobile proxy

Proxy có IP thuộc dải của nhà mạng dân dụng hoặc di động thay vì dải datacenter, nên khó bị phân loại là traffic tự động hơn. Đi kèm là câu hỏi nguồn gốc IP đó ở đâu ra và ai đã đồng ý những gì — cùng với cơ chế hồ, xoay vòng và vận hành, để ở §6.


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í