HanyanOS 部署手记

1. The Problem

HanyanOS runs on a dual-node topology. The Singapore Lightsail VPS (52.220.247.252) serves as the public edge, while the Brisbane N100 home server (192.168.1.18) hosts all real services — but it lives behind a carrier-grade NAT with no public IP and no possibility of port forwarding. The N100 is completely invisible to the internet.

The naive approach — SSH reverse tunnels — works for a handful of ports but becomes brittle and unscalable beyond two or three services. With seven subdomains, a mail system, and a growing agent ecosystem, we needed a dedicated, encrypted, production-grade tunnel. Enter FRP (Fast Reverse Proxy).

2. Architecture Overview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────┐     ┌─────────────────────────────────┐
│ VPS Singapore (Lightsail) │ │ N100 Brisbane (Home) │
│ Public: 52.220.247.252 │ │ LAN: 192.168.1.18 (NAT'd) │
│ │ │ │
│ FRPS (Server) │◄────┤ FRPC (Client) │
│ :7443 ──── control channel ────┤ │ Connects outbound to VPS:7443 │
│ :7444 → maps to ───────────────┤────►│ :80 (Nginx local) │
│ :7446 → maps to ───────────────┤────►│ :25 (SMTP) │
│ :7447 → maps to ───────────────┤────►│ :465 (SMTP Submission) │
│ :7448 → maps to ───────────────┤────►│ :993 (IMAPS) │
│ :7449 → maps to ───────────────┤────►│ :8091 (SnappyMail) │
│ :7450 → maps to ───────────────┤────►│ :5678 (n8n Webhook) │
│ │ │ │
│ Backup: SSH Reverse Tunnel │ │ Backup: SSH -R │
│ :2222 → N100:22 │ │ :8090 → N100:8090 │
└─────────────────────────────────┘ └─────────────────────────────────┘

Data Flow (Example: HTTPS Blog Request)

1
2
3
4
5
Client → blog.chenyun.org → DNS A → VPS:443
→ Nginx stream SNI → VPS:8443
→ Nginx HTTPS proxy_pass → 127.0.0.1:7444
→ FRPS forwards through tunnel → N100 FRPC
→ 127.0.0.1:80 → N100 Nginx → Hexo static files

Every request traverses two reverse proxy layers (VPS Nginx + N100 Nginx) and one encrypted FRP tunnel — yet latency remains sub-50ms for most requests.

3. Configuration

FRPS (VPS Server)

1
2
3
4
5
# /etc/frp/frps.ini
[common]
bind_port = 7443
token = chenyun-frp-2026
tcp_mux = true

Minimal configuration. The server listens on port 7443 for control connections and uses a shared token for authentication. tcp_mux = true multiplexes multiple service streams over a single TCP connection, reducing overhead.

FRPC (N100 Client)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# /etc/frp/frpc.ini
[common]
server_addr = 52.220.247.252
server_port = 7443
token = chenyun-frp-2026
tcp_mux = true
login_fail_exit = false

[wordpress]
type = tcp
local_ip = 127.0.0.1
local_port = 8081
remote_port = 7444

[ai-api]
type = tcp
local_ip = 127.0.0.1
local_port = 8090
remote_port = 7445

The client initiates outbound connections from the NAT’d N100 to the VPS, establishing persistent tunnels. login_fail_exit = false ensures the client retries on connection loss rather than crashing.

Systemd Services

Both sides run as systemd services with auto-restart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/systemd/system/frpc.service
[Unit]
Description=FRP Client
After=network.target
Requires=network-online.target

[Service]
Type=simple
Restart=always
RestartSec=5
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.ini

[Install]
WantedBy=multi-user.target

4. Complete Port Mapping Table

VPS FRP Port Direction N100 Port Service
7443 Control channel (bidirectional)
7444 :80 Web services (HTTP, all subdomains)
7446 :25 SMTP inbound
7447 :465 SMTP Submission (TLS)
7448 :993 IMAPS
7449 :8091 SnappyMail webmail
7450 :5678 n8n webhook receiver

Note port 7445 is reserved for the AI API direct access. The gap between 7444 and 7446 is intentional — port 7445 was originally for the API tunnel but later consolidated through the web proxy layer.

5. Security Considerations

FRP alone is not enough. The tunnel is protected at multiple layers:

  1. Token Authentication: The shared token (chenyun-frp-2026) prevents unauthorized clients from joining the tunnel. In production, this should be rotated periodically.

  2. UFW Firewall (VPS): Only ports 22, 80, 443, 25, 465, 587, 993, 8443, and 7443 are open. The FRP data ports (7444–7450) are bound to 127.0.0.1 on the VPS side — they are not exposed to the public internet. Only Nginx on the VPS can reach them.

  3. N100 Isolation: The home server initiates all outbound connections. No inbound ports are open on the N100’s router. Even if an attacker compromises the VPS, they cannot directly reach the N100 without also compromising the FRP token.

  4. Fail2ban: Rate-limiting on SSHD and Nginx auth endpoints provides a second layer against brute-force attacks on any exposed management interfaces.

1
2
3
4
5
6
7
Internet → [UFW] → VPS:443/25/465/993 only

Nginx (127.0.0.1 only)

FRP tunnel (token auth)

N100 (invisible)

6. Problems Encountered & Solutions

Problem 1: Port Mapping Drift

Early on, FRP ports were changed multiple times as services were added and removed. The Nginx stream and proxy configurations were not always updated in sync, causing mysterious routing failures.

Solution: A version-controlled port mapping table was created in HanyanOS/memory/infrastructure/network/README.md. Any change to FRP configuration now requires a corresponding update to Nginx configs and a documented test of all affected subdomains.

Problem 2: Connection Drops Under Low Traffic

FRP’s default keepalive settings caused tunnel disconnections during idle periods (e.g., 3 AM when no one accesses the blog).

Solution: Added tcp_mux = true and systemd Restart=always with RestartSec=5. The client reconnects automatically within seconds of any disruption. With login_fail_exit = false, transient network issues on the N100’s ISP connection are handled gracefully.

Problem 3: Initial Port Binding Conflict

Port 7444 was initially used for both the AI API and WordPress before the routing was separated. Both services tried to bind to the same FRP remote port.

Solution: Separated services onto dedicated ports. The web traffic consolidation (all HTTP services through a single Nginx instance on N100:80) reduced the port count from 7+ to 3 main data ports plus the control channel.

Problem 4: Debugging Tunnel Connectivity

When a service goes down, it’s not immediately obvious whether the issue is at the FRP tunnel level or the service level.

Solution: Added ICMP monitoring via the HanyanOS patrol cron job, which checks FRPC process status and connection state every 15 minutes. Logs are written to structured error files for analysis.

7. Comparison: FRP vs SSH Reverse Tunnel

Aspect FRP SSH Reverse Tunnel
Connection persistence Excellent (auto-reconnect) Good (autossh needed)
Multi-port support Native (any number of proxies) Manual (-R per port)
Encryption TCP-level (optional TLS) SSH channel encryption
Resource usage ~15MB per binary SSH daemon overhead
Configuration Declarative INI file CLI flags
TCP Multiplexing Built-in (tcp_mux) Not natively supported

FRP was chosen over SSH tunnels for its superior multi-port management and declarative configuration. Two SSH reverse tunnels are retained as a backup control plane (:2222 → :22 for SSH access, :8090 → :8090 for API emergency access), but all production traffic flows through FRP.

8. Conclusion

FRP provides the backbone of HanyanOS’s dual-node architecture, securely bridging the public VPS edge with the private N100 compute cluster. The setup handles six production services plus a control channel on a single outbound connection, with automatic reconnection, TCP multiplexing, and a security posture that keeps the home server completely invisible to the internet.

For any homelab running behind NAT with multiple services to expose, FRP is a battle-tested, lightweight, and reliable choice — especially when paired with a hardened edge VPS.


Next in series: Stalwart Mail System — Building a Self-Hosted Email Server on N100