Giới thiệu
Trong bài viết này, tôi sẽ hướng dẫn bạn cách khắc phục lỗi khi sử dụng proxy SMS viết bằng Rust trên Google Cloud Platform (GCP). Bạn có thể đã gặp phải các lỗi như 404 Not Found hay Cloudflare 1003 khi gọi API từ địa chỉ IP thay vì tên miền. Bằng cách sử dụng Caddy, chúng ta sẽ thiết lập một endpoint HTTPS an toàn cho ứng dụng của bạn.
Tóm tắt
TL;DR: Tôi đã gọi proxy SMS Rust của mình trên một VM GCP qua IP:8080, gây ra lỗi Cloudflare 1003 và cảnh báo từ trình duyệt. Tôi đã ánh xạ một hostname (example-proxy.example.com) tới VM, mở cổng 80/443, sử dụng Caddy trước ứng dụng Rust của mình, và để nó tự động phát hành chứng chỉ Let’s Encrypt. Kết quả là một endpoint HTTPS sạch sẽ và một biến môi trường dễ dàng cho Cloudflare Worker: SMS_API_URL=https://example-proxy.example.com/sms/send.
Tại sao lại viết bài này?
Tôi đã xây dựng một ứng dụng Rust để proxy các yêu cầu SMS. Nó chạy tốt tại http://203.0.113.45:8080, nhưng khi tôi tích hợp nó với Cloudflare Worker và frontend, tôi bắt đầu thấy:
404 Not Found(từ nguồn gốc của tôi trên các đường dẫn chưa được định nghĩa)Cloudflare 1003(khi yêu cầu được thực hiện bằng IP thay vì hostname)
Bài viết này sẽ ghi lại những lỗi này thực sự có nghĩa là gì, cách mà stack web nhìn nhận yêu cầu của bạn, và các lệnh/cấu hình chính xác mà tôi đã sử dụng để khắc phục - cho sử dụng sản xuất thực tế.
Kiến trúc hệ thống (trước & sau)
Trước
- Rust SMS proxy (Axum/Actix/Hyper) → lắng nghe trên 0.0.0.0:8080
- Các yêu cầu từ Cloudflare Worker / trình duyệt đôi khi sử dụng IP:8080 trực tiếp
- Không có HTTPS trên nguồn → nội dung hỗn hợp và các hạn chế proxy
Sau
- Tên miền DNS:
example-proxy.example.com→ A record →203.0.113.45 - Caddy trên VM lắng nghe trên 80/443, kết thúc TLS với Let’s Encrypt
- Caddy reverse_proxy tới Rust tại 127.0.0.1:8080
- Biến môi trường Worker:
SMS_API_URL=https://example-proxy.example.com/sms/send
Các triệu chứng và ý nghĩa thực sự của chúng
-
404 Not Found: Nguồn gốc (ứng dụng Rust của bạn hoặc máy chủ upstream) không có đường dẫn cho yêu cầu bạn đã thực hiện (ví dụ:
/health). Đây không phải là lỗi của Cloudflare; đó là lỗi của ứng dụng/router của bạn. -
Cloudflare 1003: Cloudflare không cho phép bạn truy cập mạng của nó bằng IP thô hoặc với tiêu đề Host không khớp. Nếu mã của bạn
fetch()http://203.0.113.45:8080/...qua một vùng CF, hãy mong đợi điều này. Cloudflare muốn một tên miền mà vùng biết đến. -
Nội dung hỗn hợp / Vấn đề TLS: Nếu frontend của bạn là HTTPS nhưng API của bạn là HTTP, trình duyệt có thể chặn hoặc cảnh báo. Bạn muốn HTTPS → HTTPS.
Từ vựng chính (định nghĩa nhanh, thực tiễn)
- Hostname / FQDN: Một tên dễ đọc cho con người (ví dụ:
example-proxy.example.com) ánh xạ tới một IP qua DNS. - A record: Bản ghi DNS ánh xạ một hostname tới một địa chỉ IPv4.
- Reverse proxy: Một máy chủ công khai chuyển tiếp các yêu cầu đến backend riêng tư (ứng dụng Rust của chúng tôi).
- TLS (SSL): Mã hóa cho HTTP. Cung cấp cho bạn
https://và khóa xanh của trình duyệt. - Let’s Encrypt: Một CA miễn phí phát hành chứng chỉ tự động. Caddy sẽ xử lý điều này cho bạn.
- SNI: Tiện ích mở rộng TLS cho phép một IP phục vụ nhiều hostname bằng cách thông báo cho máy chủ cert nào sẽ sử dụng.
- HTTP-01 challenge: Kiểm tra Let’s Encrypt chứng minh bạn kiểm soát một tên miền bằng cách trả lời một yêu cầu trên cổng 80.
- Origin: Máy chủ ứng dụng thực tế của bạn (ở đây: Rust trên 127.0.0.1:8080).
- Edge: Điểm vào công cộng (ở đây: Caddy trên 80/443; đôi khi là Cloudflare hoặc một bộ cân bằng tải).
- CORS: Chính sách bảo mật của trình duyệt cho các yêu cầu cross-origin. Nếu một Cloudflare Worker gọi máy chủ nguồn của bạn từ máy chủ tới máy chủ, bạn thường không cần CORS trên nguồn.
- Project ID (GCP): Chuỗi định danh cho dự án Google Cloud của bạn (không phải số dự án số).
Cách sửa lỗi - các bước chính xác (dễ sao chép/dán)
1) Chỉ định một hostname cho VM của bạn
Tôi đã sử dụng DuckDNS để tạo example-proxy.example.com trỏ tới 203.0.113.45.
Kiểm tra nhanh:
dig +short example-proxy.example.com
# Mong đợi: 203.0.113.45
2) Mở cổng 80 và 443 trong Google Cloud
Chúng ta cần 80 cho Let’s Encrypt (HTTP-01) và 443 cho lưu lượng HTTPS thực tế.
gcloud compute firewall-rules create allow-http --allow tcp:80 --description="Cho phép HTTP"
gcloud compute firewall-rules create allow-https --allow tcp:443 --description="Cho phép HTTPS"
Nếu
gcloudbáo lỗi về dự án của bạn, hãy thiết lập trước:
gcloud config set project YOUR_PROJECT_ID
3) Cài đặt Caddy
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
4) Cấu hình Caddy làm reverse proxy tới Rust
Tạo /etc/caddy/Caddyfile:
example-proxy.example.com {
reverse_proxy 127.0.0.1:8080
}
Tải lại Caddy và kiểm tra trạng thái:
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
Điều gì xảy ra ở đây: Caddy tự động lấy chứng chỉ Let’s Encrypt cho
example-proxy.example.com(sử dụng HTTP-01 challenge trên cổng 80), sau đó bắt đầu phục vụ HTTPS trên cổng 443 với chứng chỉ đó.
5) Liên kết ứng dụng Rust của bạn với localhost
Giữ cổng 8080 riêng tư phía sau Caddy:
./your_proxy_binary --host 127.0.0.1 --port 8080
Nếu bạn đã mở cổng 8080 ra internet trước đó, bạn có thể xóa/vô hiệu hóa quy tắc tường lửa đó để đảm bảo an toàn.
6) Kiểm tra như một chuyên gia
Nếu bạn có một đường dẫn sức khỏe:
curl -I https://example-proxy.example.com/health
Nếu không, hãy truy cập endpoint thực của bạn:
curl -i -X POST \
-H "content-type: application/json" \
-d '{"to":"+821012345678","text":"hello"}' \
https://example-proxy.example.com/sms/send
Bạn sẽ thấy một phản hồi HTTPS sạch từ dịch vụ Rust của bạn thông qua Caddy.
7) Chỉ định môi trường Cloudflare Worker / backend tới hostname
Trong mã:
const endpoint = env.SMS_API_URL || "https://example-proxy.example.com/sms/send";
Trong wrangler.toml:
[vars]
SMS_API_URL = "https://example-proxy.example.com/sms/send"
Triển khai Worker/Pages của bạn và bạn đã hoàn tất.
Tại sao điều này hoạt động (giải thích dòng yêu cầu)
- DNS phân giải
example-proxy.example.com → 203.0.113.45. - Trình duyệt (hoặc Worker) kết nối tới cổng 443 và nói qua SNI: “Tôi là
example-proxy.example.com.” - Caddy trình bày chứng chỉ Let’s Encrypt hợp lệ cho tên miền đó.
- Caddy reverse_proxy yêu cầu tới
http://127.0.0.1:8080(ứng dụng Rust của bạn). - Phản hồi được truyền trở lại qua kết nối TLS an toàn tới client.
Kết quả: một khóa xanh đáng tin cậy, không có nội dung hỗn hợp, không có Cloudflare 1003, và một URL ổn định duy nhất cho ứng dụng của bạn.
Khắc phục sự cố & Câu hỏi thường gặp
Q: Caddy không lấy được chứng chỉ.
- Đảm bảo
example-proxy.example.comphân giải tới203.0.113.45. - Đảm bảo các cổng 80/443 được mở và có thể truy cập từ internet.
- Kiểm tra nhật ký:
journalctl -u caddy -n 100 --no-pager. - Xác nhận không có gì khác đang lắng nghe trên cổng 80.
Q: Tôi vẫn nhận được 404 trên /health.
- Đó là router Rust của bạn, không phải Caddy. Hãy thêm một đường dẫn hoặc kiểm tra
/sms/send.
Q: Tôi có cần CORS không?
- Nếu trình duyệt gọi trực tiếp nguồn gốc Rust của bạn, có. Nếu Worker gọi máy chủ nguồn từ máy chủ tới máy chủ, bạn thường không cần CORS trên nguồn.
Q: Tôi có thể sử dụng Google HTTPS Load Balancer thay vì không?
- Có. Nó quản lý TLS và mở rộng tốt, nhưng thiết lập nặng hơn và có thể tăng chi phí. Caddy là một lựa chọn tốt cho một VM duy nhất.
Q: Tôi có thể tránh mở cổng với Cloudflare Tunnel không?
- Có. Một đường hầm ánh xạ một tên miền công khai tới
127.0.0.1:8080mà không cần mở cổng. Đây cũng là một lựa chọn tốt.
Bảo mật & tăng cường sản xuất
- Liên kết Rust với 127.0.0.1 để chỉ Caddy có thể truy cập.
- Đóng cổng 8080 từ internet nếu bạn đã mở trước đó.
- Sử dụng biến môi trường cho các endpoint và bí mật.
- Giới hạn tỷ lệ / xác thực cho
/sms/sendđể ngăn chặn lạm dụng. - Giám sát nhật ký (
journalctl, nhật ký ứng dụng) và đặt cảnh báo. - Sao lưu & cập nhật: giữ Caddy và các gói hệ điều hành được cập nhật.
Thêm: Đường dẫn sức khỏe tối giản trong Rust
Axum
rust
use axum::{routing::{get, post}, Router};
use std::net::SocketAddr;
async fn health() -> &'static str { "ok" }
async fn send_sms() { /* xử lý của bạn */ }
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/health", get(health))
.route("/sms/send", post(send_sms));
let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
Actix-web
rust
use actix_web::{get, post, App, HttpServer, Responder, HttpResponse};
#[get("/health")]
async fn health() -> impl Responder { HttpResponse::Ok().body("ok") }
async fn send_sms() -> impl Responder { HttpResponse::Ok().finish() }
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(health).route("/sms/send", actix_web::web::post().to(send_sms)))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Danh sách kiểm tra cuối cùng
- [ ] DNS:
example-proxy.example.com → 203.0.113.45 - [ ] Tường lửa GCP: 80/443 được cho phép
- [ ] Caddy đã cài đặt và
reverse_proxy 127.0.0.1:8080 - [ ] Rust được liên kết với 127.0.0.1:8080
- [ ]
curl -I https://example-proxy.example.com/…hoạt động - [ ] Biến môi trường Worker:
SMS_API_URL=https://example-proxy.example.com/sms/send
Những điều cần nhớ
- Đừng gọi backend của bạn bằng IP thô—sử dụng một hostname.
- Đặt một reverse proxy (Caddy/Nginx) ở rìa để xử lý TLS.
- Giữ ứng dụng của bạn riêng tư trên localhost; chỉ mở 80/443.
- Ưu tiên các endpoint dựa trên biến môi trường để triển khai sạch sẽ.
Bây giờ proxy SMS Rust của bạn đã sẵn sàng cho sản xuất với một URL HTTPS thích hợp mà frontend và Workers của bạn có thể tin tưởng.