mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-09 01:36:52 +08:00
feat: enable full IPv6/dual-stack support for networking, proxy binding, and web management interface
This commit is contained in:
+78
-33
@@ -207,7 +207,7 @@ def generate_random_suffix():
|
|||||||
def load_ui_cfg():
|
def load_ui_cfg():
|
||||||
import json
|
import json
|
||||||
path = "/opt/aimilivpn/vpngate_data/ui_auth.json"
|
path = "/opt/aimilivpn/vpngate_data/ui_auth.json"
|
||||||
cfg = {"host": "0.0.0.0", "port": 8787, "secret_path": "EJsW2EeBo9lY", "password": ""}
|
cfg = {"host": "::", "port": 8787, "secret_path": "EJsW2EeBo9lY", "password": ""}
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
@@ -295,31 +295,35 @@ def get_public_ip():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
import urllib.request
|
import urllib.request
|
||||||
try:
|
# Try dual-stack first, then IPv6-only, then IPv4-only
|
||||||
req = urllib.request.Request("https://api.ipify.org", headers={"User-Agent": "curl/7.68.0"})
|
for api_url in ["https://api64.ipify.org", "https://api6.ipify.org", "https://api.ipify.org"]:
|
||||||
with urllib.request.urlopen(req, timeout=1.5) as r:
|
try:
|
||||||
ip = r.read().decode().strip()
|
req = urllib.request.Request(api_url, headers={"User-Agent": "curl/7.68.0"})
|
||||||
if ip:
|
with urllib.request.urlopen(req, timeout=2) as r:
|
||||||
try:
|
ip = r.read().decode().strip()
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
if ip:
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
try:
|
||||||
f.write(ip)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
except Exception:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
pass
|
f.write(ip)
|
||||||
return ip
|
except Exception:
|
||||||
except Exception:
|
pass
|
||||||
pass
|
return ip
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return "您的服务器公网IP"
|
return "您的服务器公网IP"
|
||||||
|
|
||||||
def check_port_listening(port):
|
def check_port_listening(port):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
for host, family in [("127.0.0.1", socket.AF_INET), ("::1", socket.AF_INET6)]:
|
||||||
s.settimeout(0.2)
|
try:
|
||||||
try:
|
s = socket.socket(family, socket.SOCK_STREAM)
|
||||||
s.connect(("127.0.0.1", port))
|
s.settimeout(0.2)
|
||||||
s.close()
|
s.connect((host, port))
|
||||||
return True
|
s.close()
|
||||||
except Exception:
|
return True
|
||||||
return False
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def get_service_pid(service_name="aimilivpn.service"):
|
def get_service_pid(service_name="aimilivpn.service"):
|
||||||
try:
|
try:
|
||||||
@@ -413,7 +417,15 @@ def print_status():
|
|||||||
print_line(format_line(f"管理后台 (Port {ui_port})", backend_status))
|
print_line(format_line(f"管理后台 (Port {ui_port})", backend_status))
|
||||||
print_line(format_line("连接核心 (OpenVPN)", openvpn_status))
|
print_line(format_line("连接核心 (OpenVPN)", openvpn_status))
|
||||||
|
|
||||||
login_ip = "127.0.0.1" if cfg.get("host") == "127.0.0.1" else get_public_ip()
|
host_cfg = cfg.get("host", "::")
|
||||||
|
if host_cfg in ("127.0.0.1", "localhost"):
|
||||||
|
login_ip = "127.0.0.1"
|
||||||
|
elif host_cfg == "::1":
|
||||||
|
login_ip = "[::1]"
|
||||||
|
elif host_cfg == "::":
|
||||||
|
login_ip = get_public_ip()
|
||||||
|
else:
|
||||||
|
login_ip = f"[{host_cfg}]" if ":" in host_cfg else host_cfg
|
||||||
print_line(format_line("网页登录地址", f"{yellow}http://{login_ip}:{ui_port}/{secret_path}/{reset}"))
|
print_line(format_line("网页登录地址", f"{yellow}http://{login_ip}:{ui_port}/{secret_path}/{reset}"))
|
||||||
print_line(format_line("网页管理账号", cfg.get("username", "未配置")))
|
print_line(format_line("网页管理账号", cfg.get("username", "未配置")))
|
||||||
curr_pwd = cfg.get("password", "")
|
curr_pwd = cfg.get("password", "")
|
||||||
@@ -441,9 +453,26 @@ def print_status():
|
|||||||
else:
|
else:
|
||||||
print_line(format_line("节点状态", "无活动连接"))
|
print_line(format_line("节点状态", "无活动连接"))
|
||||||
print_line()
|
print_line()
|
||||||
|
local_proxy = state.get("local_proxy", "http://127.0.0.1:7928")
|
||||||
|
import urllib.parse
|
||||||
|
try:
|
||||||
|
parsed = urllib.parse.urlsplit(local_proxy)
|
||||||
|
proxy_host = parsed.hostname or "127.0.0.1"
|
||||||
|
proxy_port = parsed.port or 7928
|
||||||
|
except Exception:
|
||||||
|
proxy_host = "127.0.0.1"
|
||||||
|
proxy_port = 7928
|
||||||
|
|
||||||
|
if proxy_host == "::":
|
||||||
|
socks_addr = "127.0.0.1"
|
||||||
|
elif ":" in proxy_host:
|
||||||
|
socks_addr = f"[{proxy_host}]"
|
||||||
|
else:
|
||||||
|
socks_addr = proxy_host
|
||||||
|
|
||||||
print_line("【使用方法】")
|
print_line("【使用方法】")
|
||||||
print_line(f" export http_proxy=socks5://127.0.0.1:7928")
|
print_line(f" export http_proxy=socks5://{socks_addr}:{proxy_port}")
|
||||||
print_line(f" export https_proxy=socks5://127.0.0.1:7928")
|
print_line(f" export https_proxy=socks5://{socks_addr}:{proxy_port}")
|
||||||
print_line("=======================================================")
|
print_line("=======================================================")
|
||||||
|
|
||||||
def run_service_cmd(cmd):
|
def run_service_cmd(cmd):
|
||||||
@@ -593,13 +622,19 @@ def configure_web():
|
|||||||
if key == '1':
|
if key == '1':
|
||||||
print("\033[H\033[J", end="")
|
print("\033[H\033[J", end="")
|
||||||
print("选择网页登录绑定地址:")
|
print("选择网页登录绑定地址:")
|
||||||
print(" 1. 仅允许本地登录 (127.0.0.1 - 更安全)")
|
print(" 1. 仅允许本地 IPv4 登录 (127.0.0.1 - 更安全)")
|
||||||
print(" 2. 允许公网IP登录 (0.0.0.0 - 方便远程)")
|
print(" 2. 允许 IPv4 公网登录 (0.0.0.0)")
|
||||||
sel = input("请选择 (1 或 2, 默认2): ").strip()
|
print(" 3. 允许 IPv4 & IPv6 双栈公网登录 (:: - 推荐)")
|
||||||
|
print(" 4. 仅允许本地 IPv6 登录 (::1)")
|
||||||
|
sel = input("请选择 (1/2/3/4, 默认3): ").strip()
|
||||||
if sel == '1':
|
if sel == '1':
|
||||||
cfg['host'] = "127.0.0.1"
|
cfg['host'] = "127.0.0.1"
|
||||||
else:
|
elif sel == '2':
|
||||||
cfg['host'] = "0.0.0.0"
|
cfg['host'] = "0.0.0.0"
|
||||||
|
elif sel == '4':
|
||||||
|
cfg['host'] = "::1"
|
||||||
|
else:
|
||||||
|
cfg['host'] = "::"
|
||||||
save_ui_cfg(cfg)
|
save_ui_cfg(cfg)
|
||||||
print(f"绑定地址已更新为: {cfg['host']}")
|
print(f"绑定地址已更新为: {cfg['host']}")
|
||||||
ask_restart()
|
ask_restart()
|
||||||
@@ -611,7 +646,10 @@ def configure_web():
|
|||||||
save_ui_cfg(cfg)
|
save_ui_cfg(cfg)
|
||||||
print("安全登录后缀已随机重置成功!")
|
print("安全登录后缀已随机重置成功!")
|
||||||
print(f"您的全新安全登录后缀为: {new_path}")
|
print(f"您的全新安全登录后缀为: {new_path}")
|
||||||
print(f"新的访问路径为: http://{cfg['host']}:{cfg['port']}/{new_path}/")
|
display_host = cfg['host']
|
||||||
|
if ":" in display_host:
|
||||||
|
display_host = f"[{display_host}]"
|
||||||
|
print(f"新的访问路径为: http://{display_host}:{cfg['port']}/{new_path}/")
|
||||||
ask_restart()
|
ask_restart()
|
||||||
break
|
break
|
||||||
elif key == '3' or key == 'q' or key == '\x03':
|
elif key == '3' or key == 'q' or key == '\x03':
|
||||||
@@ -970,7 +1008,7 @@ while True:
|
|||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json
|
||||||
cfg = {
|
cfg = {
|
||||||
'host': '0.0.0.0',
|
'host': '::',
|
||||||
'port': int('$UI_PORT'),
|
'port': int('$UI_PORT'),
|
||||||
'secret_path': '$SECRET_PATH',
|
'secret_path': '$SECRET_PATH',
|
||||||
'username': '$UI_USERNAME',
|
'username': '$UI_USERNAME',
|
||||||
@@ -1041,13 +1079,20 @@ echo -e "正在获取 VPS 公网 IP..."
|
|||||||
PUBLIC_IP=$(curl -s --max-time 3 https://api.ipify.org || curl -s --max-time 3 https://ifconfig.me || curl -s --max-time 3 icanhazip.com || echo "您的服务器公网IP")
|
PUBLIC_IP=$(curl -s --max-time 3 https://api.ipify.org || curl -s --max-time 3 https://ifconfig.me || curl -s --max-time 3 icanhazip.com || echo "您的服务器公网IP")
|
||||||
echo -n "$PUBLIC_IP" > "${INSTALL_DIR}/vpngate_data/public_ip.txt"
|
echo -n "$PUBLIC_IP" > "${INSTALL_DIR}/vpngate_data/public_ip.txt"
|
||||||
|
|
||||||
|
# Get VPS public IPv6
|
||||||
|
echo -e "正在获取 VPS 公网 IPv6..."
|
||||||
|
PUBLIC_IPV6=$(curl -6 -s --max-time 3 https://api.ipify.org || curl -6 -s --max-time 3 https://ifconfig.me || curl -6 -s --max-time 3 icanhazip.com || echo "")
|
||||||
|
|
||||||
echo -e "\n${GREEN}==========================================================${PLAIN}"
|
echo -e "\n${GREEN}==========================================================${PLAIN}"
|
||||||
echo -e "${GREEN} AimiliVPN 源码一键部署已完成!${PLAIN}"
|
echo -e "${GREEN} AimiliVPN 源码一键部署已完成!${PLAIN}"
|
||||||
echo -e "${GREEN}==========================================================${PLAIN}"
|
echo -e "${GREEN}==========================================================${PLAIN}"
|
||||||
echo -e " * 网页控制面板: ${BLUE}http://${PUBLIC_IP}:${UI_PORT}/${SECRET_PATH}/${PLAIN}"
|
echo -e " * 网页控制面板: ${BLUE}http://${PUBLIC_IP}:${UI_PORT}/${SECRET_PATH}/${PLAIN}"
|
||||||
|
if [ -n "$PUBLIC_IPV6" ]; then
|
||||||
|
echo -e " * 网页控制面板(IPv6): ${BLUE}http://[${PUBLIC_IPV6}]:${UI_PORT}/${SECRET_PATH}/${PLAIN}"
|
||||||
|
fi
|
||||||
echo -e " * 网页管理账号: ${YELLOW}${USERNAME}${PLAIN}"
|
echo -e " * 网页管理账号: ${YELLOW}${USERNAME}${PLAIN}"
|
||||||
echo -e " * 网页管理密码: ${YELLOW}${PASSWORD}${PLAIN}"
|
echo -e " * 网页管理密码: ${YELLOW}${PASSWORD}${PLAIN}"
|
||||||
echo -e " * HTTP/SOCKS5 代理端口: ${BLUE}http://127.0.0.1:7928/${PLAIN}"
|
echo -e " * HTTP/SOCKS5 代理端口: ${BLUE}http://127.0.0.1:7928/${PLAIN} 或 ${BLUE}http://[::1]:7928/${PLAIN}"
|
||||||
echo -e " --------------------------------------------------------"
|
echo -e " --------------------------------------------------------"
|
||||||
echo -e " * 快速状态指令: ${YELLOW}ml status${PLAIN} 或 ${YELLOW}ml${PLAIN}"
|
echo -e " * 快速状态指令: ${YELLOW}ml status${PLAIN} 或 ${YELLOW}ml${PLAIN}"
|
||||||
echo -e " * 查看实时日志: ${YELLOW}ml logs${PLAIN}"
|
echo -e " * 查看实时日志: ${YELLOW}ml logs${PLAIN}"
|
||||||
|
|||||||
+47
-6
@@ -28,6 +28,11 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
|
|||||||
return host
|
return host
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, host)
|
||||||
|
return host
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
import random
|
import random
|
||||||
tx_id = random.getrandbits(16).to_bytes(2, "big")
|
tx_id = random.getrandbits(16).to_bytes(2, "big")
|
||||||
@@ -261,18 +266,54 @@ def proxy_client(client: socket.socket, address: tuple[str, int]) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def start_proxy_server(host: str, port: int) -> None:
|
def start_proxy_server(host: str, port: int) -> None:
|
||||||
|
is_ipv6 = ":" in host or host == ""
|
||||||
|
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||||
try:
|
try:
|
||||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
server = socket.socket(af, socket.SOCK_STREAM)
|
||||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
if is_ipv6:
|
||||||
|
try:
|
||||||
|
server.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
server.bind((host, port))
|
server.bind((host, port))
|
||||||
server.listen(256)
|
server.listen(256)
|
||||||
print(f"HTTP/SOCKS5 proxy listening on {host}:{port}", flush=True)
|
print(f"HTTP/SOCKS5 proxy listening on {host}:{port}", flush=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import vpn_utils
|
if is_ipv6 and host == "::":
|
||||||
diag = vpn_utils.diagnose_local_obstructions(port)
|
print(f"[警告] 绑定 IPv6 {host}:{port} 失败 ({e}),正在尝试回退至 IPv4 0.0.0.0 ...", flush=True)
|
||||||
diag_msg = diag[1] if diag else str(e)
|
try:
|
||||||
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on {host}:{port}: {diag_msg}", flush=True)
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
return
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server.bind(("0.0.0.0", port))
|
||||||
|
server.listen(256)
|
||||||
|
print(f"HTTP/SOCKS5 proxy listening on 0.0.0.0:{port} (仅 IPv4)", flush=True)
|
||||||
|
except Exception as ex:
|
||||||
|
import vpn_utils
|
||||||
|
diag = vpn_utils.diagnose_local_obstructions(port, host="0.0.0.0")
|
||||||
|
diag_msg = diag[1] if diag else str(ex)
|
||||||
|
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on 0.0.0.0:{port}: {diag_msg}", flush=True)
|
||||||
|
return
|
||||||
|
elif is_ipv6 and host == "::1":
|
||||||
|
print(f"[警告] 绑定 IPv6 {host}:{port} 失败 ({e}),正在尝试回退至 IPv4 127.0.0.1 ...", flush=True)
|
||||||
|
try:
|
||||||
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server.bind(("127.0.0.1", port))
|
||||||
|
server.listen(256)
|
||||||
|
print(f"HTTP/SOCKS5 proxy listening on 127.0.0.1:{port} (仅 IPv4)", flush=True)
|
||||||
|
except Exception as ex:
|
||||||
|
import vpn_utils
|
||||||
|
diag = vpn_utils.diagnose_local_obstructions(port, host="127.0.0.1")
|
||||||
|
diag_msg = diag[1] if diag else str(ex)
|
||||||
|
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on 127.0.0.1:{port}: {diag_msg}", flush=True)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
import vpn_utils
|
||||||
|
diag = vpn_utils.diagnose_local_obstructions(port, host=host)
|
||||||
|
diag_msg = diag[1] if diag else str(e)
|
||||||
|
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on {host}:{port}: {diag_msg}", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+40
-18
@@ -194,7 +194,9 @@ def get_physical_interface() -> str | None:
|
|||||||
|
|
||||||
def tcp_latency_ms(host: str, port: int, dev: str | None = None) -> int:
|
def tcp_latency_ms(host: str, port: int, dev: str | None = None) -> int:
|
||||||
started = time.time()
|
started = time.time()
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
# Auto-detect address family based on host address
|
||||||
|
af = socket.AF_INET6 if ":" in host else socket.AF_INET
|
||||||
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
s.settimeout(5)
|
s.settimeout(5)
|
||||||
if dev:
|
if dev:
|
||||||
@@ -265,21 +267,29 @@ def ping_latency_ms(host: str, port: int, fallback_ping: int = 0) -> int:
|
|||||||
|
|
||||||
def check_and_fix_dns() -> None:
|
def check_and_fix_dns() -> None:
|
||||||
"""
|
"""
|
||||||
Checks if DNS resolution is broken in WSL.
|
Checks if DNS resolution is broken.
|
||||||
If names fail but direct IP connections work, appends public DNS nameservers to /etc/resolv.conf.
|
If names fail but direct IP connections work, appends public DNS nameservers to /etc/resolv.conf.
|
||||||
|
Supports both IPv4 and IPv6 network environments.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
socket.gethostbyname("www.vpngate.net")
|
socket.getaddrinfo("www.vpngate.net", 443)
|
||||||
return
|
return
|
||||||
except socket.gaierror:
|
except (socket.gaierror, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
network_ok = False
|
network_ok = False
|
||||||
for ip in ["8.8.8.8", "1.1.1.1"]:
|
# Test IPv4 DNS servers first, then IPv6
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
dns_targets = [
|
||||||
|
("8.8.8.8", 53, socket.AF_INET),
|
||||||
|
("1.1.1.1", 53, socket.AF_INET),
|
||||||
|
("2001:4860:4860::8888", 53, socket.AF_INET6),
|
||||||
|
("2606:4700:4700::1111", 53, socket.AF_INET6),
|
||||||
|
]
|
||||||
|
for ip, port, af in dns_targets:
|
||||||
|
s = socket.socket(af, socket.SOCK_DGRAM)
|
||||||
try:
|
try:
|
||||||
s.settimeout(2)
|
s.settimeout(2)
|
||||||
s.connect((ip, 53))
|
s.connect((ip, port))
|
||||||
network_ok = True
|
network_ok = True
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -435,7 +445,7 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
|||||||
dns_ok = False
|
dns_ok = False
|
||||||
for test_domain in ["api.ipify.org", "dns.google", "one.one.one.one"]:
|
for test_domain in ["api.ipify.org", "dns.google", "one.one.one.one"]:
|
||||||
try:
|
try:
|
||||||
socket.gethostbyname(test_domain)
|
socket.getaddrinfo(test_domain, 443)
|
||||||
dns_ok = True
|
dns_ok = True
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -443,10 +453,12 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
|||||||
|
|
||||||
# 2. 检查是否能解析 API 域名
|
# 2. 检查是否能解析 API 域名
|
||||||
api_dns_ok = False
|
api_dns_ok = False
|
||||||
api_ip = None
|
api_addr = None # (af, ip) tuple
|
||||||
try:
|
try:
|
||||||
api_ip = socket.gethostbyname(domain)
|
results = socket.getaddrinfo(domain, port, 0, socket.SOCK_STREAM)
|
||||||
api_dns_ok = True
|
if results:
|
||||||
|
api_dns_ok = True
|
||||||
|
api_addr = (results[0][0], results[0][4][0]) # (address_family, ip)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -458,7 +470,8 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
|||||||
|
|
||||||
# 3. 检查 TCP 连接 API 域名
|
# 3. 检查 TCP 连接 API 域名
|
||||||
api_conn_ok = False
|
api_conn_ok = False
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
api_af, api_ip = api_addr
|
||||||
|
s = socket.socket(api_af, socket.SOCK_STREAM)
|
||||||
s.settimeout(4)
|
s.settimeout(4)
|
||||||
try:
|
try:
|
||||||
s.connect((api_ip, port))
|
s.connect((api_ip, port))
|
||||||
@@ -473,8 +486,15 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
|||||||
|
|
||||||
if not api_conn_ok:
|
if not api_conn_ok:
|
||||||
ext_conn_ok = False
|
ext_conn_ok = False
|
||||||
for test_ip, test_port in [("8.8.8.8", 53), ("1.1.1.1", 53)]:
|
# Test both IPv4 and IPv6 external connectivity
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
ext_targets = [
|
||||||
|
("8.8.8.8", 53, socket.AF_INET),
|
||||||
|
("1.1.1.1", 53, socket.AF_INET),
|
||||||
|
("2001:4860:4860::8888", 53, socket.AF_INET6),
|
||||||
|
("2606:4700:4700::1111", 53, socket.AF_INET6),
|
||||||
|
]
|
||||||
|
for test_ip, test_port, af in ext_targets:
|
||||||
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
s.settimeout(3)
|
s.settimeout(3)
|
||||||
try:
|
try:
|
||||||
s.connect((test_ip, test_port))
|
s.connect((test_ip, test_port))
|
||||||
@@ -490,7 +510,7 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
|||||||
if ext_conn_ok:
|
if ext_conn_ok:
|
||||||
return 1008, f"[ERR_API_IP_BLOCKED_OR_DOWN] 连接 API 服务器失败。原因: 外部网络连接通畅,但无法建立到 {domain} ({api_ip}:{port}) 的连接,可能是由于官方 IP 遭 GFW/防火墙 IP 阻断封锁或官方服务器宕机。"
|
return 1008, f"[ERR_API_IP_BLOCKED_OR_DOWN] 连接 API 服务器失败。原因: 外部网络连接通畅,但无法建立到 {domain} ({api_ip}:{port}) 的连接,可能是由于官方 IP 遭 GFW/防火墙 IP 阻断封锁或官方服务器宕机。"
|
||||||
else:
|
else:
|
||||||
return 1009, "[ERR_VPS_OUTBOUND_BLOCKED] VPS 完全断网。原因: 任何外部测试连接均失败,请检查 VPS 网卡和宿主机连接。"
|
return 1009, "[ERR_VPS_OUTBOUND_BLOCKED] VPS 完全断网。原因: 任何外部测试连接均失败(IPv4 和 IPv6 均不可达),请检查 VPS 网卡和宿主机连接。"
|
||||||
|
|
||||||
return 1010, f"[ERR_API_TLS_INTERFERENCE] HTTPS/TLS 握手被干扰。原因: 可以建立 TCP 连接但请求超时,通常是由于防火墙通过 SNI 阻断了 TLS 握手流。"
|
return 1010, f"[ERR_API_TLS_INTERFERENCE] HTTPS/TLS 握手被干扰。原因: 可以建立 TCP 连接但请求超时,通常是由于防火墙通过 SNI 阻断了 TLS 握手流。"
|
||||||
|
|
||||||
@@ -524,13 +544,15 @@ def diagnose_openvpn_failure(log_tail: list[str]) -> tuple[int, str]:
|
|||||||
return 2010, "[ERR_OVPN_UNKNOWN] OpenVPN 其他运行时异常。原因: 连接握手期间发生其他协议错误,详细信息请查看日志尾部。"
|
return 2010, "[ERR_OVPN_UNKNOWN] OpenVPN 其他运行时异常。原因: 连接握手期间发生其他协议错误,详细信息请查看日志尾部。"
|
||||||
|
|
||||||
|
|
||||||
def diagnose_local_obstructions(proxy_port: int = 7928) -> tuple[int, str] | None:
|
def diagnose_local_obstructions(proxy_port: int = 7928, host: str = "127.0.0.1") -> tuple[int, str] | None:
|
||||||
import sys
|
import sys
|
||||||
# 1. 检查端口是否被占用
|
# 1. 检查端口是否被占用
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
is_ipv6 = ":" in host or host == ""
|
||||||
|
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||||
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
try:
|
try:
|
||||||
s.bind(("127.0.0.1", proxy_port))
|
s.bind((host, proxy_port))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == 98 or e.errno == 10048 or "already in use" in str(e).lower():
|
if e.errno == 98 or e.errno == 10048 or "already in use" in str(e).lower():
|
||||||
return 3005, f"[ERR_PORT_IN_USE] 本地代理端口 {proxy_port} 被占用。原因: 其他进程已抢占该端口,导致本系统代理网关启动失败。请运行 'lsof -i :{proxy_port}' 检查占用进程。"
|
return 3005, f"[ERR_PORT_IN_USE] 本地代理端口 {proxy_port} 被占用。原因: 其他进程已抢占该端口,导致本系统代理网关启动失败。请运行 'lsof -i :{proxy_port}' 检查占用进程。"
|
||||||
|
|||||||
+86
-20
@@ -23,14 +23,57 @@ import concurrent.futures
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Force socket to resolve IPv4 only to avoid slow AAAA (IPv6) DNS resolution timeouts (e.g. in WSL)
|
# Prefer IPv4 resolution to avoid slow AAAA DNS timeouts (e.g. in WSL),
|
||||||
|
# but fall back to system default (IPv6) if IPv4 resolution fails.
|
||||||
|
# This ensures pure-IPv6 VPS (with NAT64/clatd) can still function.
|
||||||
_orig_getaddrinfo = socket.getaddrinfo
|
_orig_getaddrinfo = socket.getaddrinfo
|
||||||
def _ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
def _ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
||||||
if family == 0:
|
if family == 0:
|
||||||
family = socket.AF_INET
|
if isinstance(host, str) and ":" in host:
|
||||||
|
return _orig_getaddrinfo(host, port, socket.AF_INET6, type, proto, flags)
|
||||||
|
# Try IPv4 first for speed; fall back to system default (allows IPv6/NAT64)
|
||||||
|
try:
|
||||||
|
results = _orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
|
||||||
|
if results:
|
||||||
|
return results
|
||||||
|
except socket.gaierror:
|
||||||
|
pass
|
||||||
|
return _orig_getaddrinfo(host, port, 0, type, proto, flags)
|
||||||
return _orig_getaddrinfo(host, port, family, type, proto, flags)
|
return _orig_getaddrinfo(host, port, family, type, proto, flags)
|
||||||
socket.getaddrinfo = _ipv4_getaddrinfo
|
socket.getaddrinfo = _ipv4_getaddrinfo
|
||||||
|
|
||||||
|
class DualStackHTTPServer(ThreadingHTTPServer):
|
||||||
|
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
|
||||||
|
host, port = server_address
|
||||||
|
if ":" in host or host == "":
|
||||||
|
self.address_family = socket.AF_INET6
|
||||||
|
else:
|
||||||
|
self.address_family = socket.AF_INET
|
||||||
|
|
||||||
|
try:
|
||||||
|
super().__init__(server_address, RequestHandlerClass, bind_and_activate)
|
||||||
|
except OSError as e:
|
||||||
|
if self.address_family == socket.AF_INET6:
|
||||||
|
fallback_host = "0.0.0.0" if host in ("::", "") else "127.0.0.1"
|
||||||
|
print(f"[警告] 绑定 Web 管理后台 IPv6 {host}:{port} 失败 ({e}),正在尝试回退至 IPv4 {fallback_host} ...", flush=True)
|
||||||
|
# 关闭第一次失败时可能已创建的 socket
|
||||||
|
try:
|
||||||
|
self.socket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.address_family = socket.AF_INET
|
||||||
|
super().__init__((fallback_host, port), RequestHandlerClass, bind_and_activate)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
if self.address_family == socket.AF_INET6:
|
||||||
|
try:
|
||||||
|
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
super().server_bind()
|
||||||
|
|
||||||
import vpn_utils
|
import vpn_utils
|
||||||
import proxy_server
|
import proxy_server
|
||||||
|
|
||||||
@@ -43,9 +86,9 @@ OPENVPN_TEST_TIMEOUT_SECONDS = int(os.environ.get("OPENVPN_TEST_TIMEOUT_SECONDS"
|
|||||||
OPENVPN_CMD = os.environ.get("OPENVPN_CMD", "openvpn")
|
OPENVPN_CMD = os.environ.get("OPENVPN_CMD", "openvpn")
|
||||||
OPENVPN_AUTH_USER = os.environ.get("OPENVPN_AUTH_USER", "vpn")
|
OPENVPN_AUTH_USER = os.environ.get("OPENVPN_AUTH_USER", "vpn")
|
||||||
OPENVPN_AUTH_PASS = os.environ.get("OPENVPN_AUTH_PASS", "vpn")
|
OPENVPN_AUTH_PASS = os.environ.get("OPENVPN_AUTH_PASS", "vpn")
|
||||||
LOCAL_PROXY_HOST = os.environ.get("LOCAL_PROXY_HOST", "127.0.0.1")
|
LOCAL_PROXY_HOST = os.environ.get("LOCAL_PROXY_HOST", "::")
|
||||||
LOCAL_PROXY_PORT = int(os.environ.get("LOCAL_PROXY_PORT", "7928"))
|
LOCAL_PROXY_PORT = int(os.environ.get("LOCAL_PROXY_PORT", "7928"))
|
||||||
UI_HOST = os.environ.get("UI_HOST", "0.0.0.0")
|
UI_HOST = os.environ.get("UI_HOST", "::")
|
||||||
UI_PORT = int(os.environ.get("UI_PORT", "8787"))
|
UI_PORT = int(os.environ.get("UI_PORT", "8787"))
|
||||||
INVALID_BACKOFF_SECONDS = int(os.environ.get("INVALID_BACKOFF_SECONDS", str(30 * 60)))
|
INVALID_BACKOFF_SECONDS = int(os.environ.get("INVALID_BACKOFF_SECONDS", str(30 * 60)))
|
||||||
|
|
||||||
@@ -122,7 +165,7 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
"username": "",
|
"username": "",
|
||||||
"secret_path": "EJsW2EeBo9lY",
|
"secret_path": "EJsW2EeBo9lY",
|
||||||
"password": "",
|
"password": "",
|
||||||
"host": "0.0.0.0",
|
"host": "::",
|
||||||
"port": 8787
|
"port": 8787
|
||||||
}
|
}
|
||||||
updated = False
|
updated = False
|
||||||
@@ -214,7 +257,8 @@ def get_state() -> dict[str, Any]:
|
|||||||
state.setdefault("target_valid_nodes", TARGET_VALID_NODES)
|
state.setdefault("target_valid_nodes", TARGET_VALID_NODES)
|
||||||
state.setdefault("fetch_interval_seconds", FETCH_INTERVAL_SECONDS)
|
state.setdefault("fetch_interval_seconds", FETCH_INTERVAL_SECONDS)
|
||||||
state.setdefault("check_interval_seconds", CHECK_INTERVAL_SECONDS)
|
state.setdefault("check_interval_seconds", CHECK_INTERVAL_SECONDS)
|
||||||
state.setdefault("local_proxy", f"http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}")
|
_proxy_display = f"[{LOCAL_PROXY_HOST}]" if ":" in LOCAL_PROXY_HOST else LOCAL_PROXY_HOST
|
||||||
|
state.setdefault("local_proxy", f"http://{_proxy_display}:{LOCAL_PROXY_PORT}")
|
||||||
state.setdefault("last_fetch_status", "not_started")
|
state.setdefault("last_fetch_status", "not_started")
|
||||||
state.setdefault("last_check_message", "")
|
state.setdefault("last_check_message", "")
|
||||||
state.setdefault("blacklisted_nodes", 0)
|
state.setdefault("blacklisted_nodes", 0)
|
||||||
@@ -933,7 +977,8 @@ def connect_node(node_id: str) -> str:
|
|||||||
for item in nodes:
|
for item in nodes:
|
||||||
item["active"] = item.get("id") == node_id
|
item["active"] = item.get("id") == node_id
|
||||||
if item["active"]:
|
if item["active"]:
|
||||||
item["probe_message"] = f"Active node. HTTP proxy: http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}"
|
_ph = f"[{LOCAL_PROXY_HOST}]" if ":" in LOCAL_PROXY_HOST else LOCAL_PROXY_HOST
|
||||||
|
item["probe_message"] = f"Active node. HTTP proxy: http://{_ph}:{LOCAL_PROXY_PORT}"
|
||||||
write_json(NODES_FILE, nodes)
|
write_json(NODES_FILE, nodes)
|
||||||
|
|
||||||
set_state(last_check_message="正在测试本地代理出站联通性与出口 IP...")
|
set_state(last_check_message="正在测试本地代理出站联通性与出口 IP...")
|
||||||
@@ -2281,7 +2326,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stat-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="color: var(--primary);"><path stroke-linecap="round" stroke-linejoin="round" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071a10.5 10.5 0 0114.14 0M1.414 8.05a16 16 0 0121.172 0" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="stat-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="color: var(--primary);"><path stroke-linecap="round" stroke-linejoin="round" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071a10.5 10.5 0 0114.14 0M1.414 8.05a16 16 0 0121.172 0" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin: 0 0 4px 0; font-size: 16px; font-weight: 600; color: var(--text-primary);">本地代理出口检测 (Port 7928)</h3>
|
<h3 style="margin: 0 0 4px 0; font-size: 16px; font-weight: 600; color: var(--text-primary);">本地代理出口检测</h3>
|
||||||
<p style="margin: 0; font-size: 13px; color: var(--text-secondary);">
|
<p style="margin: 0; font-size: 13px; color: var(--text-secondary);">
|
||||||
测试本地 HTTP/SOCKS5 代理是否成功通过当前 VPN 节点出站,并获取实际出口公网 IP 和延迟。
|
测试本地 HTTP/SOCKS5 代理是否成功通过当前 VPN 节点出站,并获取实际出口公网 IP 和延迟。
|
||||||
</p>
|
</p>
|
||||||
@@ -2696,7 +2741,8 @@ function render(){
|
|||||||
|
|
||||||
const statusMessage = state.last_check_message || "";
|
const statusMessage = state.last_check_message || "";
|
||||||
const activeNodeInfo = activeNode ? `<span class="badge available" style="margin-left:8px; padding:2px 8px;">${esc(translateCountry(activeNode.country))} (${activeNode.id})</span>` : `<span class="badge unavailable" style="margin-left:8px; padding:2px 8px;">无</span>`;
|
const activeNodeInfo = activeNode ? `<span class="badge available" style="margin-left:8px; padding:2px 8px;">${esc(translateCountry(activeNode.country))} (${activeNode.id})</span>` : `<span class="badge unavailable" style="margin-left:8px; padding:2px 8px;">无</span>`;
|
||||||
$("status").innerHTML=`<span class="status-dot"></span>HTTP 代理本地接口:http://127.0.0.1:7928 | 活动节点:${activeNodeInfo} | 状态:${statusMessage}`;
|
const localProxy = state.local_proxy || `http://127.0.0.1:${state.proxy_port || 7928}`;
|
||||||
|
$("status").innerHTML=`<span class="status-dot"></span>HTTP 代理本地接口:${localProxy} | 活动节点:${activeNodeInfo} | 状态:${statusMessage}`;
|
||||||
|
|
||||||
// Update proxy test status card based on background checks
|
// Update proxy test status card based on background checks
|
||||||
const pBadge = $("proxy_status_badge");
|
const pBadge = $("proxy_status_badge");
|
||||||
@@ -3318,12 +3364,17 @@ setInterval(async () => {
|
|||||||
|
|
||||||
def check_proxy_health() -> dict[str, Any]:
|
def check_proxy_health() -> dict[str, Any]:
|
||||||
# 1. 检测代理服务端口是否在监听
|
# 1. 检测代理服务端口是否在监听
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
is_ipv6 = ":" in LOCAL_PROXY_HOST
|
||||||
|
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||||
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
s.settimeout(1.5)
|
s.settimeout(1.5)
|
||||||
try:
|
try:
|
||||||
s.connect(("127.0.0.1", LOCAL_PROXY_PORT))
|
connect_host = LOCAL_PROXY_HOST
|
||||||
|
if connect_host in ("::", "0.0.0.0", ""):
|
||||||
|
connect_host = "::1" if is_ipv6 else "127.0.0.1"
|
||||||
|
s.connect((connect_host, LOCAL_PROXY_PORT))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT)
|
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT, host=LOCAL_PROXY_HOST)
|
||||||
diag_msg = diag[1] if diag else f"端口 {LOCAL_PROXY_PORT} 连接失败,原因: {e}"
|
diag_msg = diag[1] if diag else f"端口 {LOCAL_PROXY_PORT} 连接失败,原因: {e}"
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -3345,10 +3396,20 @@ def check_proxy_health() -> dict[str, Any]:
|
|||||||
|
|
||||||
# 3. 使用 curl 通过本地 SOCKS5 代理接口测试 IP 与实际延迟
|
# 3. 使用 curl 通过本地 SOCKS5 代理接口测试 IP 与实际延迟
|
||||||
def _curl_check_ip(url: str) -> dict[str, Any] | None:
|
def _curl_check_ip(url: str) -> dict[str, Any] | None:
|
||||||
|
proxy_host = LOCAL_PROXY_HOST
|
||||||
|
if proxy_host == "::":
|
||||||
|
proxy_url = f"socks5h://[::1]:{LOCAL_PROXY_PORT}"
|
||||||
|
elif proxy_host == "0.0.0.0":
|
||||||
|
proxy_url = f"socks5h://127.0.0.1:{LOCAL_PROXY_PORT}"
|
||||||
|
elif ":" in proxy_host:
|
||||||
|
proxy_url = f"socks5h://[{proxy_host}]:{LOCAL_PROXY_PORT}"
|
||||||
|
else:
|
||||||
|
proxy_url = f"socks5h://{proxy_host}:{LOCAL_PROXY_PORT}"
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"curl", "-4", "-s",
|
"curl", "-s",
|
||||||
"-w", "\n%{time_total} %{http_code}",
|
"-w", "\n%{time_total} %{http_code}",
|
||||||
"-x", f"socks5h://127.0.0.1:{LOCAL_PROXY_PORT}",
|
"-x", proxy_url,
|
||||||
url,
|
url,
|
||||||
"--max-time", "5"
|
"--max-time", "5"
|
||||||
]
|
]
|
||||||
@@ -3376,7 +3437,7 @@ def check_proxy_health() -> dict[str, Any]:
|
|||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT)
|
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT, host=LOCAL_PROXY_HOST)
|
||||||
if diag:
|
if diag:
|
||||||
return {"ok": False, "error": f"出口连接测试失败 | 本机诊断结果: {diag[1]}"}
|
return {"ok": False, "error": f"出口连接测试失败 | 本机诊断结果: {diag[1]}"}
|
||||||
|
|
||||||
@@ -3404,7 +3465,7 @@ def background_proxy_checker() -> None:
|
|||||||
else:
|
else:
|
||||||
error_msg = res.get("error", "未知错误")
|
error_msg = res.get("error", "未知错误")
|
||||||
if active_openvpn_node_id:
|
if active_openvpn_node_id:
|
||||||
print(f"[警告] 7928 端口本地代理当前不可用!原因: {error_msg}", flush=True)
|
print(f"[警告] {LOCAL_PROXY_PORT} 端口本地代理当前不可用!原因: {error_msg}", flush=True)
|
||||||
log_to_json("WARNING", "Proxy", f"代理不可用: {error_msg}")
|
log_to_json("WARNING", "Proxy", f"代理不可用: {error_msg}")
|
||||||
set_state(
|
set_state(
|
||||||
proxy_ok=False,
|
proxy_ok=False,
|
||||||
@@ -3840,7 +3901,7 @@ def main() -> None:
|
|||||||
"target_valid_nodes": TARGET_VALID_NODES,
|
"target_valid_nodes": TARGET_VALID_NODES,
|
||||||
"fetch_interval_seconds": FETCH_INTERVAL_SECONDS,
|
"fetch_interval_seconds": FETCH_INTERVAL_SECONDS,
|
||||||
"check_interval_seconds": CHECK_INTERVAL_SECONDS,
|
"check_interval_seconds": CHECK_INTERVAL_SECONDS,
|
||||||
"local_proxy": f"http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}",
|
"local_proxy": f"http://{'[' + LOCAL_PROXY_HOST + ']' if ':' in LOCAL_PROXY_HOST else LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}",
|
||||||
"active_openvpn_node_id": "",
|
"active_openvpn_node_id": "",
|
||||||
"last_fetch_status": "starting",
|
"last_fetch_status": "starting",
|
||||||
"last_check_message": "服务已启动,正在初始化网络并获取候选 VPN 节点...",
|
"last_check_message": "服务已启动,正在初始化网络并获取候选 VPN 节点...",
|
||||||
@@ -3854,11 +3915,16 @@ def main() -> None:
|
|||||||
# Wait for the gateway to officially start
|
# Wait for the gateway to officially start
|
||||||
print("[网关] 正在启动代理网关...", flush=True)
|
print("[网关] 正在启动代理网关...", flush=True)
|
||||||
gateway_ready = False
|
gateway_ready = False
|
||||||
|
is_ipv6 = ":" in LOCAL_PROXY_HOST
|
||||||
|
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(af, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
s.settimeout(0.5)
|
s.settimeout(0.5)
|
||||||
s.connect((LOCAL_PROXY_HOST, LOCAL_PROXY_PORT))
|
connect_host = LOCAL_PROXY_HOST
|
||||||
|
if connect_host in ("::", "0.0.0.0", ""):
|
||||||
|
connect_host = "::1" if is_ipv6 else "127.0.0.1"
|
||||||
|
s.connect((connect_host, LOCAL_PROXY_PORT))
|
||||||
gateway_ready = True
|
gateway_ready = True
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -3884,7 +3950,7 @@ def main() -> None:
|
|||||||
|
|
||||||
print(f"UI: http://{ui_host}:{ui_port}/", flush=True)
|
print(f"UI: http://{ui_host}:{ui_port}/", flush=True)
|
||||||
print(f"Proxy: http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}", flush=True)
|
print(f"Proxy: http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}", flush=True)
|
||||||
ThreadingHTTPServer((ui_host, ui_port), Handler).serve_forever()
|
DualStackHTTPServer((ui_host, ui_port), Handler).serve_forever()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user