Add mainland China geo gate (allow Shanghai/overseas).
Nginx auth_request plus MaxMind City lookup blocks non-Shanghai mainland CN with a fixed notice page for sparkmessenger/sparkmsg (and deploy helpers for the shared Singapore host). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cc3085050c
commit
94dd2d819c
38
deploy/geo-guard/install-host.sh
Normal file
38
deploy/geo-guard/install-host.sh
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install / update wyrs-geo-guard on the Singapore ECS (run on host or via SSH).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
DST=/opt/wyrs-geo-guard
|
||||
|
||||
need_pkg() {
|
||||
if ! rpm -q "$1" >/dev/null 2>&1; then
|
||||
dnf install -y "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
need_pkg python3-geoip2
|
||||
# maxminddb usually pulled in; ensure present
|
||||
need_pkg python3-maxminddb || true
|
||||
|
||||
if [[ ! -f /usr/share/GeoIP/GeoLite2-City.mmdb ]]; then
|
||||
echo "missing /usr/share/GeoIP/GeoLite2-City.mmdb" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST" /etc/nginx/snippets /var/www/wyrs-geo-deny
|
||||
install -m 0755 "$ROOT/wyrs_geo_guard.py" "$DST/wyrs_geo_guard.py"
|
||||
install -m 0644 "$ROOT/wyrs-geo-guard.service" /etc/systemd/system/wyrs-geo-guard.service
|
||||
install -m 0644 "$ROOT/../nginx/snippets/wyrs-cn-geo-access.conf" /etc/nginx/snippets/wyrs-cn-geo-access.conf
|
||||
install -m 0644 "$ROOT/../nginx/static/cn-mainland-denied.html" /var/www/wyrs-geo-deny/index.html
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now wyrs-geo-guard.service
|
||||
systemctl restart wyrs-geo-guard.service
|
||||
sleep 0.3
|
||||
curl -fsS "http://127.0.0.1:18099/healthz" >/dev/null
|
||||
echo "geo-guard ok"
|
||||
# quick probes
|
||||
curl -sS -o /dev/null -w "shanghai:%{http_code}\n" "http://127.0.0.1:18099/check?ip=114.80.80.80"
|
||||
curl -sS -o /dev/null -w "hangzhou:%{http_code}\n" "http://127.0.0.1:18099/check?ip=223.5.5.5"
|
||||
curl -sS -o /dev/null -w "us:%{http_code}\n" "http://127.0.0.1:18099/check?ip=8.8.8.8"
|
||||
38
deploy/geo-guard/probe_proxy_geo.py
Normal file
38
deploy/geo-guard/probe_proxy_geo.py
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
|
||||
def http_via_proxy(sni, client_ip, path="/"):
|
||||
raw = socket.create_connection(("127.0.0.1", 8443), timeout=8)
|
||||
hdr = f"PROXY TCP4 {client_ip} 127.0.0.1 54321 8443\r\n".encode()
|
||||
raw.sendall(hdr)
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
tls = ctx.wrap_socket(raw, server_hostname=sni)
|
||||
req = (
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {sni}\r\n"
|
||||
f"Connection: close\r\n\r\n"
|
||||
).encode()
|
||||
tls.sendall(req)
|
||||
data = b""
|
||||
while True:
|
||||
chunk = tls.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
tls.close()
|
||||
head = data.split(b"\r\n\r\n", 1)[0].decode("latin1", "replace")
|
||||
status = head.split("\r\n", 1)[0]
|
||||
body = data.split(b"\r\n\r\n", 1)[1] if b"\r\n\r\n" in data else b""
|
||||
snippet = body[:160].decode("utf-8", "replace").replace("\n", " ")
|
||||
print(f"{sni} ip={client_ip} -> {status} | {snippet!r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for sni in ("sparkmsg.wyrs365.com", "www.fullstorynovel.com"):
|
||||
http_via_proxy(sni, "114.80.80.80") # Shanghai
|
||||
http_via_proxy(sni, "223.5.5.5") # Hangzhou
|
||||
http_via_proxy(sni, "8.8.8.8") # US
|
||||
17
deploy/geo-guard/wyrs-geo-guard.service
Normal file
17
deploy/geo-guard/wyrs-geo-guard.service
Normal file
@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Wyrs CN geo guard (mainland except Shanghai)
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=GEOIP_CITY_DB=/usr/share/GeoIP/GeoLite2-City.mmdb
|
||||
Environment=WYRS_GEO_LISTEN=127.0.0.1
|
||||
Environment=WYRS_GEO_PORT=18099
|
||||
ExecStart=/usr/bin/python3 /opt/wyrs-geo-guard/wyrs_geo_guard.py
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
136
deploy/geo-guard/wyrs_geo_guard.py
Normal file
136
deploy/geo-guard/wyrs_geo_guard.py
Normal file
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local auth_request backend: allow overseas + Shanghai; block other mainland CN."""
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import sys
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import geoip2.database
|
||||
from geoip2.errors import AddressNotFoundError
|
||||
|
||||
DB_PATH = os.environ.get("GEOIP_CITY_DB", "/usr/share/GeoIP/GeoLite2-City.mmdb")
|
||||
LISTEN = os.environ.get("WYRS_GEO_LISTEN", "127.0.0.1")
|
||||
PORT = int(os.environ.get("WYRS_GEO_PORT", "18099"))
|
||||
|
||||
_reader = None # type: Optional[geoip2.database.Reader]
|
||||
|
||||
|
||||
def get_reader():
|
||||
global _reader
|
||||
if _reader is None:
|
||||
_reader = geoip2.database.Reader(DB_PATH)
|
||||
return _reader
|
||||
|
||||
|
||||
def client_ip(handler):
|
||||
# Prefer query ?ip= for local probes; production uses X-Real-IP from nginx.
|
||||
qs = parse_qs(urlparse(handler.path).query)
|
||||
if "ip" in qs and qs["ip"]:
|
||||
return qs["ip"][0].strip()
|
||||
xri = handler.headers.get("X-Real-IP", "").strip()
|
||||
if xri:
|
||||
return xri.split(",")[0].strip()
|
||||
return handler.client_address[0]
|
||||
|
||||
|
||||
def is_trusted_local(ip):
|
||||
try:
|
||||
addr = ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return True
|
||||
return bool(
|
||||
addr.is_private
|
||||
or addr.is_loopback
|
||||
or addr.is_link_local
|
||||
or addr.is_reserved
|
||||
)
|
||||
|
||||
|
||||
def is_shanghai_mainland(ip):
|
||||
"""Return True if Shanghai, False if other mainland CN, None if not mainland / unknown."""
|
||||
try:
|
||||
rec = get_reader().city(ip)
|
||||
except AddressNotFoundError:
|
||||
return None
|
||||
except Exception as exc: # keep site up if DB quirks
|
||||
sys.stderr.write("geo lookup error ip=%s: %s\n" % (ip, exc))
|
||||
return None
|
||||
|
||||
cc = (rec.country.iso_code or "").upper()
|
||||
if cc != "CN":
|
||||
return None
|
||||
|
||||
for sub in rec.subdivisions:
|
||||
iso = (sub.iso_code or "").upper()
|
||||
name = (sub.name or "").strip().lower()
|
||||
if iso == "SH" or name == "shanghai":
|
||||
return True
|
||||
|
||||
city = (rec.city.name or "").strip().lower()
|
||||
if city == "shanghai":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def allowed(ip):
|
||||
if is_trusted_local(ip):
|
||||
return True
|
||||
decision = is_shanghai_mainland(ip)
|
||||
# None = overseas / HK-MO-TW / unknown → allow
|
||||
# True = Shanghai → allow
|
||||
# False = other mainland → deny
|
||||
return decision is not False
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None: # noqa: A003
|
||||
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = self.path.split("?", 1)[0]
|
||||
if path not in ("/check", "/healthz"):
|
||||
self.send_response(404)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if path == "/healthz":
|
||||
body = b"ok\n"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
|
||||
ip = client_ip(self)
|
||||
if allowed(ip):
|
||||
self.send_response(204)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("X-Wyrs-Geo", "allow")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
self.send_response(403)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("X-Wyrs-Geo", "deny-cn")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.isfile(DB_PATH):
|
||||
print(f"missing GeoIP DB: {DB_PATH}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
get_reader()
|
||||
httpd = ThreadingHTTPServer((LISTEN, PORT), Handler)
|
||||
print(f"wyrs-geo-guard listening on {LISTEN}:{PORT} db={DB_PATH}", flush=True)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -19,6 +19,8 @@ On this ECS, `:443` is shared with other wyrs HTTPS sites. Do **not** replace `/
|
||||
- `host/sparkr.conf` → `/etc/nginx/conf.d/` (RustFS S3; DNS A record `sparkr.wyrs365.com` → this host)
|
||||
- `host/sparkmessenger.conf` → `/etc/nginx/conf.d/`(官网;`sparkmessenger.wyrs365.com` / `sparkmsg.wyrs365.com`)
|
||||
- `host/spark-sni.conf` → `/etc/nginx/stream.d/`
|
||||
- `snippets/wyrs-cn-geo-access.conf` → `/etc/nginx/snippets/`(大陆除上海拦截;依赖 `wyrs-geo-guard`)
|
||||
- `deploy/geo-guard/` → 本机 `127.0.0.1:18099` MaxMind City 鉴权(海外+上海放行)
|
||||
|
||||
官网静态文件在 `/var/www/sparkmessenger.wyrs365.com`,部署:
|
||||
|
||||
@ -26,6 +28,12 @@ On this ECS, `:443` is shared with other wyrs HTTPS sites. Do **not** replace `/
|
||||
bash deploy/website/deploy.sh
|
||||
```
|
||||
|
||||
大陆地区访问控制(官网 + 同机 `fullstorynovel.com`):
|
||||
|
||||
- GeoIP:`/usr/share/GeoIP/GeoLite2-City.mmdb`(subdivision `SH` / city Shanghai 视为上海)
|
||||
- 服务:`systemctl status wyrs-geo-guard`
|
||||
- 拒绝页:`/var/www/wyrs-geo-deny/index.html`(文案:按照相关部门规定大陆地区禁止访问)
|
||||
|
||||
Existing `listen 443 ssl` vhosts move to `127.0.0.1:8443 ssl proxy_protocol` so SNI can take public 443. `00-spark-http.conf` sets `real_ip_header proxy_protocol` and `port_in_redirect off` so `$remote_addr` is the client and redirects stay on 443.
|
||||
|
||||
spark-server (Docker):
|
||||
|
||||
25
deploy/nginx/snippets/wyrs-cn-geo-access.conf
Normal file
25
deploy/nginx/snippets/wyrs-cn-geo-access.conf
Normal file
@ -0,0 +1,25 @@
|
||||
# Include inside HTTPS server{} for public sites.
|
||||
# Rule: overseas + Shanghai allow; other mainland CN → 403 Chinese notice.
|
||||
# Requires: systemd wyrs-geo-guard on 127.0.0.1:18099
|
||||
|
||||
auth_request /_wyrs_geo_check;
|
||||
error_page 403 /wyrs-cn-denied.html;
|
||||
|
||||
location = /_wyrs_geo_check {
|
||||
internal;
|
||||
auth_request off;
|
||||
proxy_pass http://127.0.0.1:18099/check;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_connect_timeout 100ms;
|
||||
proxy_read_timeout 100ms;
|
||||
}
|
||||
|
||||
location = /wyrs-cn-denied.html {
|
||||
internal;
|
||||
auth_request off;
|
||||
default_type text/html;
|
||||
charset utf-8;
|
||||
alias /var/www/wyrs-geo-deny/index.html;
|
||||
}
|
||||
37
deploy/nginx/static/cn-mainland-denied.html
Normal file
37
deploy/nginx/static/cn-mainland-denied.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<title>访问受限</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0f1419;
|
||||
color: #e7ecf3;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
main {
|
||||
max-width: 28rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
color: #c5ced8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p>按照相关部门规定大陆地区禁止访问</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@ -36,11 +36,34 @@ if [[ ! -f "$SITE_SRC/index.html" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> upload site + nginx conf"
|
||||
"${SSH[@]}" "mkdir -p '$SITE_DST' /var/www/certbot /tmp/spark-nginx-host"
|
||||
echo "==> upload site + nginx conf + geo-guard"
|
||||
"${SSH[@]}" "mkdir -p '$SITE_DST' /var/www/certbot /tmp/spark-nginx-host /tmp/spark-geo-guard"
|
||||
rsync -az --delete -e "ssh -i $KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new" \
|
||||
"$SITE_SRC/" "$HOST:$SITE_DST/"
|
||||
"${SCP[@]}" "$ROOT/deploy/nginx/host/sparkmessenger.conf" "$HOST:/tmp/spark-nginx-host/sparkmessenger.conf"
|
||||
"${SCP[@]}" \
|
||||
"$ROOT/deploy/geo-guard/wyrs_geo_guard.py" \
|
||||
"$ROOT/deploy/geo-guard/wyrs-geo-guard.service" \
|
||||
"$ROOT/deploy/geo-guard/install-host.sh" \
|
||||
"$ROOT/deploy/nginx/snippets/wyrs-cn-geo-access.conf" \
|
||||
"$ROOT/deploy/nginx/static/cn-mainland-denied.html" \
|
||||
"$HOST:/tmp/spark-geo-guard/"
|
||||
"${SSH[@]}" bash -s <<'GEO'
|
||||
set -euo pipefail
|
||||
mkdir -p /opt/wyrs-geo-guard /etc/nginx/snippets /var/www/wyrs-geo-deny
|
||||
# install-host.sh expects repo-relative paths; invoke pieces here
|
||||
if ! rpm -q python3-geoip2 >/dev/null 2>&1; then
|
||||
dnf install -y python3-geoip2
|
||||
fi
|
||||
install -m 0755 /tmp/spark-geo-guard/wyrs_geo_guard.py /opt/wyrs-geo-guard/wyrs_geo_guard.py
|
||||
install -m 0644 /tmp/spark-geo-guard/wyrs-geo-guard.service /etc/systemd/system/wyrs-geo-guard.service
|
||||
install -m 0644 /tmp/spark-geo-guard/wyrs-cn-geo-access.conf /etc/nginx/snippets/wyrs-cn-geo-access.conf
|
||||
install -m 0644 /tmp/spark-geo-guard/cn-mainland-denied.html /var/www/wyrs-geo-deny/index.html
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now wyrs-geo-guard.service
|
||||
systemctl restart wyrs-geo-guard.service
|
||||
curl -fsS http://127.0.0.1:18099/healthz >/dev/null
|
||||
GEO
|
||||
|
||||
echo "==> ensure TLS certificates"
|
||||
"${SSH[@]}" bash -s -- "$PRIMARY" "${DOMAINS[@]}" <<'REMOTE'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user