feat: 合并边缘节点运维并完善 Surge 配置安全
迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 3x-ui database schema notes
|
||||
|
||||
Read this before writing a script that adds a new protocol or inbound by hand.
|
||||
Everything here was confirmed by reading the live sqlite DB (`/etc/x-ui/x-ui.db`)
|
||||
and the generated runtime config (`/usr/local/x-ui/bin/config.json`) on a real
|
||||
box — not from 3x-ui's own documentation, which doesn't cover this.
|
||||
|
||||
## Why DB-direct instead of the panel API
|
||||
|
||||
The panel HTTP API 403s unauthenticated/scripted requests on the builds this
|
||||
skill has been tested against (security middleware). Every inbound this skill
|
||||
adds is written straight into the sqlite DB, followed by `systemctl restart
|
||||
x-ui` to pick up the change. `08-xui-bootstrap.py` is the reference
|
||||
implementation — read it before writing a new one-off script.
|
||||
|
||||
## Relevant tables
|
||||
|
||||
## The single-client model (why one `node-main` owns all six inbounds)
|
||||
|
||||
This skill deliberately creates **one** client, `node-main`, linked to all six
|
||||
inbounds — not one client per protocol. Because the `clients` row is
|
||||
protocol-generic (it carries both a `uuid` and a `password`), that single row
|
||||
authenticates VMess (by its `uuid`) *and* Trojan+Shadowsocks (by its
|
||||
`password`) depending on which inbound the traffic hits. The payoff is entirely
|
||||
in the panel/subscription UX: the Clients page shows one row, its Info dialog
|
||||
lists all six share links + QR codes, and its one subscription (by `sub_id`)
|
||||
carries all six nodes. Trojan and Shadowsocks therefore **share one password**
|
||||
(`NODE_PASSWORD`) — intended for a single-user node.
|
||||
|
||||
**`inbounds`** — one row per inbound (listener). Columns that matter:
|
||||
`user_id` (**set to 1** — a NULL/empty user_id makes the inbound invisible in
|
||||
the panel even though it works; see pitfalls.md), `port`, `protocol`
|
||||
(`vmess`/`trojan`/`shadowsocks`/...), `listen` (`127.0.0.1` here — nginx does
|
||||
the public-facing TLS), `settings` (protocol-specific JSON, see below),
|
||||
`stream_settings` (transport JSON — this skill uses
|
||||
`{"network":"ws","security":"none","wsSettings":{...},"externalProxy":[{...}]}`;
|
||||
`security` is `"none"` because nginx already terminated TLS upstream, and the
|
||||
`externalProxy` entry makes generated share links point at `<cover>:443` over
|
||||
TLS instead of the useless internal `127.0.0.1:<port>` — see pitfalls.md),
|
||||
`tag` (used to target this inbound in routing rules).
|
||||
|
||||
**`clients`** — a *protocol-generic* credential table, not one table per
|
||||
protocol. Relevant columns: `email` (must be unique — this skill uses ONE email,
|
||||
`node-main`, for all six inbounds), `uuid` (used by vmess/vless), `password`
|
||||
(used by trojan/shadowsocks), `sub_id` (groups this client's inbounds into one
|
||||
subscription), `security`/`flow` (vmess/vless-only). There is no column that
|
||||
says which protocol a client belongs to — that's implied entirely by which
|
||||
inbound(s) it's linked to via `client_inbounds`, and 3x-ui's backend picks
|
||||
`uuid` vs `password` based on *that inbound's* `protocol` field when it builds
|
||||
the live xray config. So node-main sets both `uuid` and `password`.
|
||||
|
||||
**`client_inbounds`** — links a client to an inbound: `(client_id, inbound_id,
|
||||
flow_override, created_at)`. One row per (client, inbound) pair — node-main gets
|
||||
six rows (one per inbound).
|
||||
|
||||
**`client_traffics`** — traffic-stat tracking, **one row per client `email`,
|
||||
not per inbound** (email is globally unique in this table). Insert exactly one
|
||||
row for `node-main`, pointing `inbound_id` at any one of its linked inbounds
|
||||
(which one doesn't matter — it's just where the panel attributes the stats row).
|
||||
|
||||
**`settings`** (key/value store) — the `xrayTemplateConfig` key holds the JSON
|
||||
that 3x-ui actually feeds to xray-core (merged with the DB-derived inbounds at
|
||||
runtime). This skill's WARP outbound + routing rule live here. **Read-modify-write
|
||||
this, never overwrite it wholesale** — the WARP routing rule's `inboundTag`
|
||||
array needs the new protocol's warp-inbound tag *appended*, not replaced,
|
||||
or a previous protocol's warp routing silently stops working.
|
||||
|
||||
## `settings` JSON shape per protocol (in the `inbounds.settings` column)
|
||||
|
||||
```jsonc
|
||||
// vmess
|
||||
{"clients": [{"id": "<uuid>", "email": "node-main", "enable": true, "subId": "<sub-id>", "alterId": 0}], "disableInsecureEncryption": false}
|
||||
// trojan
|
||||
{"clients": [{"password": "<NODE_PASSWORD>", "email": "node-main", "enable": true, "subId": "<sub-id>"}], "fallbacks": []}
|
||||
// shadowsocks — note the TOP-LEVEL "password" (prevents the v3.5.0 link-gen panic, see pitfalls.md)
|
||||
{"clients": [{"password": "<NODE_PASSWORD>", "method": "chacha20-poly1305", "email": "node-main", "enable": true, "subId": "<sub-id>"}], "method": "chacha20-poly1305", "password": "<NODE_PASSWORD>", "network": "tcp,udp"}
|
||||
```
|
||||
|
||||
Keep the client embedded in `settings.clients` on BOTH schema generations.
|
||||
The normalized `clients` and `client_inbounds` tables are derived projections;
|
||||
upgrade/import migration rebuilds them from this JSON. An empty array may
|
||||
appear to work until migration removes the projected credentials. The migrated
|
||||
`08-xui-bootstrap.py` writes both representations; existing table-only nodes
|
||||
are repaired by `08b-embed-settings-clients.py` after a database backup.
|
||||
Use the full client shape in those scripts, including explicit `enable: true`
|
||||
and consistent subscription/quota fields across all six inbounds. Verify the
|
||||
generated `/usr/local/x-ui/bin/config.json` after restarting. Minimal protocol
|
||||
credential fields (the common fields above are also required):
|
||||
|
||||
```jsonc
|
||||
// vmess client — all three use the SAME email "node-main" (one client)
|
||||
{"id": "<uuid>", "email": "node-main", "alterId": 0}
|
||||
// trojan client
|
||||
{"password": "<NODE_PASSWORD>", "email": "node-main"}
|
||||
// shadowsocks client
|
||||
{"password": "<NODE_PASSWORD>", "method": "chacha20-poly1305", "email": "node-main"}
|
||||
```
|
||||
|
||||
## Adding a fourth protocol later (e.g. VLESS+REALITY)
|
||||
|
||||
1. Add its ports/paths/credential to `config.env` (follow the `PORT_<PROTO>_{DIRECT,WARP}` /
|
||||
`WS_PATH_<PROTO>_{DIRECT,WARP}` naming already used for vmess/trojan/ss).
|
||||
2. Add a `(protocol, port_direct, port_warp, path_direct, path_warp)` tuple to
|
||||
the `PROTOCOLS` list in `08-xui-bootstrap.py`, and its client-shape branch
|
||||
to `inbound_settings()` (REALITY additionally needs `realitySettings` in
|
||||
`stream_settings` with a real "borrowed" target site — this is per-inbound
|
||||
TLS-mimicry config the outer nginx doesn't touch, so `security` in
|
||||
`stream_settings` would need to be `"reality"` for that inbound specifically
|
||||
instead of `"none"`, and you'd route it directly rather than through the
|
||||
nginx WS+TLS cover this skill uses for the other three, since REALITY does
|
||||
its own TLS handshake and can't be terminated by an upstream nginx first).
|
||||
3. Add the new WS paths to `PROXIES` in `09-aapanel-sites.py` (skip this for
|
||||
REALITY specifically, since it bypasses nginx as noted above and would need
|
||||
its own firewall port opened instead).
|
||||
4. Add the new protocol's outbound block to the smoke-test client generator in
|
||||
`12-smoke-test.sh`.
|
||||
5. Re-run `08-xui-bootstrap.sh` (idempotent — existing inbounds are skipped by
|
||||
port) then `09-aapanel-sites.sh`, then `12-smoke-test.sh`. `08-xui-bootstrap.py`
|
||||
already sets `user_id=1`, adds the `externalProxy` link hint, links the new
|
||||
inbounds to the existing `node-main` client, and (for shadowsocks-family
|
||||
protocols) writes the top-level `password` — so a new protocol inherits all
|
||||
version-compat fixes as long as you extend `PROTOCOLS` and its client-shape
|
||||
logic rather than hand-inserting rows. Keep `settings.clients` populated
|
||||
for each added protocol as well as maintaining the projection tables.
|
||||
|
||||
## Subscription (optional, not enabled by this skill)
|
||||
|
||||
3x-ui's subscription server (`/sub/<sub_id>`) returns every share link for a
|
||||
client's `sub_id` — since node-main's `sub_id` covers all six inbounds, one
|
||||
subscription URL delivers all six nodes and stays in sync as you add/remove
|
||||
inbounds. It listens on its own port (2096 by default, HTTP) and is **off by
|
||||
default** in this skill. Two gotchas if you turn it on:
|
||||
- The sub port is **not** opened by `10-ufw.sh` (only 443 + panel ports are).
|
||||
Don't just `ufw allow 2096` — that exposes a plaintext-HTTP endpoint carrying
|
||||
every node's credentials on a guessable `IP:port`. Instead reverse-proxy a
|
||||
non-obvious subdomain (e.g. `sub.<root>`) → `127.0.0.1:2096` through nginx on
|
||||
443 with the wildcard cert, same pattern as the panels, so the subscription
|
||||
is HTTPS and hidden behind 443.
|
||||
- "Empty reply from server" on the sub port is almost always the firewall
|
||||
(connection reached UFW's default drop), not 3x-ui — verify by curling
|
||||
`http://127.0.0.1:2096/sub/<id>` **on the box** first (200 there = the
|
||||
service and links are fine, the problem is purely how it's exposed).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Centralized wildcard certs (fleet option)
|
||||
|
||||
`03-sign-wildcard-cert.sh` signs the wildcard **on the box** — simplest for a single node.
|
||||
For a fleet you may prefer to sign every wildcard on **one** host and push to the rest, so
|
||||
the Cloudflare DNS token lives in exactly one place and renewals are centralized.
|
||||
|
||||
## Model
|
||||
|
||||
```
|
||||
one signer host (acme.sh + CF token)
|
||||
┌───────────────────────────────────┐
|
||||
│ acme.sh --dns dns_cf │ issues *.nodeA.example.com,
|
||||
│ --reloadcmd distribute.sh │ *.nodeB.example.com, …
|
||||
└───────────────┬───────────────────┘
|
||||
│ scp fullchain+key on every renew
|
||||
┌─────────────┼─────────────┐
|
||||
nodeA nodeB nodeC (BT cert dirs + nginx reload)
|
||||
```
|
||||
|
||||
## Signer setup (once)
|
||||
|
||||
```bash
|
||||
export CF_Token='<token>'
|
||||
acme.sh --set-default-ca --server letsencrypt
|
||||
acme.sh --issue --dns dns_cf -d nodeA.example.com -d '*.nodeA.example.com' --keylength ec-256
|
||||
```
|
||||
|
||||
## Distribution reloadcmd
|
||||
|
||||
A small script the signer runs after each renewal — for each target it scp's the cert into
|
||||
the aaPanel cert dirs and reloads nginx. Register it once:
|
||||
|
||||
```bash
|
||||
acme.sh --install-cert -d nodeA.example.com --ecc --reloadcmd "/root/acme-deploy/distribute.sh"
|
||||
```
|
||||
|
||||
`distribute.sh` (sketch — adapt hosts/sites to yours; **no real hosts here**):
|
||||
|
||||
```bash
|
||||
TARGETS=(
|
||||
"<nodeA_ip>:root:nodeA.example.com,<cover>.nodeA.example.com,<xui>.nodeA.example.com,<panel>.nodeA.example.com"
|
||||
)
|
||||
SRC=/root/.acme.sh/nodeA.example.com_ecc
|
||||
for entry in "${TARGETS[@]}"; do
|
||||
host="${entry%%:*}"; rest="${entry#*:}"; user="${rest%%:*}"; sites="${rest#*:}"
|
||||
scp -o BatchMode=yes "$SRC/fullchain.cer" "$user@$host:/tmp/.fc.pem"
|
||||
scp -o BatchMode=yes "$SRC/nodeA.example.com.key" "$user@$host:/tmp/.pk.pem"
|
||||
ssh "$user@$host" "IFS=,; for s in \$sites; do d=/www/server/panel/vhost/cert/\$s; mkdir -p \$d;
|
||||
install -m 644 /tmp/.fc.pem \$d/fullchain.pem; install -m 600 /tmp/.pk.pem \$d/privkey.pem; done;
|
||||
rm -f /tmp/.fc.pem /tmp/.pk.pem; /www/server/nginx/sbin/nginx -s reload"
|
||||
done
|
||||
```
|
||||
|
||||
## Trust + notes
|
||||
|
||||
- Put the signer's SSH **public** key in each target's `authorized_keys`; the signer needs write to the BT cert dirs + nginx reload only.
|
||||
- To add a region to an existing wildcard, `--issue` again with the extra `-d` names (SAN change requires a re-issue, not `--renew`), then extend `TARGETS`.
|
||||
- Keep the CF token on the signer only; targets never see it.
|
||||
@@ -0,0 +1,42 @@
|
||||
# DNS records this node needs (any provider)
|
||||
|
||||
The wildcard cert (`03-sign-wildcard-cert.sh`) proves domain control via **DNS-01** using
|
||||
whatever `ACME_DNS_PLUGIN` you set — acme.sh supports Cloudflare, DNSPod, Aliyun, Route53,
|
||||
GoDaddy, and dozens more. That part is provider-agnostic.
|
||||
|
||||
The only thing that is *not* automated for non-Cloudflare providers is creating the host
|
||||
**A/AAAA records**. That's just two names. Create them in your DNS provider's console/API:
|
||||
|
||||
| Name | Type | Value | Proxy/CDN |
|
||||
|---|---|---|---|
|
||||
| `<DOMAIN_ROOT>` (e.g. `nodeA.example.com`) | A | `SERVER_IP` | **off** (DNS-only / grey cloud) |
|
||||
| `*.<DOMAIN_ROOT>` | A | `SERVER_IP` | **off** |
|
||||
| `<DOMAIN_ROOT>` | AAAA | `SERVER_IPV6` | off (skip if no IPv6) |
|
||||
| `*.<DOMAIN_ROOT>` | AAAA | `SERVER_IPV6` | off |
|
||||
|
||||
Notes:
|
||||
- Keep them **unproxied** — the node terminates its own TLS and needs the real client IP path; a proxy/CDN in front would break the WS cover and the panel anti-scan logic.
|
||||
- TTL 300 is fine.
|
||||
- The wildcard `*.<DOMAIN_ROOT>` covers the landing / cover / panel subdomains, so you don't add per-subdomain records.
|
||||
|
||||
## Cloudflare shortcut
|
||||
|
||||
If your zone is on Cloudflare, skip the manual step — `02-cf-dns.py` upserts all four
|
||||
records for you via the API (`CF_API_TOKEN` with `Zone:DNS:Edit`). Run it from wherever the
|
||||
token lives; it never touches the VPS.
|
||||
|
||||
## Picking the acme.sh plugin + creds
|
||||
|
||||
Set `ACME_DNS_PLUGIN` and the matching credential vars in `config.env` (acme.sh reads them
|
||||
from the environment; `03-sign-wildcard-cert.sh` forwards whichever are set):
|
||||
|
||||
| Provider | `ACME_DNS_PLUGIN` | env vars |
|
||||
|---|---|---|
|
||||
| Cloudflare | `dns_cf` | `CF_API_TOKEN` (forwarded as `CF_Token`) |
|
||||
| DNSPod | `dns_dp` | `DP_Id`, `DP_Key` |
|
||||
| Aliyun | `dns_ali` | `Ali_Key`, `Ali_Secret` |
|
||||
| AWS Route53 | `dns_aws` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
|
||||
| GoDaddy | `dns_gd` | `GD_Key`, `GD_Secret` |
|
||||
| others | see acme.sh dnsapi | per its README |
|
||||
|
||||
Full list: <https://github.com/acmesh-official/acme.sh/wiki/dnsapi>
|
||||
@@ -0,0 +1,52 @@
|
||||
# Pitfalls (read before you run)
|
||||
|
||||
Every item here cost real debugging time on a live deployment. They are ordered by how likely they are to bite you.
|
||||
|
||||
## aaPanel
|
||||
|
||||
- **Never `systemctl restart btpanel`.** `btpanel.service` is `enabled` but its `ExecStart` is `/etc/init.d/bt start` (SysV); the systemd unit shows `active: inactive (dead)` while the panel process (`webserver`) runs fine. `systemctl restart btpanel` stops the panel and it does **not** come back → your reverse-proxied panel returns **502**. To restart use `bt 1` or `/etc/init.d/bt start`. Same for nginx (`/etc/init.d/nginx`, not `systemctl`).
|
||||
- **Reverse-proxy needs `CDN_PROXY=True`** in `/www/server/panel/config/cdn.conf`, or the panel rejects proxied requests. Restart the panel after setting it.
|
||||
- **Anti-scan fake 404/403 — the UA has to look like a REAL browser, not just contain the word "Mozilla".** aaPanel returns a *fake* nginx 404 to requests it judges to be scanners. On aaPanel 7.x this check got noticeably stricter: a minimal UA like `Mozilla/5.0 (Macintosh)` (which used to be enough) now gets judged as a scanner and served the fake 404 — even hitting `/login` or `/` directly on the box over the unix socket gives the same fake 404 with that UA. Use a full, realistic UA string, e.g. `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15`, and it works immediately. **If the entrance/domain/path all test as 404 no matter what you try, check the UA string completeness FIRST** — before you start doubting `CDN_PROXY`, domain binding, or the cert. This one costs the most debugging time of anything in this file. A real browser was never affected either way.
|
||||
- Also **403** on direct `IP:port` access once a panel domain is bound — that one's real and expected, not a bug.
|
||||
- **aaPanel 7.x moved several backend modules into a new `class_v2` directory** (e.g. `theme_config.py`). Scripts that do `sys.path.insert(0, "/www/server/panel/class")` and then `import panelSite` (or anything that transitively imports `common.py` → `theme_config`) will fail with `ModuleNotFoundError: theme_config` unless `/www/server/panel/class_v2` is *also* on `sys.path`. `09-aapanel-sites.py` already does this; if you hand-write a new script that touches panel internals, add the same `sys.path.insert(0, "/www/server/panel/class_v2")` line.
|
||||
- **Build sites through the panel, never hand-write vhosts.** A hand-written `/www/server/panel/vhost/nginx/*.conf` works in nginx but is invisible/unmanageable in the panel UI. Use the panel classes (`AddSite`/`SetSSL`/`CreateProxy`/`HttpToHttps`) — that's what `09-aapanel-sites.py` does. The panel HTTP API can't be driven by curl (CSRF + anti-scan); drive the classes with the panel's bundled `pyenv` python.
|
||||
- **nginx 1.24 uses `listen 443 ssl http2;`** — it does **not** support the newer `http2 on;` directive; that syntax fails `nginx -t`.
|
||||
- **The aaPanel install-wait loop must not use `pgrep -f install_7.0` to detect "still installing".** A command like `until run 'test -f BT-Panel && ! pgrep -f install_7.0'; do sleep 15; done` has a self-match bug: the check command's own argv literally contains the string `"install_7.0"`, so `pgrep -f` matches the check itself every time, and the condition can never go true — the loop spins forever even though the install finished in ~1 minute. `04-install-aapanel.sh` instead waits for the literal string `Congratulations` in the install log plus the `BT-Panel` binary existing. If you ever add a similar poll-by-pgrep check, make sure the pattern you grep for doesn't appear in the checking command's own command line (the classic fix elsewhere is `pgrep -f '[i]nstall_7.0'` — the bracket trick — but matching a log string is simpler and was chosen here).
|
||||
|
||||
## 3x-ui (newer versions, Xray 26.x) — multi-protocol notes
|
||||
|
||||
- **Keep `inbounds.settings.clients` populated.** The normalized `clients` and `client_inbounds` tables are projections. Upgrades/migrations rebuild them from settings JSON; a table-only deployment can lose all credentials. The migrated `08-xui-bootstrap.py` includes this fix; repair older nodes with `08b-embed-settings-clients.py` on the VPS after a database backup.
|
||||
|
||||
- **One client owns all six inbounds — don't create one client per protocol.** The whole point of the single-client design is that the panel's Clients page shows ONE row ("node-main") whose Info dialog lists all six share links + QR codes and a single subscription covering all six. That works because the `clients` table is protocol-generic (next bullet): one row carrying both a `uuid` and a `password`, linked via `client_inbounds` to all six inbounds. `08-xui-bootstrap.py` does exactly this. (An earlier iteration created three separate clients — node-main / trojan-main / ss-main — which "worked" for traffic but split the links across three panel rows and made the single-subscription view impossible; consolidating them later meant deleting two clients and re-pointing credentials. Build it as one client from the start.)
|
||||
- **The `clients` table is protocol-generic, not protocol-specific.** It has both a `uuid` column and a `password` column (plus VMess/VLESS-only fields like `security`/`flow`). VMess/VLESS inbounds read `uuid`; Trojan/Shadowsocks inbounds read `password`. There's no "protocol" column on `clients` itself — which field 3x-ui's backend actually uses depends on the *inbound's* `protocol` value at the point it builds the live xray config, not on anything stored on the client row. This isn't documented anywhere obvious; it was confirmed by reading the live generated `/usr/local/x-ui/bin/config.json` after inserting rows and restarting `x-ui`, and seeing the right credential land in the right field for each protocol. **Consequence:** with one client, Trojan and Shadowsocks share ONE password (that client's `password` column); VMess uses its `uuid`. That's intended for a single-user node — it's what `NODE_PASSWORD` in `config.env` is.
|
||||
- **`user_id` on an inbound must be set (1), or the inbound is INVISIBLE in the panel.** The panel's Inbounds and Clients lists filter by the logged-in admin's `user_id`. A DB-inserted inbound with a NULL/empty `user_id` exists in the DB and proxies traffic perfectly (xray loads it, smoke test passes), but the panel UI silently filters it out — you'll see "only the vmess inbounds show up" even though all six are there. `08-xui-bootstrap.py` sets `user_id=1` on every inserted inbound. If you hand-insert an inbound later, set it too (or `UPDATE inbounds SET user_id=1 WHERE user_id IS NULL OR user_id=''`).
|
||||
- **Every inbound needs an `externalProxy` in streamSettings, or the generated share links are useless.** These inbounds listen on `127.0.0.1` with `security:"none"` (nginx terminates TLS at the edge on `<cover>:443`). 3x-ui doesn't know the public address, so by default it generates links pointing at `127.0.0.1:<internal port>` with no TLS — which no client can use. Add `"externalProxy":[{"forceTls":"tls","dest":"<cover host>","port":443,"sni":"<cover host>","remark":"<name>"}]` to each inbound's streamSettings and every generated link/QR/subscription instead points at `<cover>:443` over TLS. `08-xui-bootstrap.py` does this. (This is a *link-generation* hint only — it's a 3x-ui-specific field that's stripped when building the live xray config, so it never affects the data path.)
|
||||
- **Shadowsocks inbound `settings` needs a TOP-LEVEL `password` field, or the panel's "copy links" endpoint returns an empty 200 and the UI shows "No response data".** 3x-ui v3.5.0's share-link generator does `settings["password"].(string)` — an *unchecked* Go type assertion. A normal (non-2022) SS inbound has no top-level password (the real per-client password lives on the client), so that assertion hits `nil` and **panics**. gin's recovery swallows the panic (its error writer is `io.Discard`, so nothing reaches `journalctl`), the `/panel/api/clients/subLinks/<subId>` endpoint returns HTTP 200 with an EMPTY body, and the frontend — seeing no parseable JSON — reports "No response data" and drops *all* the links (not just SS). The subscription server returns 500 for the same reason. Fix: write a top-level `"password"` into the SS inbound's `settings` (auth still goes through the client table; for chacha20 ciphers this field doesn't even appear in the emitted `ss://` link — it exists only to keep the assertion from panicking). `08-xui-bootstrap.py` sets it. Debugging note: this presents as "200 empty body", not an error status — so watch for empty-but-200 API responses, and remember the sub server's Go logs are discarded.
|
||||
- **`client_traffics.email` is GLOBALLY UNIQUE.** One client shared across two inbounds (e.g. a protocol's direct + warp inbound) gets **one** `client_traffics` row, not one per inbound — inserting two rows with the same email throws `UNIQUE constraint failed`.
|
||||
- **`client_inbounds` has a `flow_override` column** (client_id, inbound_id, flow_override, created_at) — include it in the INSERT even though it's usually empty; some 3x-ui builds may reject a 3-column insert if the schema doesn't line up with what an older reference implementation expected.
|
||||
- **Extend `xrayTemplateConfig`'s WARP routing rule, never overwrite the whole thing.** The WARP-egress inbounds' tags (and their `inbound-127.0.0.1:<port>` fallback form) need to be appended to the *existing* rule's `inboundTag` array. Re-generating and writing the whole `xrayTemplateConfig` from scratch (instead of read-modify-write) silently deletes whatever protocols were added in a previous run.
|
||||
- **Auth is by UUID/password**, so a mismatch between the traffic-stat email label and the running config email is cosmetic, not a fault.
|
||||
- **Protocol mismatch when testing.** A VLESS test client against a `vmess+ws` inbound is correctly rejected (`websocket close 1000`) — that's not a server bug, it's the wrong protocol. Match the test client's protocol (vmess/trojan/shadowsocks) exactly to the inbound you're testing, with the right `security`/`method`/`password` for that protocol.
|
||||
- **Panel API `/login` may 403** on newer builds (security middleware). Do DB-direct edits + `systemctl restart x-ui` instead of the HTTP API — confirmed true both for the original VMess bootstrap and again when adding Trojan/Shadowsocks later.
|
||||
- **Shadowsocks 2022-blake3-\* ciphers need an exact-byte-length base64 password/PSK** — get the length wrong and xray fails to start with no obvious error pointing at the cipher. `chacha20-poly1305` (or `aes-256-gcm`) takes a plain password string, is much harder to misconfigure, and is still a modern AEAD cipher — prefer it unless you have a specific reason to use the 2022 series.
|
||||
|
||||
## Cloudflare WARP
|
||||
|
||||
- **`gpg` must be installed first.** On a minimal Debian, `gpg --dearmor` for the repo keyring fails with `gpg: command not found`, and the later `apt update` then errors `Failed to parse keyring … sqv returned an error code`. `apt-get install -y gnupg` before adding the repo.
|
||||
- Use the **`bookworm`** repo line even on Debian 13 (trixie) — the bookworm package works.
|
||||
- **WARP can lie about being connected.** The control plane shows `Connected` while the SOCKS proxy stops egressing. The watchdog (`warp-healthcheck.*`) checks `warp=on` *through the proxy* and restarts `warp-svc` when it isn't.
|
||||
|
||||
## nginx / cover
|
||||
|
||||
- WS upgrade must return **101** with `--http1.1` + the `Upgrade`/`Connection` headers. If you test with HTTP/2 you'll get 400 (HTTP/2 doesn't carry the Upgrade frame).
|
||||
- Default server for unmatched `server_name` should `return 444` so scanners hitting the bare IP / wrong host get dropped.
|
||||
|
||||
## Monitoring
|
||||
|
||||
- **Komari** auto-discovery registers the node with an auto-generated name (`Auto-<hostname>`). Rename/group it in your panel afterward (panel-specific; often a small DB update or a UI action). The agent falls back to a v1 WebSocket if the panel has no v2 API — that's normal.
|
||||
- **Nezha** agent install one-liners change over time. If the scripted install fails, drop the `nezha-agent` binary + a `config.yml` (server, `tls`, `client_secret`, `uuid`) and a small systemd unit — the agent connects out to `SERVER:PORT` (gRPC), so only outbound is needed.
|
||||
|
||||
## SSH / access
|
||||
|
||||
- Some providers ship a `.pem`/`.ppk` bundle; use the `.pem`. `chmod 600` it or ssh refuses it.
|
||||
- Fresh boxes may run fail2ban; repeated failed auth from a dynamic IP can lock you out. Get your key in early (step 0) so you're not relying on password auth.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Why VMess + Trojan + Shadowsocks, and when to add a fourth
|
||||
|
||||
This skill's default 6-inbound target (3 protocols × direct/WARP) was chosen for a specific
|
||||
combination of constraints: client compatibility (Surge excludes the Shadowsocks-over-WS lines; see the project capability matrix), transport safety (no UDP, so nothing here is exposed to networks that
|
||||
throttle UDP indiscriminately), and zero extra server software (all three are native to
|
||||
3x-ui/Xray-core — no sing-box, no separate Hysteria2/TUIC binary to maintain).
|
||||
|
||||
If your constraints are different, the right protocol set is different too. This file is the
|
||||
reasoning, not a rule — read it before assuming the default 3 are the "right" choice for a new
|
||||
node.
|
||||
|
||||
## The three defaults
|
||||
|
||||
| Protocol | Transport overhead | GFW-detection posture | Client support |
|
||||
|---|---|---|---|
|
||||
| VMess (+WS+TLS) | Two encryption layers (VMess's own AEAD, on top of the outer TLS) + WS framing — heaviest of the three | Weakest of the three; historically a specific target of active probing, and Xray-core itself now prints a deprecation warning for it | Nearly universal |
|
||||
| Trojan (+WS+TLS) | One layer (just the outer TLS) + WS framing — noticeably lighter than VMess | Stronger: Trojan's whole design is "indistinguishable from a real HTTPS site to anyone without the password" | Nearly universal, including Surge |
|
||||
| Shadowsocks (+WS+TLS) | Similar to Trojan — SS's own cipher is lightweight, WS+TLS is the same cover as the others | Comparable to VMess's cover level, but without VMess's specific deprecated-protocol baggage | Supported by compatible WS clients; Surge does not support SS over WS |
|
||||
|
||||
All three are plain TCP end-to-end between client and server (the WS layer rides on TCP), so
|
||||
none of them are exposed to networks that indiscriminately throttle UDP — that includes the
|
||||
"UDP forwarding" toggle some clients show for VMess/Trojan/SS: it tunnels UDP *payloads* inside
|
||||
the already-established TCP connection, it doesn't open a real UDP socket to the server.
|
||||
|
||||
## What "UDP is faster" actually means (and doesn't)
|
||||
|
||||
QUIC-based protocols (Hysteria2, TUIC) run on top of UDP, and that fact alone doesn't make them
|
||||
faster — raw UDP has no reliability or congestion control built in. Their speed advantage comes
|
||||
from what they build *on top of* UDP: no head-of-line blocking across multiplexed streams,
|
||||
faster connection setup/resumption, connection migration across network changes, and (for
|
||||
Hysteria2 specifically) an aggressive custom congestion-control algorithm designed for lossy
|
||||
links. That advantage is real and large specifically on **high-latency or high-packet-loss**
|
||||
networks. On a clean, low-latency network, well-implemented TCP+TLS (Trojan, VLESS) is often
|
||||
just as fast, sometimes faster (QUIC's per-packet encryption has real per-byte CPU cost that
|
||||
kernel-optimized TCP+TLS doesn't). Don't add a UDP-based protocol to this skill's target list on
|
||||
the assumption that "UDP is just faster" — add it because you specifically need resilience on
|
||||
bad networks or fast network-switching, and you've weighed that against UDP's other real risk:
|
||||
plenty of networks throttle/block UDP indiscriminately, which no amount of protocol-level
|
||||
cleverness fixes.
|
||||
|
||||
## When to add a fourth protocol
|
||||
|
||||
- **Client doesn't support any of the three (rare)** — e.g. something that only speaks VLESS or
|
||||
Hysteria2. Add exactly what that client needs; see `3x-ui-schema.md` for the
|
||||
mechanical steps (VLESS/VLESS+REALITY are native to 3x-ui/Xray-core, no extra software).
|
||||
- **You specifically need the strongest current anti-active-probing posture** — VLESS+REALITY
|
||||
(or Trojan+REALITY; Xray-core supports REALITY on both) borrows a real site's TLS handshake,
|
||||
which is currently the hardest thing for active probing to distinguish from genuinely visiting
|
||||
that site. Native to 3x-ui. Needs picking a "borrowed" target site with real TLS 1.3, decent
|
||||
traffic, and no active blocking of its own in your target region.
|
||||
- **You specifically need next-generation traffic-pattern resistance** — AnyTLS defends against
|
||||
statistical traffic-shape analysis (packet size/timing), which REALITY's handshake-mimicry
|
||||
doesn't address by itself. Not native to 3x-ui/Xray-core — needs sing-box installed
|
||||
server-side, as a separate service alongside 3x-ui (different ports, no conflict).
|
||||
- **You specifically need resilience on bad/mobile networks and already have a UDP fallback
|
||||
elsewhere** — Hysteria2 (preferred over TUIC: larger community, more obfuscation options,
|
||||
more battle-tested) if you don't already have a node covering this case. Also needs sing-box
|
||||
or a standalone Hysteria2 binary, not native to 3x-ui.
|
||||
- **Don't add a protocol "just in case."** Every inbound is one more thing to keep working
|
||||
through aaPanel/3x-ui/Xray version upgrades — see `pitfalls.md` for how much
|
||||
version drift alone (not malicious blocking) can break a working inbound.
|
||||
@@ -0,0 +1,47 @@
|
||||
# 裸 nginx、QUIC 与既有节点维护
|
||||
|
||||
脚本从各节点原工作副本迁入,历史 `-sg`/`-rn` 后缀保留;这些后缀表示流程分支,不表示脚本只能在该站点使用。所有 `.sh` 从本机通过 SSH 修改 VPS。命令中的 `scripts/` 指私有节点工作目录里的副本。
|
||||
|
||||
## 选择部署分支
|
||||
|
||||
| 步骤 | aaPanel 管理 nginx | 裸 nginx |
|
||||
|---|---|---|
|
||||
| 准备机器 | `00-preflight.sh` | `00-preflight-sg.sh`(含 swap、BBR、apt nginx) |
|
||||
| DNS | `02-cf-dns.py` | 同左 |
|
||||
| 证书 | `03-sign-wildcard-cert.sh` | `03-sign-cert-sg.sh`,证书在 `/etc/nginx/ssl/<root>/` |
|
||||
| nginx | `04-install-aapanel.sh`、`04b-install-nginx.sh`、`05-harden-aapanel.sh` | 已由 preflight 安装,跳过 aaPanel 步骤 |
|
||||
| WARP | `06-install-warp.sh`、`06b-warp-watchdog.sh` | 同左 |
|
||||
| TCP 节点 | `07-install-3xui.sh`、`08-xui-bootstrap.sh` | 同左 |
|
||||
| 站点 | `09-aapanel-sites.sh`、`09b-default-444.sh` | `09-nginx-sites-sg.sh` |
|
||||
| 防火墙 | `10-ufw.sh`(核对实际面板端口) | `10-ufw-sg.sh`(TCP 22/80/443) |
|
||||
| TCP 验收 | `12-smoke-test.sh` | `12-smoke-test-sg.sh` |
|
||||
|
||||
`11-monitoring.sh` 是可选监控安装。既有站点以自己的实际配置为准;不要把完整部署流水线当成日常维护命令。
|
||||
|
||||
## QUIC
|
||||
|
||||
在私有 `scripts/config.env` 设置所需的 `PORT_HY2_DIRECT`/`PORT_HY2_WARP` 或 `PORT_TUIC_DIRECT`/`PORT_TUIC_WARP`,未设置的线路跳过。Hysteria2 使用 `NODE_PASSWORD`,可设置 `HY2_OBFS_PASSWORD`(salamander)及带宽;TUIC 使用 `XUI_UUID`+`NODE_PASSWORD`。
|
||||
|
||||
裸 nginx 分支执行 `bash scripts/13-install-singbox-quic.sh`,它推送 `13-singbox-config.py`、生成 sing-box 配置、做 `sing-box check`、开放配置的 UDP 端口并启动服务。aaPanel 节点需要先按实际证书目录适配,不能直接采用默认 `/etc/nginx/ssl/<root>/`。
|
||||
|
||||
该脚本的续期动作面向裸 nginx,会设为 `systemctl reload nginx; systemctl restart sing-box`。既有机器若已有集中证书复制或其他服务动作,先读取并保留旧钩子,再合并;不能直接用此步骤替换旧配置。UFW 之外还要核对供应商云安全组。
|
||||
|
||||
`10-ufw-rn.sh` 是四条 QUIC 线路都存在时的历史防火墙分支,要求两条 HY2 与两条 TUIC 端口,不用于仅 HY2 的机器。
|
||||
|
||||
生成链接在本机执行:
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
python3 scripts/14-generate-links-and-qr.py
|
||||
python3 scripts/15-generate-quic-links.py
|
||||
```
|
||||
|
||||
第二个脚本读取 `HY2_OBFS_PASSWORD`;仅 TUIC 时也保留该字段为空。新增占位配置默认 QUIC 端口为空,不会隐式部署 QUIC。配置生成器能省略混淆,但现有链接生成器始终写 salamander;生成 HY2 链接时应提供非空混淆密码,或先同步修改链接与服务端逻辑。
|
||||
|
||||
## 3x-ui 升级与 SOCKS5
|
||||
|
||||
新 `08-xui-bootstrap.py` 已写入 `settings.clients`,旧部署用 `08b-embed-settings-clients.py` 补齐。该 Python 文件在 VPS 上执行,默认会重启 x-ui;`XUI_DB` 可指定数据库,`RESTART_XUI=0` 可由任务安排重启。修改前备份数据库,完成后重新核对六条 TCP 线路与 WARP 规则。
|
||||
|
||||
`16-add-socks5.py` 是在 VPS 上运行的可选内部 SOCKS5 入站工具,通过 `SOCKS_PORT`、`SOCKS_USER`、`SOCKS_PASS`、`SERVER_IP` 等环境变量配置,默认监听公网且会重启 x-ui。按访问意图设置 `LISTEN`;用户名密码认证本身不加密。它不是标准六条线路部署步骤,也不是 Proxy Station 可导入类型。
|
||||
|
||||
这里保存的是已有部署工具的流程说明。涉及新版本安装或升级时,先核对官方版本兼容性,再在授权范围执行;本次工程迁移没有对生产节点执行这些脚本。
|
||||
175
.claude/skills/edge-node-multi-protocol/references/workflow.md
Normal file
175
.claude/skills/edge-node-multi-protocol/references/workflow.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# aaPanel deployment workflow
|
||||
|
||||
This is the migrated aaPanel workflow. Run shell commands from the LOCAL private node workspace: Bash scripts invoke SSH and their Python companions run on the VPS. Standalone nginx, QUIC and upgrade repairs are documented in `standalone-and-quic.md`. Keep live config out of the skill source directory.
|
||||
|
||||
Turn a bare Debian/Ubuntu VPS into a fully managed edge node running **three proxy protocols side by side, each with a direct-egress and a Cloudflare-WARP clean-exit inbound — six inbounds in total.** This is the standard target of this skill, not an optional extra: a single protocol is a fragile bet (if it gets fingerprinted or a client stops supporting it, the whole node goes dark), and three protocol families sharing the same domain/cert/firewall costs almost nothing extra to run once the panel infrastructure is up.
|
||||
|
||||
**What it builds**
|
||||
|
||||
| Layer | Component | Result |
|
||||
|---|---|---|
|
||||
| Panel | aaPanel International (7.x) + nginx | `<SUB_PANEL>.<root>` reverse-proxied, secret-path entrance, hardened |
|
||||
| Proxy | 3x-ui (Xray) | `<SUB_XUI>.<root>` panel; **6 inbounds** behind nginx: VMess/Trojan/Shadowsocks × direct/WARP |
|
||||
| Clean exit | cloudflare-warp (proxy mode) | every protocol's WARP inbound egresses through Cloudflare WARP |
|
||||
| TLS | acme.sh DNS-01 (any provider) | wildcard `*.<root>` cert, auto-renew + reload, shared by all 6 inbounds |
|
||||
| DNS | Cloudflare API (or your provider, by hand) | `<root>` + `*.<root>` A/AAAA (DNS-only) |
|
||||
| Cover | nginx vhosts | landing page + node cover (6 WS paths) + default-server `444` |
|
||||
| Watchdog | systemd timer | WARP proxy self-heal every 60s |
|
||||
| Monitoring | Nezha + Komari agents (optional) | node appears on your panels |
|
||||
| Access | SSH key + optional shortcut | passwordless login |
|
||||
|
||||
> **This is a template, not a turnkey binary.** You supply your own domain, Cloudflare token, server IP, and passwords. No secrets, domains, or routing rules are baked in.
|
||||
|
||||
## Why three protocols instead of one
|
||||
|
||||
VMess, Trojan, and Shadowsocks(+WS/TLS) use TCP and are implemented by 3x-ui/Xray-core. Client support depends on the protocol/transport combination: Surge does not support Shadowsocks over WebSocket. Proxy Station filters unsupported combinations using `shared/src/protocols.ts`. Running all three from day one means: if a client only supports a subset (e.g. Surge doesn't do VLESS), you're already covered; if one protocol family gets fingerprinted, you have two independent fallbacks without touching the box again. See `protocol-choice.md` for the fuller reasoning (performance/detection tradeoffs, and how to add a fourth protocol like VLESS+REALITY later).
|
||||
|
||||
## Inputs to collect first
|
||||
|
||||
Fill a working copy of `scripts/config.env.example` → `config.env` (git-ignored by you). Collect these before touching the box:
|
||||
|
||||
- `SERVER_IP` / `SERVER_IPV6` — the new VPS public addresses (amd64 or arm64)
|
||||
- `SSH_USER` / `SSH_KEY` — `root` or a sudo user (scripts escalate via `sudo`), and the private key
|
||||
- `TZ` — server timezone (default `UTC`)
|
||||
- `DOMAIN_ROOT` — the sub-zone this node owns, e.g. `nodeA.example.com` (you get `*.nodeA.example.com`)
|
||||
- `ACME_DNS_PLUGIN` + creds — DNS provider for the wildcard cert. Default `dns_cf` (Cloudflare). Any acme.sh-supported provider works (DNSPod/Aliyun/Route53/GoDaddy/…) — set the plugin + its env vars (`dns-records.md`).
|
||||
- `CF_API_TOKEN` / `CF_ZONE_ID` — Cloudflare only: token with **Zone:DNS:Edit** (enables the turnkey A/AAAA record script). Non-CF users create the A/AAAA records themselves.
|
||||
- `SUB_PANEL` / `SUB_XUI` / `SUB_COVER` — the subdomain labels for the three planes. **If your zone is public, choose non-obvious labels** — don't reuse a guessable convention or people can enumerate your control planes from DNS.
|
||||
- `AAPANEL_USER` / `AAPANEL_ENTRANCE` — desired panel username + secret entrance path (e.g. `/somesecret`)
|
||||
- `XUI_USER` / `XUI_PATH` — 3x-ui username + webBasePath (e.g. `/somesecret/`)
|
||||
- **Six WS cover paths + two ports each** (`PORT_<PROTO>_{DIRECT,WARP}` / `WS_PATH_<PROTO>_{DIRECT,WARP}` for `VMESS`/`TROJAN`/`SS`) — pick your own, non-obvious, don't reuse a naming convention across them
|
||||
- `XUI_UUID` + `NODE_PASSWORD` (+ `XUI_SUBID`, `SS_METHOD` default `chacha20-poly1305`) — the credentials for the **single `node-main` client** that owns all six inbounds: `XUI_UUID` authenticates the two VMess inbounds, `NODE_PASSWORD` authenticates the two Trojan **and** two Shadowsocks inbounds (they share it — it's one client). Generate strong values at runtime; **store them in your password manager**, never in git.
|
||||
- monitoring (optional) — Nezha `SERVER:PORT` + shared key; Komari endpoint + auto-discovery key
|
||||
|
||||
## Workflow
|
||||
|
||||
For a fresh aaPanel node, follow the selected steps in order from the local private workspace. Shell scripts read `scripts/config.env` and invoke SSH; their Python companions run on the VPS. For an existing node, read its handover notes and run only the steps required by the task.
|
||||
|
||||
### 0. Verify access & prep
|
||||
```bash
|
||||
scripts/00-preflight.sh # confirms SSH, records specs, sets timezone, installs base pkgs (curl/gnupg/ufw)
|
||||
```
|
||||
Also drop **your** public key into the box's `~/.ssh/authorized_keys` for passwordless login.
|
||||
|
||||
### 1. Wildcard DNS records
|
||||
```bash
|
||||
python3 scripts/02-cf-dns.py # Cloudflare: upserts <root> + *.<root> A/AAAA -> SERVER_IP/IPV6 (proxied=false)
|
||||
```
|
||||
Non-Cloudflare users: create the same records by hand, see `dns-records.md`.
|
||||
|
||||
### 2. Wildcard TLS cert (acme.sh DNS-01)
|
||||
```bash
|
||||
scripts/03-sign-wildcard-cert.sh # on-box acme.sh --dns dns_cf, issues *.<root> (ECDSA P-256, Let's Encrypt)
|
||||
```
|
||||
One cert, shared by all 6 inbounds via the configured `<SUB_COVER>.<root>` cover domain. Fleet cert signing: `centralized-certs.md`.
|
||||
|
||||
### 3. aaPanel International + nginx
|
||||
```bash
|
||||
scripts/04-install-aapanel.sh # official install_7.0_en.sh (headless); waits for the log's success banner
|
||||
scripts/05-harden-aapanel.sh # sets username/password/entrance, binds panel domain, CDN_PROXY=True
|
||||
scripts/04b-install-nginx.sh # install_soft.sh 0 install nginx 1.24 (aaPanel-managed)
|
||||
```
|
||||
`pitfalls.md` has three aaPanel-7.x-specific fixes baked into these scripts already (install-wait logic, `class_v2` import path, anti-scan UA strictness) — read it if you're editing these rather than just running them.
|
||||
|
||||
### 4. Cloudflare WARP (clean exit)
|
||||
```bash
|
||||
scripts/06-install-warp.sh # cloudflare-warp, proxy mode SOCKS5 127.0.0.1:40000, verify warp=on
|
||||
scripts/06b-warp-watchdog.sh # installs warp-healthcheck.sh + systemd timer (self-heal every 60s)
|
||||
```
|
||||
|
||||
### 5. 3x-ui (Xray) + six inbounds under one client
|
||||
```bash
|
||||
scripts/07-install-3xui.sh # installs 3x-ui, sets panel 127.0.0.1:14533 + XUI_PATH + creds
|
||||
scripts/08-xui-bootstrap.sh # adds ALL SIX inbounds under ONE node-main client, in one pass
|
||||
```
|
||||
`08-xui-bootstrap.py` creates a **single `node-main` client** (carrying both the VMess UUID and the shared
|
||||
Trojan/Shadowsocks password) linked to all six inbounds — so the panel's Clients page shows one row whose
|
||||
Info dialog lists all six share links + QR codes and one subscription. It bakes in four version-compat
|
||||
fixes that are easy to miss and each cost real debugging on a live v3.5.0 box: `user_id=1` (else inbounds
|
||||
are invisible in the panel), an `externalProxy` link hint (else generated links point at the useless
|
||||
internal `127.0.0.1:<port>`), a top-level SS `password` (else the panel's copy-links endpoint panics and
|
||||
returns empty → "No response data"), and read-modify-write of the WARP routing rule (else re-runs wipe it).
|
||||
It writes directly into 3x-ui's sqlite DB rather than using the panel API (which 403s scripted requests on
|
||||
this build) — see `3x-ui-schema.md` and `pitfalls.md` for
|
||||
the table layout if you need to add a 4th protocol or debug a client not authenticating.
|
||||
|
||||
### 6. aaPanel sites (panel-native, UI-manageable)
|
||||
```bash
|
||||
scripts/09-aapanel-sites.sh # AddSite + SetSSL + CreateProxy (x6 for cover) + HttpToHttps, via panel classes
|
||||
scripts/09b-default-444.sh # unmatched server_name -> return 444; drop landing page
|
||||
```
|
||||
|
||||
### 7. Firewall + monitoring (optional)
|
||||
```bash
|
||||
scripts/10-ufw.sh # allow 22/80/443 + panel port + aaPanel 888/39000-40000
|
||||
scripts/11-monitoring.sh # optional: install Nezha + Komari agents (placeholders in config.env)
|
||||
```
|
||||
No extra firewall ports needed for the 6 inbounds — they're all `127.0.0.1`-only, reverse-proxied through nginx on 443.
|
||||
|
||||
### 8. Smoke test — all six inbounds
|
||||
```bash
|
||||
scripts/12-smoke-test.sh
|
||||
```
|
||||
Checks: WS upgrade returns 101 on all 6 cover paths; landing/3x-ui/aaPanel panels return 200 (aaPanel test
|
||||
uses a full realistic UA string, see pitfalls); an unmatched hostname gets dropped (444); and — via a
|
||||
temporary on-box xray client covering all 6 outbounds — every `*-direct` egress IP equals `SERVER_IP` with
|
||||
`warp=off`, and every `*-warp` egress IP is a Cloudflare address with `warp=on`.
|
||||
|
||||
### 9. Generate client links + QR codes
|
||||
```bash
|
||||
python3 scripts/14-generate-links-and-qr.py # run LOCALLY, not on the box
|
||||
```
|
||||
Writes `vmess://`, `trojan://` and Xray-style `ss://` share links + QR codes via local `qrencode`, plus sing-box JSON snippets for Shadowsocks. Proxy Station parses these SS links; other clients may require transport-specific import. Nothing is sent to a third-party service. See
|
||||
`templates/02-node-info-template.md` for how to hand these to a client.
|
||||
|
||||
### 10. Documentation
|
||||
Fill in the three templates in `templates/` (handover doc, node info, ops guide) with this node's real
|
||||
values — see "Documentation templates" below.
|
||||
|
||||
## Domains this creates
|
||||
|
||||
With `DOMAIN_ROOT=nodeA.example.com` and example labels `SUB_COVER=edge`, `SUB_XUI=gateway`, `SUB_PANEL=console` (pick your own):
|
||||
|
||||
| Domain | Role | Backend |
|
||||
|---|---|---|
|
||||
| `nodeA.example.com` | landing page | static |
|
||||
| `<SUB_COVER>.nodeA.example.com` | node cover: 6 WS paths, one per inbound | 127.0.0.1:57707-57712 |
|
||||
| `<SUB_XUI>.nodeA.example.com` | 3x-ui panel | 127.0.0.1:14533 |
|
||||
| `<SUB_PANEL>.nodeA.example.com` | aaPanel | 127.0.0.1:\<panel port\> |
|
||||
| `*.nodeA.example.com` (default) | unmatched → `return 444` | — |
|
||||
|
||||
> The labels above are just examples. **Never publish the real labels/paths you pick** — on a public zone they are the map to your control planes and tunnels.
|
||||
|
||||
## Adding a protocol to an already-provisioned node
|
||||
|
||||
You don't need to redo steps 0-4. Add the new protocol's env vars to `config.env`, extend the `PROTOCOLS`
|
||||
list in `scripts/08-xui-bootstrap.py` and the `PROXIES` loop in `scripts/09-aapanel-sites.py` (both scripts
|
||||
are idempotent — re-running them skips ports/sites that already exist), re-run `08-xui-bootstrap.sh` then
|
||||
`09-aapanel-sites.sh`, then re-run the smoke test with the new outbound added. Full walkthrough with the
|
||||
concrete steps used to add Trojan+Shadowsocks to a VMess-only node: `3x-ui-schema.md`.
|
||||
|
||||
## Documentation templates
|
||||
|
||||
`templates/01-handover-template.md`, `templates/02-node-info-template.md`, `templates/03-ops-guide-template.md`
|
||||
are meant to be copied out and filled in with a real node's values after deployment (they're written with
|
||||
`{{PLACEHOLDER}}` tokens, not a real node's domain/keys). See `../templates/README.md` for the full list of
|
||||
tokens each one needs.
|
||||
|
||||
## References
|
||||
|
||||
- `pitfalls.md` — **read first.** aaPanel 7.x version-compatibility fixes (install-wait logic, `class_v2`, anti-scan UA strictness), 3x-ui multi-protocol schema gotchas, WARP/nginx/monitoring/SSH pitfalls. Skipping it costs hours.
|
||||
- `3x-ui-schema.md` — the sqlite table layout 3x-ui uses, and how to add a 4th protocol.
|
||||
- `protocol-choice.md` — why VMess+Trojan+Shadowsocks, performance/detection tradeoffs vs VLESS+REALITY/AnyTLS/Hysteria2/TUIC, and when you'd want a 4th protocol.
|
||||
- `dns-records.md` — the exact records to create, and how to use a non-Cloudflare DNS provider.
|
||||
- `centralized-certs.md` — sign wildcards on one host and push to a fleet.
|
||||
|
||||
## Project entry points
|
||||
|
||||
Use the project root `AGENTS.md` and the shared `edge-node-multi-protocol` skill. The former standalone agent guides are migration snapshots, not active instructions.
|
||||
|
||||
## Privacy / safety notes
|
||||
|
||||
- Never commit `config.env`, cert keys, panel/xui passwords, or any UUID/password/token. Add them to your password manager.
|
||||
- The scripts contain **no** real domains, IPs, subscription rules, or routing policy — only placeholders. Keep it that way when you adapt them.
|
||||
- WARP mode here is for **your own** clean egress; it is not a bypass for anyone else's controls.
|
||||
- The link/QR-code generator (step 9) never sends anything to a third-party service — QR codes are generated fully offline with local `qrencode`.
|
||||
Reference in New Issue
Block a user