#!/usr/bin/env python3 """Run LOCALLY. Emits share links + QR codes + sing-box snippets for the QUIC plane (Hysteria2 / TUIC x direct/WARP). Companion to 14-generate-links-and-qr.py, which covers the six TCP inbounds. QR codes are produced offline with the local `qrencode` binary -- these carry live credentials and must never be pasted into an online QR generator. """ import json, os, re, subprocess, sys from urllib.parse import quote HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "node-links") def load_env(): env, path = {}, os.path.join(HERE, "config.env") if not os.path.exists(path): sys.exit("ERROR: config.env not found") for line in open(path): m = re.match(r'\s*([A-Z0-9_]+)\s*=\s*"?([^"#\n]*)"?', line) if m: env[m.group(1)] = m.group(2).strip() return env E = load_env() COVER = "%s.%s" % (E["SUB_COVER"], E["DOMAIN_ROOT"]) NAME = E.get("NODE_NAME", "Edge Node") PW = E["NODE_PASSWORD"] UUID = E["XUI_UUID"] OBFS = E["HY2_OBFS_PASSWORD"] CC = E.get("TUIC_CC", "bbr") os.makedirs(OUT, exist_ok=True) def write(fname, content): with open(os.path.join(OUT, fname), "w") as f: f.write(content + "\n") print(" wrote", fname) def qr(fname, data): try: subprocess.run(["qrencode", "-o", os.path.join(OUT, fname), "-s", "6", data], check=True) print(" wrote", fname) except (FileNotFoundError, subprocess.CalledProcessError): print(" (skipped %s -- install qrencode for QR codes)" % fname) def p(name): """Empty/absent port -> protocol not deployed on this node; skip it.""" return (E.get(name) or "").strip() # --- Hysteria2: hy2:// is the format sing-box / v2rayN / Shadowrocket accept --- for suffix, port in (("direct", p("PORT_HY2_DIRECT")), ("warp", p("PORT_HY2_WARP"))): if not port: continue tag = "%s-hy2-%s" % (NAME, suffix if suffix == "direct" else "warp-clean-exit") link = ("hy2://%s@%s:%s/?sni=%s&alpn=h3&obfs=salamander&obfs-password=%s#%s" % (quote(PW, safe=""), COVER, port, COVER, quote(OBFS, safe=""), quote(tag))) write("hy2-%s.link" % suffix, link) qr("hy2-%s-qr.png" % suffix, link) write("hy2-%s-singbox.json" % suffix, json.dumps({ "type": "hysteria2", "tag": tag, "server": COVER, "server_port": int(port), "password": PW, "obfs": {"type": "salamander", "password": OBFS}, "up_mbps": int(E.get("HY2_UP_MBPS", 500)), "down_mbps": int(E.get("HY2_DOWN_MBPS", 500)), "tls": {"enabled": True, "server_name": COVER, "alpn": ["h3"]}, }, indent=2)) # --- TUIC v5 --- for suffix, port in (("direct", p("PORT_TUIC_DIRECT")), ("warp", p("PORT_TUIC_WARP"))): if not port: continue tag = "%s-tuic-%s" % (NAME, suffix if suffix == "direct" else "warp-clean-exit") link = ("tuic://%s:%s@%s:%s?sni=%s&alpn=h3&congestion_control=%s&udp_relay_mode=native#%s" % (UUID, quote(PW, safe=""), COVER, port, COVER, CC, quote(tag))) write("tuic-%s.link" % suffix, link) qr("tuic-%s-qr.png" % suffix, link) write("tuic-%s-singbox.json" % suffix, json.dumps({ "type": "tuic", "tag": tag, "server": COVER, "server_port": int(port), "uuid": UUID, "password": PW, "congestion_control": CC, "udp_relay_mode": "native", "zero_rtt_handshake": False, "heartbeat": "10s", "tls": {"enabled": True, "server_name": COVER, "alpn": ["h3"]}, }, indent=2)) print("DONE — QUIC links + QR + sing-box snippets in", OUT) print("NOTE: hy2:// and tuic:// share-link formats are client-dependent; the sing-box") print(" JSON snippets are the unambiguous fallback if a client rejects a link.")