Logo PayFS CPPAI × EzyPlatform
Loạt bài · Bài 2

Sử dụng bộ công cụ của CPPAI dành cho EzyPlatform để tạo trò chơi tìm hiểu Nginx

Từ mã nguồn nginx tới một trò chơi chạy trên EzyPlatform: giao việc cho Claude Code đọc source, dựng một trang độc lập, thêm quái vật, lỗ hổng CVE có thật và âm thanh, quay trailer tự động, rồi đưa lên site bằng bộ công cụ ezy của CPPAI.

Ở bài 1, chúng ta đã cài lệnh ezy, cài plugin ezyplatform cho Claude Code và dựng EzyPlatform chạy local bằng Docker. Bài này dùng chính bộ công cụ đó cho một việc cụ thể và vui hơn: biến mã nguồn nginx thành một trò chơi học tập, chạy như một trang của site EzyPlatform. Gần như toàn bộ mã của trò chơi do agent viết; phần của con người là đặt yêu cầu, chọn hướng và duyệt trước mỗi lần ghi lên site.

Kết quả: NGINX Packet Quest

Trailer 60 giây, do chính trò chơi tự diễn và được quay tự động (xem bước 6).

Người chơi là một gói tin GET /api/orders. Gói tin đi qua thành phố NGINX theo đúng vòng đời của một request trong mã nguồn: từ tiến trình master, vòng lặp epoll, bộ phân tích HTTP, 11 giai đoạn xử lý, upstream, chuỗi bộ lọc cho tới keepalive. Ở mỗi trạm có một NPC giải thích đoạn code tương ứng rồi hỏi một câu; trả lời đúng thì cổng mở.

17 trạm

Đi theo mã nguồn

Mỗi NPC trích hàm thật như ngx_http_core_run_phases(), ngx_http_upstream_get_peer(), kèm giá trị mặc định đã kiểm chứng.

13 quái vật

Kiểu tấn công có thật

Slowloris, thundering herd, request smuggling, SSRF, brute-force… Muốn hạ quái phải chọn đúng cơ chế của nginx.

12 CVE · 6 bẫy

Bảo mật thật

Lỗ hổng lấy từ changelog của nginx, kèm những lỗi cấu hình hay gặp như alias thiếu dấu /.

Bạn có thể chơi thử ngay trên trình duyệt. Toàn bộ trò chơi nằm trong một file content.html khoảng 240 KB: canvas vẽ bản đồ, âm thanh tự tổng hợp bằng Web Audio, không dùng thư viện ngoài.

Chuẩn bị

  • Dự án site đã tạo ở bài 1 (ví dụ ezy new mmo-game --mode local), đã cài EzyArticle và ezy check báo kết nối thành công.
  • Claude Code đã có plugin ezyplatform@cppai.
  • Mã nguồn nginx đặt trong thư mục dự án để agent đọc được:
git clone https://github.com/nginx/nginx.git

Bản chúng tôi dùng là 1.31.9 (đọc từ src/core/nginx.h).

Bước 1: Cho agent đọc mã nguồn nginx

Mọi thứ bắt đầu từ một yêu cầu bằng tiếng Việt trong Claude Code, mở tại thư mục dự án:

Đọc mã nguồn của nginx và tạo một trò chơi ở dạng trang độc lập. Người chơi đóng vai một gói tin tìm cách đi qua bản đồ chứa các luồng của nginx, ở mỗi điểm có NPC giải thích ý nghĩa và cách đi qua. Bản đồ sinh động, có cây cối hoa cỏ, theo bộ nhận diện thương hiệu của nginx.

Agent không viết ngay. Nó đọc các file cốt lõi trước: src/event/ngx_event.c (vòng lặp sự kiện), src/event/ngx_event_accept.c, src/http/ngx_http_request.c (đọc và phân tích request, keepalive), src/http/ngx_http_core_module.c (11 giai đoạn, tìm location), src/http/ngx_http_upstream_round_robin.c (cân bằng tải) và danh sách bộ lọc trong auto/modules.

Điều đáng giá nhất ở bước này: trước khi đưa một con số vào lời thoại, agent dò lại trong code. Ví dụ:

NPC nóiBằng chứng trong mã nguồn
client_header_timeout mặc định 60sngx_conf_merge_msec_value(conf->client_header_timeout, prev->client_header_timeout, 60000)
Vòng lặp rewrite bị chặn sau 10 lầnr->uri_changes = NGX_HTTP_MAX_URI_CHANGES + 1, với hằng số bằng 10
HTTP thường gửi vào cổng HTTPS nhận mã nội bộ 497#define NGX_HTTP_TO_HTTPS 497, phát hiện qua recv(..., MSG_PEEK) và byte 0x16
limit_req từ chối mặc định bằng 503ngx_conf_merge_uint_value(conf->status_code, prev->status_code, NGX_HTTP_SERVICE_UNAVAILABLE)
Round-robin “mượt” chọn A, A, B với trọng số 5/1/1Tính tay theo current_weight += effective_weight trong ngx_http_upstream_get_peer()

Kết quả là 17 trạm, mỗi trạm có lời thoại, một đoạn code trích dẫn và một câu hỏi ba lựa chọn. Mỗi lựa chọn đều có lời giải thích vì sao đúng hay sai.

Bước 2: Dựng trò chơi thành một trang độc lập

Trò chơi cần toàn màn hình, không dùng header và footer chung của site, nên agent chọn kiểu trang STANDALONE_PAGE theo quy ước trong AGENTS.md của dự án: toàn bộ HTML, CSS và JavaScript nằm trong content.html.

pages/game-hanh-trinh-goi-tin-nginx/
├── content.html   # toàn bộ trò chơi
└── meta.json
{
  "slug": "game-hanh-trinh-goi-tin-nginx",
  "title": "Hành trình gói tin NGINX",
  "pageType": "STANDALONE_PAGE",
  "contentType": "HTML",
  "status": "DRAFT",
  "metadata": {}
}

Bản đồ được vẽ trên canvas: đường đi nối các trạm, quảng trường lát gạch lục giác màu xanh #009639 của nginx, cây, hoa, nấm, hồ nước, một dòng sông có cầu gỗ ở trạm bộ lọc và bướm bay. Trò chơi có minimap, sổ tay ghi lại những gì đã học, điều khiển bằng bàn phím và có bàn phím ảo cho điện thoại.

Bản đồ thành phố NGINX với các trạm, NPC, quái vật và cây hoa
Bản đồ thành phố NGINX: mỗi quảng trường là một trạm, NPC có dấu ! đang chờ bạn.

Cẩn thận với Thymeleaf. EzyArticle có thể xử lý trang bằng Thymeleaf, và [[…]] trong văn bản bị hiểu là biểu thức inline. Agent đã soát và tránh mọi chỗ có hai dấu [ liền nhau trong JavaScript, chẳng hạn mảng lồng nhau viết liền nhau.

Bước 3: Làm trò chơi sinh động qua từng vòng trao đổi

Phần còn lại là một chuỗi yêu cầu ngắn. Khi yêu cầu còn mơ hồ, agent đề xuất trước, đợi người duyệt rồi mới làm:

Người dùng nóiAgent làm
“Có thể thêm quái vật được không? Tôi chưa biết ý nghĩa của chúng, hãy đề xuất.”Đề xuất 13 quái, mỗi con là một kiểu tấn công hoặc sự cố có thật, đặt đúng ở trạm nginx chống lại nó, kèm “vũ khí” là chỉ thị cấu hình.
“Cả 13 con, phải hạ, chạm vào là mở trận, chết là đi lại từ đầu.”Quái tuần tra và đuổi theo; trận đấu là câu hỏi chọn vũ khí; cổng chỉ mở khi đã hạ hết quái của trạm.
“Nhân vật phải có máu”, “quái cũng phải có máu”, “cần âm thanh”Gói tin có 100 HP, quái thường 2 hiệp, boss Zombie Upstream 3 hiệp; nhạc nền, nhạc chiến đấu và hiệu ứng tổng hợp bằng Web Audio, không cần file âm thanh.
“Trên điện thoại có nên nhắc quay ngang không?”Phân tích khung nhìn (dọc chỉ thấy 9 ô, ngang thấy 22 ô), rồi làm lời nhắc có thể bỏ qua, layout riêng cho màn ngang thấp và nút toàn màn hình trên Android.
“Đóng trình duyệt mà âm thanh vẫn còn”Tạm dừng AudioContext khi trang bị ẩn (visibilitychange, pagehide, freeze), phát tiếp khi quay lại.
Trận đấu với Đàn Bò Rừng, thanh máu của gói tin và quái vật
Trận đấu với Đàn Bò Rừng (thundering herd): vũ khí đúng là accept_mutex hoặc reuseport, và hiệp 2 hỏi về cờ EPOLLEXCLUSIVE.
Trận đấu với boss Zombie Upstream
Boss Zombie Upstream: ba hiệp về ngx_http_upstream_next(), max_fails/fail_timeout và proxy_read_timeout.

Bước 4: Đưa lỗ hổng bảo mật vào trò chơi

Khi được hỏi về “những lỗ hổng bảo mật bạn nhận thấy từ nginx”, agent trả lời thẳng: khi đọc code, nó không tự phát hiện lỗ hổng mới nào. Nguồn đáng tin là danh sách lỗ hổng đã công bố và đã vá mà chính kho mã ghi lại trong docs/xml/nginx/changes.xml, khoảng 45 mục từ 2005 tới 2026.

Từ đó trò chơi tách hai khái niệm mà người mới hay nhầm:

  • Quái vật là tấn công từ bên ngoài, nginx cấu hình đúng sẽ chống được.
  • Vết nứt CVE là lỗi nằm trong chính nginx: cấu hình đúng cũng không cứu được, phải vá (nâng cấp). Mỗi vết nứt được vá qua ba bước: ai bị ảnh hưởng, hậu quả, cách xử lý. Thước trên HUD cho biết phiên bản nginx tối thiểu để chặn các lỗ hổng đã vá.
  • Bẫy cấu hình là lỗi do người viết cấu hình: alias thiếu /, $uri trong header dẫn tới CRLF injection, add_header bị mất khi khai báo lại ở location, set_real_ip_from 0.0.0.0/0, lộ thư mục .git, proxy_pass theo tham số dẫn tới SSRF.
Vá vết nứt CVE-2025-23419 qua ba bước
Vá CVE-2025-23419 (tái dùng session TLS giữa các virtual server): bước 1 hỏi ai bị ảnh hưởng.
Dẫm phải bẫy cấu hình alias thiếu dấu gạch chéo
Dẫm phải bẫy location /img { alias /data/images/; }: request /img../secret.txt thoát ra ngoài thư mục ảnh.

Nội dung CVE chỉ dừng ở mức của changelog: cái gì, ảnh hưởng gì, vá ở phiên bản nào, cách tạm giảm thiểu. Không có payload hay cách khai thác.

Bước 5: Để agent tự kiểm thử

Trò chơi lớn dần lên khoảng 3.000 dòng. Agent tự kiểm tra sau mỗi lần sửa, không đợi người chơi thử mới phát hiện lỗi:

  • Kiểm tra cú pháp JavaScript bằng node --check.
  • Chạy logic trò chơi trong Node với một DOM giả: bắt đầu, đánh quái, vá CVE, dẫm bẫy, chết rồi reset, và cả màn chiến thắng.
  • Mô phỏng AudioContext để kiểm tra âm thanh dừng khi trang bị ẩn và phát lại khi hiện.
  • Trích khung hình từ video trailer rồi tự xem. Nhờ vậy đã phát hiện phụ đề đè lên nhân vật và một câu hỏi bị lặp chữ.

Bước 6: Quay trailer tự động

Thay vì tự chơi rồi quay màn hình, agent thêm vào trò chơi một chế độ trailer (?trailer=1): một kịch bản khoảng 60 giây dùng chính các hàm thật của trò chơi. Camera lia qua thành phố, gói tin tự đi, tự đánh quái, vá CVE, dẫm bẫy, hạ boss, kèm phụ đề tiếng Việt. Một script quay tự động nằm trong tools/trailer/:

cd tools/trailer
npm install
npm run record
  • Playwright mở Chromium ở 1920×1080 và quay bằng CDP screencast. Mỗi khung hình có dấu thời gian để video khớp đúng thời gian thực.
  • Âm thanh được render lại offline bằng OfflineAudioContext theo đúng các sự kiện đã diễn ra, xuất thành WAV.
  • ffmpeg ghép hình và tiếng thành MP4 H.264, chuẩn hoá âm lượng bằng loudnorm, kèm ảnh poster.

Mỗi khi trò chơi thay đổi, chỉ cần chạy lại là có trailer mới.

Gói tin về tới nhà, cảnh cuối của trailer
Cảnh cuối của trailer: gói tin mang 200 OK về nhà.

Bước 7: Triển khai lên EzyPlatform bằng bộ công cụ ezy

Trong terminal, bật site local và kiểm tra kết nối như ở bài 1:

cd mmo-game
docker compose up -d
ezy check

Với ezy 0.4.0, mọi việc liên quan đến nội dung (đồng bộ, publish, kiểm tra trang) làm qua coding agent; lệnh ezy chỉ lo thiết lập và kiểm tra. Mở Claude Code trong thư mục dự án rồi nhắn:

Bạn nhắnSkillKết quả
“Trang game-hanh-trinh-goi-tin-nginx trên máy và trên server có khớp không?”ezy-syncBáo trang có trên server chưa, có lệch nội dung không
“Publish trang game-hanh-trinh-goi-tin-nginx”ezy-publishChạy thử, đợi bạn duyệt, sao lưu, publish, đọc lại kiểm tra, in lệnh khôi phục
“Kiểm tra trang game trên site local”ezy-verifyKiểm tra http://localhost:8080/game-hanh-trinh-goi-tin-nginx

Lần đầu tạo trang

Ở plugin ezyplatform 0.5.0, ezy-publish chỉ cập nhật trang đã có trên server. Trang hoàn toàn mới phải tạo một lần bằng extension EzyArticle cho VS Code: mở thư mục dự án, mở content.html của trang rồi chạy EzyArticle: Publish Content To Server. Từ lần sau, mọi cập nhật đi qua ezy-publish với chạy thử và sao lưu.

Extension publish vào môi trường mặc định của thư mục đang mở. Khi làm bài này, lệnh Publish chạy nhầm trong cửa sổ VS Code của một dự án khác, vốn trỏ tới site thật, và trò chơi lên thẳng production thay vì Docker local. Nếu bạn có nhiều dự án, hãy mở mỗi dự án trong một cửa sổ VS Code riêng và kiểm tra environment.json trước khi publish.

Để người dùng không cần VS Code và luôn thấy rõ server đích trước khi ghi, chúng tôi đã gửi pull request #1 cho repo cppai-vn/agents-plugins. Với bản sửa này, ezy-publish tạo được trang và fragment chưa có trên server, bằng đúng API save-simply mà extension dùng. Bản chạy thử ghi rõ bài nào sẽ tạo mới và tạo lên server nào. Status lấy theo meta.json (mặc định DRAFT). Sau khi tạo, script in lệnh remove để hoàn tác. Khi bản này được phát hành, lần publish đầu tiên cũng chỉ còn là một câu nhắn cho agent.

Những điều rút ra

kiểm chứng

Mọi con số đều trỏ về code

Yêu cầu agent đọc source và dò lại từng giá trị mặc định, mã lỗi trước khi viết lời thoại. Với bảo mật, dựa vào changelog chính thức thay vì đoán.

vòng ngắn

Đề xuất trước, làm sau

Những câu hỏi mở như “nên thêm gì”, “có nên nhắc xoay ngang” cho ra phương án rõ ràng để chọn, thay vì một khối code bất ngờ.

plan → duyệt → apply

Ghi lên site luôn có người duyệt

Bộ công cụ ezy chạy thử trước, sao lưu và in lệnh khôi phục. Hãy đọc kỹ bản chạy thử, nhất là server đích.

Tiếp theo

Bạn đã thấy một vòng làm việc trọn vẹn với bộ công cụ của CPPAI: từ ý tưởng, mã nguồn, kiểm thử, trailer tới triển khai lên EzyPlatform. Hãy chơi thử NGINX Packet Quest, và xem các bài khác tại mục lục loạt bài.

Bộ nhận diện thương hiệu của CPPAI, lời cảm ơn từ EzyPlatform