fix: 限制边缘节点仅使用 IPv4 并记录维护约定
This commit is contained in:
@@ -5,14 +5,13 @@ whatever `ACME_DNS_PLUGIN` you set — acme.sh supports Cloudflare, DNSPod, Aliy
|
||||
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:
|
||||
**A records**. The current fleet is IPv4-only: keep `SERVER_IPV6` empty and do not create
|
||||
AAAA records. Create these two names 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.
|
||||
@@ -21,10 +20,14 @@ Notes:
|
||||
|
||||
## 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
|
||||
If your zone is on Cloudflare, skip the manual step — `02-cf-dns.py` upserts the two A
|
||||
records when `SERVER_IPV6` is empty (`CF_API_TOKEN` with `Zone:DNS:Edit`). Run it from wherever the
|
||||
token lives; it never touches the VPS.
|
||||
|
||||
Leaving `SERVER_IPV6` empty does **not** delete existing AAAA records. For an existing
|
||||
node, back up DNS records, remove the AAAA records under that node's domain, then check
|
||||
the API and public DNS. See [IPv4-only fleet policy](../../../../docs/EDGE-IPV4-ONLY.md).
|
||||
|
||||
## Picking the acme.sh plugin + creds
|
||||
|
||||
Set `ACME_DNS_PLUGIN` and the matching credential vars in `config.env` (acme.sh reads them
|
||||
|
||||
@@ -32,6 +32,7 @@ Every item here cost real debugging time on a live deployment. They are ordered
|
||||
|
||||
## Cloudflare WARP
|
||||
|
||||
- **Disabling host IPv6 does not disable IPv6 inside WARP's proxy tunnel.** Keep the IPv4-only guards in Xray and sing-box: force IPv4 domain resolution and reject `::/0` before WARP routing. `UseIPv4` can fall back to system resolution in Xray; use `ForceIPv4`. Do not pass unresolved target domains to WARP. See [fleet policy](../../../../docs/EDGE-IPV4-ONLY.md).
|
||||
- **`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.
|
||||
|
||||
@@ -107,22 +107,26 @@ TEMPLATE = {
|
||||
"inbounds": [{"listen": "127.0.0.1", "port": 62789, "protocol": "dokodemo-door",
|
||||
"settings": {"address": "127.0.0.1"}, "tag": "api"}],
|
||||
"outbounds": [
|
||||
{"protocol": "freedom", "settings": {}},
|
||||
{"protocol": "freedom", "settings": {"domainStrategy": "ForceIPv4"},
|
||||
"streamSettings": {"sockopt": {"domainStrategy": "ForceIPv4"}}},
|
||||
{"tag": "warp-outbound", "protocol": "freedom",
|
||||
"settings": {"domainStrategy": "UseIPv6v4"}, "proxySettings": {"tag": "warp-socks"}},
|
||||
"settings": {"domainStrategy": "ForceIPv4"},
|
||||
"streamSettings": {"sockopt": {"domainStrategy": "ForceIPv4"}},
|
||||
"proxySettings": {"tag": "warp-socks"}},
|
||||
{"tag": "warp-socks", "protocol": "socks",
|
||||
"settings": {"servers": [{"address": "127.0.0.1", "port": WARP_SOCKS_PORT}]}},
|
||||
{"protocol": "blackhole", "settings": {}, "tag": "blocked"},
|
||||
],
|
||||
"policy": {"system": {"statsInboundDownlink": True, "statsInboundUplink": True}},
|
||||
"routing": {"rules": [
|
||||
{"ip": ["::/0"], "outboundTag": "blocked", "type": "field"},
|
||||
{"inboundTag": ["api"], "outboundTag": "api", "type": "field"},
|
||||
{"inboundTag": [], "outboundTag": "warp-outbound", "type": "field"}, # tags filled in below
|
||||
{"ip": ["geoip:private"], "outboundTag": "blocked", "type": "field"},
|
||||
{"protocol": ["bittorrent"], "outboundTag": "blocked", "type": "field"},
|
||||
]},
|
||||
"stats": {},
|
||||
"dns": {"servers": ["1.1.1.1"], "queryStrategy": "UseIP"},
|
||||
"dns": {"servers": ["1.1.1.1"], "queryStrategy": "UseIPv4"},
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +262,16 @@ def main():
|
||||
# WARP routing rule: read-modify-write (fix #4) -- append, never clobber.
|
||||
row = cur.execute("SELECT value FROM settings WHERE key='xrayTemplateConfig'").fetchone()
|
||||
tmpl = json.loads(row[0]) if row else TEMPLATE
|
||||
# Enforce the fleet's IPv4-only policy when extending an existing template too.
|
||||
for outbound in tmpl["outbounds"]:
|
||||
if outbound.get("protocol") == "freedom":
|
||||
outbound.setdefault("settings", {})["domainStrategy"] = "ForceIPv4"
|
||||
outbound.setdefault("streamSettings", {}).setdefault("sockopt", {})["domainStrategy"] = "ForceIPv4"
|
||||
tmpl.setdefault("dns", {})["queryStrategy"] = "UseIPv4"
|
||||
blocked_tag = next(o["tag"] for o in tmpl["outbounds"] if o.get("protocol") == "blackhole")
|
||||
ipv6_rule = {"ip": ["::/0"], "outboundTag": blocked_tag, "type": "field"}
|
||||
if ipv6_rule not in tmpl["routing"]["rules"]:
|
||||
tmpl["routing"]["rules"].insert(0, ipv6_rule)
|
||||
for rule in tmpl["routing"]["rules"]:
|
||||
if rule.get("outboundTag") == "warp-outbound":
|
||||
for t in warp_tags:
|
||||
@@ -269,7 +283,8 @@ def main():
|
||||
cur.execute("INSERT INTO settings (key, value) VALUES ('xrayTemplateConfig', ?)", (json.dumps(tmpl, ensure_ascii=False),))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(" xrayTemplateConfig written, warp rule tags:", tmpl["routing"]["rules"][1]["inboundTag"])
|
||||
print(" xrayTemplateConfig written, warp rule tags:",
|
||||
next(r["inboundTag"] for r in tmpl["routing"]["rules"] if r.get("outboundTag") == "warp-outbound"))
|
||||
|
||||
subprocess.run(["systemctl", "restart", "x-ui"], check=True)
|
||||
time.sleep(6)
|
||||
|
||||
@@ -52,7 +52,7 @@ for tag, pvar in (("hy2-direct", "PORT_HY2_DIRECT"), ("hy2-warp", "PORT_HY2_WARP
|
||||
p = port(pvar)
|
||||
if not p:
|
||||
continue
|
||||
ib = {"type": "hysteria2", "tag": tag, "listen": "::", "listen_port": p,
|
||||
ib = {"type": "hysteria2", "tag": tag, "listen": "0.0.0.0", "listen_port": p,
|
||||
"up_mbps": up, "down_mbps": down,
|
||||
"users": [{"name": "node-main", "password": os.environ["NODE_PASSWORD"]}],
|
||||
"tls": tls()}
|
||||
@@ -67,7 +67,7 @@ for tag, pvar in (("tuic-direct", "PORT_TUIC_DIRECT"), ("tuic-warp", "PORT_TUIC_
|
||||
if not p:
|
||||
continue
|
||||
inbounds.append({
|
||||
"type": "tuic", "tag": tag, "listen": "::", "listen_port": p,
|
||||
"type": "tuic", "tag": tag, "listen": "0.0.0.0", "listen_port": p,
|
||||
"users": [{"name": "node-main", "uuid": os.environ["XUI_UUID"],
|
||||
"password": os.environ["NODE_PASSWORD"]}],
|
||||
"congestion_control": tuic_cc, "auth_timeout": "3s",
|
||||
@@ -79,7 +79,10 @@ if not inbounds:
|
||||
warp_tags = [i["tag"] for i in inbounds if i["tag"].endswith("-warp")]
|
||||
direct_tags = [i["tag"] for i in inbounds if i["tag"].endswith("-direct")]
|
||||
|
||||
rules = []
|
||||
rules = [
|
||||
{"action": "resolve", "strategy": "ipv4_only", "server": "ipv4-dns"},
|
||||
{"ip_cidr": ["::/0"], "action": "reject"},
|
||||
]
|
||||
if warp_tags:
|
||||
rules.append({"inbound": warp_tags, "outbound": "warp"})
|
||||
if direct_tags:
|
||||
@@ -88,6 +91,8 @@ rules.append({"protocol": "bittorrent", "outbound": "block"})
|
||||
|
||||
cfg = {
|
||||
"log": {"level": "warn", "timestamp": True},
|
||||
"dns": {"servers": [{"type": "udp", "tag": "ipv4-dns", "server": "1.1.1.1",
|
||||
"server_port": 53}], "strategy": "ipv4_only", "final": "ipv4-dns"},
|
||||
"inbounds": inbounds,
|
||||
"outbounds": [
|
||||
{"type": "direct", "tag": "direct"},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# --- target VPS ---
|
||||
SERVER_IP="203.0.113.10" # public IPv4
|
||||
SERVER_IPV6="2001:db8::10" # public IPv6 (optional; leave empty to skip AAAA)
|
||||
SERVER_IPV6="" # fleet is IPv4-only; do not create AAAA
|
||||
SSH_USER="root" # root, or a sudo user (scripts escalate via sudo)
|
||||
SSH_KEY="$HOME/.ssh/your-vps-key.pem" # private key for the VPS
|
||||
TZ="UTC" # server timezone (e.g. UTC, Asia/Shanghai, America/Los_Angeles)
|
||||
@@ -16,7 +16,7 @@ DOMAIN_ROOT="nodeA.example.com" # this node owns *.nodeA.exampl
|
||||
# Cloudflare is the default & has a turnkey record-creation script (02-cf-dns.py).
|
||||
# Any acme.sh-supported DNS provider works for the wildcard cert — set ACME_DNS_PLUGIN and
|
||||
# the matching credential vars below. For A/AAAA records on a non-CF provider, create them
|
||||
# yourself (see references/dns-records.md — it's two names, A + AAAA, DNS-only).
|
||||
# yourself (see references/dns-records.md — two names with A records, DNS-only).
|
||||
DNS_PROVIDER="cloudflare" # informational: cloudflare | dnspod | aliyun | route53 | other
|
||||
ACME_DNS_PLUGIN="dns_cf" # acme.sh plugin: dns_cf | dns_dp | dns_ali | dns_aws | dns_gd | ...
|
||||
|
||||
|
||||
Reference in New Issue
Block a user