HanyanOS 部署手记
1. The Problem
HanyanOS runs seven distinct services across two physical locations: a Singapore Lightsail VPS (front-end) and a Brisbane N100 home server (back-end). Each service needs its own subdomain with HTTPS termination — but AWS Lightsail instance pricing limits us to a single public IPv4 address. Running seven separate ports on the public internet would be unmanageable, insecure, and unfriendly to clients behind corporate firewalls.
The constraint was absolute: one public port (443) must intelligently route all seven domains to their respective backends, supporting both TLS-terminated web services and a non-TLS VPN protocol on the same entry point.
2. Architecture Overview
The solution uses a two-layer Nginx architecture:
1 | Internet :443 |
Layer 1 — SNI Routing: Nginx’s stream module with ssl_preread on port 443 inspects the TLS ClientHello SNI field without terminating the TLS connection. It then proxies the raw TCP stream to the appropriate upstream based on the domain name.
Layer 2 — TLS Termination: All web services land on port 8443 where a second Nginx http block terminates TLS, applies headers, and reverse-proxies to local ports (which connect through FRP or SSH tunnels to the N100 back-end).
3. Configuration Breakdown
3.1 Stream SNI Router (stream.conf)
1 | stream { |
Key design decisions:
ssl_preread onreads only the SNI field, not the full handshake — minimal overhead.- The
mapblock evaluates the SNI and selects the upstream; thedefaultcatch-all routes unknown domains to Xray for cloaking. - Six web domains share one upstream (
web_backend→:8443); the VPN domain has a separate path.
3.2 HTTPS Virtual Hosts (blog-ssl.conf)
1 | # WordPress via FRP tunnel |
Each virtual server on port 8443 uses the same wildcard SSL certificate (/etc/ssl/chenyun/fullchain.cer) issued by Let’s Encrypt for *.chenyun.org. The static sites (www, root domain) serve files directly from the VPS filesystem, while dynamic services proxy through FRP tunnels to the N100.
3.3 AI/API Gateway (ai.conf, api.conf)
1 | server { |
Both ai.chenyun.org and api.chenyun.org proxy through port 7445 — an FRP tunnel to the N100’s AI/API service on port 8090. Separate server_name directives allow future routing differentiation.
4. Tunnel Integration
The FRP tunnel architecture connects the VPS front-end to the N100 back-end:
| VPS Port | FRP Tunnel | N100 Port | Service |
|---|---|---|---|
| 7444 | FRP | 8081 | WordPress |
| 7445 | FRP | 8090 | AI/API Services |
| 8091 | SSH -R | 8091 | SnappyMail |
The FRP migration from SSH tunnels (completed 2026-05-12) significantly improved reliability — FRP provides automatic reconnection, health checks, and load balancing, whereas SSH tunnels would silently die under network instability.
5. Challenges & Lessons
Challenge 1: Stream SNI Breaks on Non-TLS Traffic
The ssl_preread module expects TLS on port 443. HTTP requests (port 80) are handled separately with a simple redirect. We considered enforcing HTTPS-only but kept port 80 for backward compatibility with legacy clients.
Challenge 2: SSL Certificate Management
A single wildcard certificate simplifies management but creates a single point of failure. If the Let’s Encrypt renewal fails, all domains go down. The fix was a cron-driven acme.sh renewal with Route53 DNS challenge, verified daily.
Challenge 3: Debugging SNI Routing
Troubleshooting the stream layer is tricky — Nginx doesn’t log SNI routing decisions unless explicitly configured. We added:
1 | log_format stream_log '$remote_addr [$time_local] $protocol $ssl_preread_server_name $upstream_addr'; |
This was invaluable during the initial deployment when we discovered that some mail clients sent SNI with trailing dots (FQDN format), causing map mismatches.
Challenge 4: Xray REALITY and Non-TLS Protocols
Xray’s REALITY protocol uses TLS-like handshakes but isn’t standard HTTPS. The ssl_preread module correctly reads the SNI, but the proxied connection must preserve the raw bytes. Using the stream module (not http) for the Xray backend was critical — any attempt to terminate TLS would break the Xray protocol.
6. Security Considerations
- UFW lockdown: Only ports 22, 80, 443, 25, 465, 587, 993, 8443, 7443 are open on the public interface.
- TLS version restriction:
TLSv1.2 TLSv1.3only — no legacy protocol support. - Cipher restriction:
HIGH:!aNULL:!MD5— modern ciphers only, no anonymous or MD5-based suites. - Default catch-all: Unknown SNI domains route to Xray, providing cloaking and preventing information leakage about the domain list.
- Rate limiting: Configured via
conf.d/rate-limit.confto prevent brute-force and DDoS.
7. Performance
Under typical load, the stream SNI router adds approximately 0.3–0.8ms of latency per connection (the time to read the 5-byte TLS record header and SNI extension). The VPS (Singapore Lightsail, 2 vCPU, 2GB RAM) handles all seven domains without measurable CPU impact. During a stress test with 500 concurrent connections across all domains, CPU stayed below 15%.
8. Summary
This two-layer Nginx architecture — stream SNI routing on port 443, then TLS termination on port 8443 — solves the one-public-IP problem elegantly. The design is extensible: adding a new subdomain requires one line in the stream map block and one server block on port 8443. Future plans include migrating mail.chenyun.org from SSH tunnel to FRP, adding HSTS preload headers, and evaluating HTTP/3 (QUIC) support via a separate UDP listener.
TL;DR: One IP, one port (443), seven domains, two layers of Nginx, three types of tunnels — and everything just works.