Vượt CGNAT bằng reverse tunnel qua VPS với frp
CGNAT khiến port forwarding vô dụng. Dùng frp trên VPS giá rẻ, cho máy trong nhà chủ động mở tunnel và terminate HTTPS bằng certificate thật.
Vì sao port forwarding không có tác dụng phía sau CGNAT
Phía sau CGNAT (carrier-grade network address translation), địa chỉ WAN trên router của bạn được chia sẻ với các thuê bao khác. Vì vậy, bạn không sở hữu public IP nào và không có cổng nào để forward. Reverse tunnel giải quyết vấn đề này: một VPS giá rẻ giữ public IP, máy trong nhà của bạn chủ động kết nối ra VPS, rồi các request đi vào sẽ quay ngược qua kết nối mà máy trong nhà đã mở. Bạn vẫn dùng phần cứng hiện có. Bạn chỉ thuê thứ ISP không cung cấp, đó là một địa chỉ có thể định tuyến.
Mỗi command bên dưới đều ghi rõ command chạy trên máy nào. Cần 2 máy: VPS có public IP và máy trong nhà đang chạy service mà bạn muốn truy cập.
Cách xác định bạn có thực sự ở sau CGNAT hay không
Mở trang quản trị của router và xem địa chỉ WAN mà router hiển thị. Sau đó hỏi Internet xem nó nhìn thấy địa chỉ nào.
# on the home box
curl -4 -s https://ifconfig.me; echoNếu hai địa chỉ trùng nhau, bạn có public IP và không cần làm những bước này. Forward port rồi dừng đọc. Nếu địa chỉ WAN của router nằm trong 100.64.0.0/10, bạn đang ở sau CGNAT. Đây là dải địa chỉ dùng chung theo RFC 6598, được dành riêng cho đúng mục đích này. Một số ISP đặt 10.0.0.0/8 ở phía WAN thay vào đó. Tình huống vẫn giống nhau, chỉ khác tên gọi.
Hãy kiểm tra một việc trước khi thuê bất kỳ dịch vụ nào. Nhiều ISP dùng CGNAT cấp một IPv6 prefix thực sự. Nếu máy trong nhà có địa chỉ IPv6 global, bạn có thể mở firewall cho địa chỉ đó và bỏ qua tunnel hoàn toàn. Cách này sẽ không hoạt động khi khách truy cập đang ở trên mạng chỉ có IPv4. Vì vậy cuối cùng hầu hết mọi người vẫn phải dùng cách này.
Cách reverse tunnel trên VPS hoạt động, khi kết nối được khởi tạo từ bên trong
CGNAT và các router gia đình thông thường đều chặn kết nối inbound không được yêu cầu. Firewall của doanh nghiệp cũng vậy. Chúng không chặn kết nối outbound, vì đó là loại kết nối mà mọi trình duyệt và mọi client cập nhật đều sử dụng hằng ngày. Khi thiết bị NAT phát hiện một kết nối TCP outbound, nó tạo mapping cho kết nối đó rồi cho phép traffic phản hồi đi qua cùng kết nối. Không có gì từ bên ngoài có thể khởi tạo kết nối đến máy trong mạng gia đình của bạn. Vì vậy, máy trong mạng gia đình sẽ khởi tạo kết nối, còn tunnel chuyển traffic quay ngược qua chính kết nối đó.
Đó là toàn bộ cơ chế. Máy trong mạng gia đình kết nối ra VPS qua một cổng và giữ kết nối mở. VPS nhận các request public rồi chuyển chúng xuống qua kết nối đã có sẵn. Không có thành phần nào cố gắng truy cập địa chỉ IP tại nhà của bạn, nên không cần mở đường cho việc đó.
Cơ chế này có 2 hệ quả, và cả hai đều hữu ích. DNS record của bạn trỏ đến VPS, không bao giờ trỏ đến mạng gia đình. Địa chỉ public của bạn lúc này là địa chỉ của VPS. Vì vậy, bất kỳ thông tin nào observer lấy được khi tra cứu IP sẽ là thông tin về một server thuê, thay vì đường truyền Internet tại nhà của bạn.
Ba cách để triển khai
ssh -R: đã được cài đặt ở cả hai đầu, phù hợp với một service hoặc bản demo tạm thời. Công cụ này không có dashboard và không có logic tự kết nối lại đáng kể.- frp: một server Go nhỏ (
frps) và client tương ứng (frpc). Phù hợp với thiết lập lâu dài có nhiều service phía sau một hostname. Đây là phần chính của hướng dẫn. - Một mesh VPN: Tailscale hoặc một WireGuard server do bạn tự vận hành. Phù hợp khi bạn muốn các thiết bị của mình kết nối riêng tư với nhau thay vì public một dịch vụ lên Internet.
Chọn mesh nếu mục tiêu của bạn là truy cập riêng tư từ các thiết bị do bạn kiểm soát. Tailscale Serve và Funnel trình bày cách public dịch vụ ra ngoài tailnet, còn WireGuard VPN tự host trên cùng VPS cung cấp mô hình tương tự mà không cần third-party coordination server trên đường truyền. Đọc một trong hai phần đó rồi bỏ qua phần còn lại của trang này. Toàn bộ phần dưới đây giả định bạn muốn có một hostname HTTPS public mà bất kỳ ai cũng có thể truy cập.
Bản rút gọn: ssh -R cho một service
Giả sử máy chủ tại nhà chạy một app trên 127.0.0.1:3000 và bạn đã có quyền truy cập SSH vào VPS.
# on the home box
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R 127.0.0.1:8080:127.0.0.1:3000 \
tunnel@vps.example.com-R 127.0.0.1:8080:127.0.0.1:3000 yêu cầu sshd trên VPS lắng nghe trên 127.0.0.1:8080 của chính VPS rồi chuyển mọi dữ liệu nhận được tại đó đến 127.0.0.1:3000 trên máy chủ tại nhà. -N nghĩa là không chạy shell. Hai tùy chọn ServerAlive khiến ssh phát hiện kết nối đã chết sau khoảng chín mươi giây, thay vì treo trên một connection không còn tồn tại.
Đây là phần thường gây nhầm lẫn. Listener đó chạy trên loopback, nên curl http://vps.example.com:8080 từ mọi nơi khác sẽ thất bại. sshd mặc định dùng GatewayPorts no, nghĩa là remote forward chỉ bind vào loopback interface. Không khắc phục bằng cách đặt GatewayPorts yes. Giữ forward trên loopback và đặt nginx phía trước nó, giống như cách cấu hình frp bên dưới, để public port là 443 với certificate và tunnel port không bao giờ lộ ra Internet. Nếu chưa chắc hiện tại service nào đang listening và trên interface nào, bài hướng dẫn ngắn về port và listener trên Linux sẽ giúp bạn kiểm tra trong mười phút.
Nếu port đã bị chiếm trên VPS, ssh sẽ in thông báo này, còn ExitOnForwardFailure=yes khiến nó dừng lại thay vì kết nối nhưng không có tunnel hoạt động:
Warning: remote port forwarding failed for listen port 8080Nguyên nhân thường gặp là một session trước đó đã chết nhưng sshd chưa phát hiện. Đặt ClientAliveInterval 30 và ClientAliveCountMax 3 trong /etc/ssh/sshd_config của VPS để các session đã chết được dọn và giải phóng port. Đưa toàn bộ command vào một systemd unit với Restart=always và một key riêng, hoặc dùng autossh. Với nhiều hơn một service, dừng ở đây và dùng frp.
Cài frp trên VPS và ghim vào một tag
frp được phát hành dưới dạng Go binary tĩnh và không có trong các archive của Ubuntu hoặc Debian, vì vậy bạn phải tự tải một release và xác minh. Hãy ghim version. Định dạng cấu hình đã thay đổi từ v0.52.0 và tên option tiếp tục được đổi sau đó, vì vậy tutorial cũ có thể đưa cho bạn các key mà binary hiện tại không nhận diện. Hướng dẫn này sử dụng v0.71.0, được phát hành ngày 14 August 2026.
# on the VPS
FRP_VERSION=0.71.0
ARCH=amd64 # use arm64 if `uname -m` prints aarch64
cd /tmp
curl -fsSLO "https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${ARCH}.tar.gz"
curl -fsSLO "https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_sha256_checksums.txt"
sha256sum --check --ignore-missing frp_sha256_checksums.txtsha256sum phải in ra đúng một dòng:
frp_0.71.0_linux_amd64.tar.gz: OK--ignore-missing là cần thiết vì file checksum bao phủ cả eighteen release asset, còn bạn chỉ tải một trong số đó. Nếu không có flag này, sha256sum sẽ báo seventeen asset còn lại bị thiếu rồi thoát với mã khác 0. Điều đó trông giống như bước xác minh thất bại dù thực tế không có lỗi.
# on the VPS
tar xzf "frp_${FRP_VERSION}_linux_${ARCH}.tar.gz"
sudo install -m 755 "frp_${FRP_VERSION}_linux_${ARCH}/frps" /usr/local/bin/frps
sudo useradd --system --no-create-home --shell /usr/sbin/nologin frp
sudo install -d -m 750 -o root -g frp /etc/frp
frps --versionfrps --version in ra 0.71.0. Chỉ frps được cài trên VPS. frpc được cài trên máy ở nhà. Cài cả hai binary ở mọi nơi là nguyên nhân khiến nhiều người vô tình chạy tunnel server tại nhà.
Cấu hình VPS: token, bắt buộc TLS, listener loopback
Trước tiên, hãy tạo một token. Đây là lớp bảo vệ duy nhất giữa tunnel của bạn và bất kỳ ai quét cổng VPS.
# on the VPS
openssl rand -base64 32Ghi giá trị đó vào /etc/frp/frps.toml:
bindAddr = "0.0.0.0"
bindPort = 7000
# Every listener frp creates for a proxy, including the HTTP vhost, stays on loopback.
proxyBindAddr = "127.0.0.1"
vhostHTTPPort = 8080
auth.method = "token"
auth.token = "PASTE_THE_OPENSSL_OUTPUT_HERE"
transport.tls.force = true
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "PASTE_A_SECOND_SECRET_HERE"
log.level = "info"4 dòng trong số này thực hiện phần bảo mật, vì vậy hãy xem từng dòng một.
auth.token phải khớp với auth.token trên client. Nếu không có dòng này, frps sẽ chấp nhận mọi client tìm thấy cổng 7000. Client đó có thể publish bất kỳ thứ gì thông qua VPS và certificate của bạn.
transport.tls.force = true từ chối mọi control connection không dùng TLS (transport layer security). Client đã bật TLS theo mặc định từ v0.50.0, nên trên thực tế bạn không mất gì và cũng đóng được trường hợp client cũ hoặc client tự viết kết nối không mã hóa mà không báo cho bạn.
proxyBindAddr = "127.0.0.1" là dòng mà phần lớn hướng dẫn bỏ qua. Đây là lý do cấu hình này có thể chạy liên tục một cách an toàn. Dòng này chuyển mọi listener mà frp mở thay mặt cho một proxy, gồm cả HTTP vhost và mọi remotePort mà client yêu cầu, sang loopback interface. Internet hoàn toàn không thể truy cập các listener đó. Cửa public duy nhất là nginx trên cổng 443, do bạn cấu hình và kiểm soát.
webServer.addr = "127.0.0.1" giữ dashboard ngoài public interface. Dashboard hiển thị đầy đủ các private service và traffic của chúng, chỉ được bảo vệ bằng một mật khẩu HTTP basic auth, nên không được đặt trên 0.0.0.0.
Đặt ownership để token không thể được đọc bởi mọi user, sau đó kiểm tra syntax trước khi khởi động bất kỳ thứ gì:
# on the VPS
sudo chown root:frp /etc/frp/frps.toml
sudo chmod 640 /etc/frp/frps.toml
sudo -u frp frps verify -c /etc/frp/frps.tomlFile hợp lệ sẽ in ra:
frps: the configuration file /etc/frp/frps.toml syntax is okMột lưu ý về format có thể giúp bạn tránh mất 1 giờ. frp chọn parser dựa trên phần mở rộng của file và hỗ trợ .toml, .yaml, .yml và .json. Các file .ini cũ vẫn được nạp qua một đường chuyển đổi legacy, nhưng INI đã deprecated và các option mới chỉ được document cho TOML. Nếu một tutorial hướng dẫn bạn dùng section [common] và server_addr = x.x.x.x, nội dung đó có trước v0.52.0 và tên key sẽ không khớp với binary bạn vừa cài.
Chạy frps dưới dạng service không có đặc quyền
bindPort là 7000 và vhostHTTPPort là 8080. Cả hai đều lớn hơn 1024, nên frps không bao giờ cần root và cũng không cần CAP_NET_BIND_SERVICE. Vì vậy, không nên đặt vhost trên cổng 80 rồi để nginx xử lý cổng đó.
Tạo /etc/systemd/system/frps.service:
[Unit]
Description=frp reverse tunnel server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=frp
Group=frp
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s
LimitNOFILE=65535
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
[Install]
WantedBy=multi-user.target# on the VPS
sudo systemctl daemon-reload
sudo systemctl enable --now frps
sudo journalctl -u frps -n 20 --no-pagerLog phải hiển thị cả hai listener. Địa chỉ quan trọng hơn số cổng:
frps tcp listen on 0.0.0.0:7000
http service listen on 127.0.0.1:8080ProtectSystem=strict khiến toàn bộ filesystem ở chế độ chỉ đọc đối với service này. frps vẫn hoạt động vì mặc định log được ghi vào standard output và journald sẽ thu thập log đó. Nếu đặt log.to thành đường dẫn file, service sẽ không thể ghi file cho đến khi bạn thêm một dòng ReadWritePaths= tương ứng. Vì vậy, hãy giữ nguyên giá trị mặc định.
Firewall: mở một cổng, không mở cả dải cổng
# on the VPS
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 7000/tcp
sudo ufw enable
sudo ufw status numberedBốn rule, trong đó một rule chỉ tồn tại để gia hạn chứng chỉ. 22 là SSH. 80 chuyển hướng sang 443 và trả lời thử thách ACME (môi trường quản lý chứng chỉ tự động). 443 phục vụ mọi app được tunnel. 7000 là cổng control của frp và là cổng duy nhất mà client cần truy cập.
Những hướng dẫn yêu cầu mở một dải cổng như sudo ufw allow 20000:30000/tcp đang mô tả thiết kế khác, trong đó mỗi service sử dụng một cổng TCP public riêng. Ở đây bạn không cần làm vậy, vì mọi kết nối đều đến 443 và frp định tuyến theo hostname. Nếu sau này bạn thực sự cần một cổng TCP public, hãy đưa proxyBindAddr về 0.0.0.0 và thêm giới hạn để client chỉ có thể sử dụng những cổng bạn chỉ định:
allowPorts = [
{ start = 20000, end = 20010 }
]
maxPortsPerClient = 5Hầu hết provider cũng chạy một network firewall trong control panel, tách biệt với ufw trên máy chủ. Một rule trông đúng trong sudo ufw status nhưng kết nối vẫn timeout thường bị chặn ở đó. Các rule ufw mà VPS thực sự cần hướng dẫn thiết lập default-deny mà phần này giả định.
Kết thúc HTTPS trên VPS bằng certificate hợp lệ
Trỏ bản ghi A cho home.example.com đến public IP của VPS. Không trỏ đến nhà bạn. Nhà bạn không có địa chỉ để trỏ đến, và đó chính là vấn đề bạn đang giải quyết.
# on the VPS
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginxTrước tiên, tạo /etc/nginx/sites-available/home.example.com với một block port 80 đơn giản để certbot có server_name tương ứng mà sử dụng:
server {
listen 80;
listen [::]:80;
server_name home.example.com;
location / { return 404; }
}# on the VPS
sudo ln -s /etc/nginx/sites-available/home.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certonly --nginx -d home.example.comnginx -t in nginx: configuration file /etc/nginx/nginx.conf test is successful khi các file được parse thành công. Chạy lệnh này trước mỗi lần reload. Khi reload thất bại, nginx vẫn giữ cấu hình cũ đang chạy, nên một lần chỉnh sửa bị lỗi sẽ trông giống như không có gì thay đổi.
WebSocket upgrade cần một map ở cấp http. Đặt map này trong /etc/nginx/conf.d/upgrade.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Bây giờ thay site file bằng cấu hình thực tế:
server {
listen 80;
listen [::]:80;
server_name home.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name home.example.com;
ssl_certificate /etc/letsencrypt/live/home.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/home.example.com/privkey.pem;
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}proxy_set_header Host $host; là bắt buộc trong trường hợp này. frp định tuyến HTTP vhost dựa trên header Host và đối chiếu header đó với danh sách customDomains trong cấu hình client. Nếu thiếu header này, nginx gửi Host: 127.0.0.1, frp không tìm thấy proxy cho hostname đó, và visitor nhận 404 trống từ frp thay vì trang của app. Giải thích từng dòng trong một block reverse proxy của nginx trình bày chức năng của các header còn lại.
# on the VPS
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-runDry run xác nhận việc gia hạn sẽ hoạt động sau 90 ngày, khi bạn không theo dõi hệ thống. Lệnh này cần port 80 có thể truy cập được, nên mới cần rule ufw đó.
Phía mạng nhà: frpc dưới dạng service không có quyền đặc biệt
Cài đặt frpc trên máy ở nhà đúng như cách bạn đã cài frps, dùng cùng version và thực hiện cùng bước kiểm tra checksum. Sau đó tạo user frp và thư mục /etc/frp giống như trước. Ghi nội dung sau vào /etc/frp/frpc.toml:
serverAddr = "vps.example.com"
serverPort = 7000
auth.method = "token"
auth.token = "PASTE_THE_SAME_TOKEN_HERE"
transport.tls.enable = true
loginFailExit = false
proxies = [
{ name = "home-app", type = "http", localIP = "127.0.0.1", localPort = 3000, customDomains = ["home.example.com"] }
]Thứ tự các key trong file này rất quan trọng, không chỉ vì lý do định dạng. TOML gán mọi key sau một table header vào table đó. Vì vậy, một thiết lập cấp cao nhất như serverAddr nếu được ghi bên dưới table header của proxy sẽ âm thầm trở thành thiết lập của proxy và bị frp bỏ qua. Việc viết danh sách proxy dưới dạng inline array như bên trên sẽ tránh được lỗi này: mọi key cấp cao nhất vẫn rõ ràng thuộc cấp cao nhất.
type = "http" định tuyến proxy này qua vhost listener thay vì chiếm một public TCP port riêng. Vì vậy firewall vẫn chỉ cần 4 rule. customDomains phải chứa hostname mà nginx chuyển tiếp trong header Host. Do đó, giá trị này là home.example.com, không bao giờ là địa chỉ IP của VPS.
loginFailExit = false quan trọng hơn bạn tưởng. Giá trị mặc định là true, khiến frpc thoát nếu lần đăng nhập đầu tiên thất bại. Trên máy ở nhà, quá trình boot có thể hoàn tất trước khi đường truyền ISP hoạt động. Khi đó service sẽ chết cho đến khi bạn tình cờ phát hiện ra. Đặt thành false để frpc tiếp tục retry cho đến khi VPS phản hồi.
Ghi nội dung sau vào /etc/systemd/system/frpc.service:
[Unit]
Description=frp reverse tunnel client
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=frp
Group=frp
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=always
RestartSec=10s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target# on the home box
sudo chown root:frp /etc/frp/frpc.toml
sudo chmod 640 /etc/frp/frpc.toml
sudo -u frp frpc verify -c /etc/frp/frpc.toml
sudo systemctl daemon-reload
sudo systemctl enable --now frpc
sudo journalctl -u frpc -n 20 --no-pagerClient đã kết nối sẽ ghi một run ID:
login to server success, get run id [3a1f9c2b7d4e5f60]Mở https://home.example.com trong trình duyệt. Bạn sẽ thấy app đang chạy tại 127.0.0.1:3000 ở nhà. Restart=always trên client là có chủ ý: kết nối về nhà có thể bị ngắt, và service phải tự hoạt động lại mà không cần bạn can thiệp.
Không expose dashboard trên interface public
Với webServer.addr = "127.0.0.1", dashboard chỉ phản hồi trên chính VPS. Truy cập dashboard từ laptop bằng local forward thay vì mở một cổng:
# on your laptop
ssh -N -L 7500:127.0.0.1:7500 you@vps.example.comMở http://127.0.0.1:7500 rồi đăng nhập bằng webServer.user và webServer.password lấy từ frps.toml. Trang này liệt kê mọi client đang kết nối và bộ đếm traffic của từng proxy. Đây là cách nhanh nhất để kiểm tra “home box hiện có đang kết nối không”. Đóng phiên SSH thì dashboard lại không thể truy cập.
Tunnel không làm được gì
Đọc phần này 2 lần, vì đây là nơi dễ xảy ra sự cố nghiêm trọng. Tunnel giúp một service riêng tư có thể truy cập từ public Internet. Nó không xác thực những người truy cập service đó. Khi https://home.example.com phân giải được, các scanner sẽ tìm thấy nó trong vài ngày, dù bạn có cho ai biết hostname hay không. Certificate Transparency log công khai mọi hostname mà bạn cấp certificate, nên hostname trở thành thông tin công khai ngay khi certbot cấp certificate thành công.
Mọi thứ bạn expose phải tự có cơ chế xác thực. Nếu app có login thực sự kèm rate limiting thì tốt. Nếu login chỉ dùng chung một password, hoặc hoàn toàn không có login, hãy đặt một authenticating proxy phía trước app trên VPS. Đặt oauth2-proxy phía trước app là cách thường dùng. Proxy này nằm giữa nginx và frp vhost mà không cần thay đổi hai đầu của tunnel.
Token trong frps.toml chỉ bảo vệ tunnel, không bảo vệ các app. Nó ngăn người lạ đăng ký proxy riêng của họ trên VPS của bạn. Token không xử lý các request đến cổng 443 cho hostname mà bạn đã chủ động công khai.
Có 2 thói quen nên duy trì. Hãy rotate token bằng cách sửa cả 2 file rồi restart cả 2 service, vì token không tự hết hạn. Đồng thời luôn cập nhật frp. Binary này là front door public của bạn. Ghi chú phát hành v0.71.0 liệt kê một lỗi server panic có thể bị kích hoạt bởi giá trị không hợp lệ do client gửi lên. Đây là loại bug cần được patch, không phải loại bug nên tự suy đoán nguyên nhân.
Các chế độ lỗi và chuỗi bạn sẽ thấy
Client không bao giờ kết nối được. journalctl -u frpc lặp lại connect to server error:, sau đó xảy ra timeout khi dial. Không có gì truy cập được cổng 7000. Kiểm tra ufw trên VPS, sau đó kiểm tra network firewall của nhà cung cấp trong control panel, rồi xác nhận tên phân giải đúng bằng getent hosts vps.example.com.
Token không đúng. Client thông báo rõ lỗi này:
login to the server failed: token in login doesn't match token from configurationCopy lại token. Newline ở cuối chuỗi hoặc $ trong một chuỗi shell không được đặt trong dấu nháy và đã được expand thành rỗng là nguyên nhân của gần như tất cả các lỗi này. Vì vậy, giá trị openssl rand -base64 32 trong file TOML phải nằm trong dấu nháy.
Tunnel đang hoạt động nhưng browser nhận 404 trống. frpc đã log đăng nhập thành công và dashboard hiển thị proxy, nhưng trang trả về 404 mà không có style của app. Điều đó có nghĩa frp không có proxy cho header Host này. Test vhost trực tiếp trên VPS, bỏ qua cả nginx và TLS:
# on the VPS
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: home.example.com' http://127.0.0.1:8080/Nếu lệnh đó trả về 404 thì customDomains bị sai. Mã phản hồi khác có nghĩa request chưa nhận đúng Host từ nginx.
nginx trả về 502. nginx đang phản hồi còn frp thì không. sudo ss -lntp | grep 8080 trên VPS phải hiển thị frps đang listen trên 127.0.0.1:8080. Output rỗng có nghĩa frps đã dừng hoặc vhostHTTPPort chưa được đặt trong frps.toml.
App cho rằng mọi visitor đều là local. Log của app ghi 127.0.0.1 cho mọi request. frp đặt X-Forwarded-For và nginx nối thêm vào đó, nên địa chỉ client thật nằm trong header này. Cấu hình app để trust header đó. Không được bỏ qua bước này nếu app rate limit theo IP address, vì hiện tại mọi visitor trên Internet đang dùng chung một bucket.
Request dài bị ngắt sau 60 giây. Upload hoặc response dạng streaming dừng giữa chừng. Đây là proxy_read_timeout mặc định của nginx, không phải lỗi của tunnel. Block ở trên tăng giá trị này lên 3600s. client_max_body_size là giới hạn tương ứng cho kích thước upload; giá trị mặc định 1 MB sẽ từ chối body lớn hơn bằng lỗi 413.
Mọi thứ hoạt động, rồi dừng sau khi router reboot. Restart=always trong unit frpc cùng với loginFailExit = false xử lý trường hợp này. Xác nhận bằng sudo systemctl is-enabled frpc; lệnh này phải in ra enabled.
FAQ
Làm thế nào biết tôi có đang ở sau CGNAT không?
So sánh địa chỉ WAN trên trang quản trị của router với kết quả curl -4 -s https://ifconfig.me từ bên trong cùng mạng. Nếu hai địa chỉ khác nhau và địa chỉ WAN của router nằm trong 100.64.0.0/10, ISP của bạn đang chạy NAT cấp nhà mạng. Dải này là không gian địa chỉ dùng chung theo RFC 6598 và được dành cho mục đích đó. Một số ISP dùng 10.0.0.0/8 ở phía WAN, với cùng ý nghĩa. Nếu hai địa chỉ trùng nhau, bạn có public IP: forward port là xong.
Tôi có cần domain name cho reverse tunnel không?
Có, với cấu hình HTTPS được mô tả ở đây. Certificate được cấp cho một hostname, còn HTTP vhost của frp định tuyến request theo header Host, nên hai đầu phải thống nhất một tên. Raw TCP proxy trên một port được đánh số có thể hoạt động với bare IP của VPS mà không cần domain, nhưng khi đó bạn không có certificate và hostname routing, nên một public port chỉ phục vụ chính xác một service.
Chạy frp trên public VPS có an toàn không?
An toàn nếu chỉ expose control port và port này có authentication. Đặt auth.token thành một giá trị ngẫu nhiên trên cả hai đầu và đặt transport.tls.force = true trên server. Sau đó đặt proxyBindAddr = "127.0.0.1" để không có port nào frp mở cho proxy hướng ra internet, đồng thời giữ dashboard trên webServer.addr = "127.0.0.1" và truy cập qua SSH local forward. Cập nhật binary khi có release mới, vì đây là process đang listen trên public address của bạn.
Tại sao không ai truy cập được port được forward bằng ssh -R?
sshd mặc định đi kèm GatewayPorts no, nên remote forward chỉ bind vào loopback interface của VPS. curl chạy ngay trên VPS sẽ hoạt động, còn curl từ nơi khác sẽ timeout. Cách sửa đúng là giữ forward trên loopback và đặt nginx trên port 443 ở phía trước. Đặt GatewayPorts yes sẽ publish một raw port không có certificate và không có TLS, khiến vấn đề còn tệ hơn.
Tôi nên dùng frp hay mesh VPN như Tailscale hoặc WireGuard?
Dùng mesh VPN khi chỉ các thiết bị của bạn cần truy cập, vì khi đó không có gì được publish và không có public hostname để người khác scan. Dùng frp khi bạn cần một public HTTPS address mà mọi browser đều có thể load, chẳng hạn webhook receiver hoặc một trang muốn chia sẻ cho những người không cài VPN client. Hai giải pháp này có thể cùng chạy trên một VPS, dùng các port khác nhau và đảm nhiệm các mục đích khác nhau.