feat: add proxy authentication, limit concurrent connections, and allow configurable proxy port.

This commit is contained in:
baoweise-bot
2026-06-07 12:57:37 +08:00
parent 2ee863b72f
commit 676188ca1a
4 changed files with 1327 additions and 852 deletions
+33 -17
View File
@@ -350,8 +350,8 @@ def check_openvpn_process():
if pid_dir.isdigit(): if pid_dir.isdigit():
try: try:
with open(os.path.join('/proc', pid_dir, 'cmdline'), 'r') as f: with open(os.path.join('/proc', pid_dir, 'cmdline'), 'r') as f:
cmd = f.read().split('\x00')[0] cmd = f.read().replace('\x00', ' ')
if 'openvpn' in cmd: if 'openvpn' in cmd and ('/opt/aimilivpn/vpngate_data' in cmd or '/opt/aimilivpn/vpngate_data/configs' in cmd):
return True return True
except Exception: except Exception:
continue continue
@@ -466,15 +466,16 @@ def print_status():
proxy_port = proxy_port proxy_port = proxy_port
if proxy_host == "::": if proxy_host == "::":
socks_addr = "127.0.0.1" proxy_addr = "127.0.0.1"
elif ":" in proxy_host: elif ":" in proxy_host:
socks_addr = f"[{proxy_host}]" proxy_addr = f"[{proxy_host}]"
else: else:
socks_addr = proxy_host proxy_addr = proxy_host
print_line("【使用方法】") print_line("【使用方法】")
print_line(f" export http_proxy=socks5://{socks_addr}:{proxy_port}") print_line(f" export http_proxy=http://{proxy_addr}:{proxy_port}")
print_line(f" export https_proxy=socks5://{socks_addr}:{proxy_port}") print_line(f" export https_proxy=http://{proxy_addr}:{proxy_port}")
print_line(f" # 也可用于 SOCKS5: socks5://{proxy_addr}:{proxy_port}")
print_line("=======================================================") print_line("=======================================================")
def run_service_cmd(cmd): def run_service_cmd(cmd):
@@ -678,6 +679,10 @@ def configure_port():
if val: if val:
port = int(val) port = int(val)
if 1 <= port <= 65535: if 1 <= port <= 65535:
if port == int(cfg.get('proxy_port', 7928)):
print("错误: 网页管理端口不能与代理出站端口相同。")
time.sleep(2)
continue
cfg['port'] = port cfg['port'] = port
save_ui_cfg(cfg) save_ui_cfg(cfg)
print(f"网页管理端口已更新为: {port}") print(f"网页管理端口已更新为: {port}")
@@ -694,6 +699,10 @@ def configure_port():
if val: if val:
port = int(val) port = int(val)
if 1024 <= port <= 65535: if 1024 <= port <= 65535:
if port == int(cfg.get('port', 8787)):
print("错误: 代理出站端口不能与网页管理端口相同。")
time.sleep(2)
continue
cfg['proxy_port'] = port cfg['proxy_port'] = port
save_ui_cfg(cfg) save_ui_cfg(cfg)
print(f"代理出站端口已更新为: {port}") print(f"代理出站端口已更新为: {port}")
@@ -1040,19 +1049,24 @@ while True:
done done
fi fi
# Write config JSON # Write config JSON. Values are passed as argv to avoid breaking Python code
python3 -c " # when username/password contain quotes, backslashes, or shell metacharacters.
python3 - "$AUTH_FILE" "$UI_PORT" "$SECRET_PATH" "$UI_USERNAME" "$UI_PASSWORD" <<'PY'
import json import json
import sys
auth_file, ui_port, secret_path, username, password = sys.argv[1:6]
cfg = { cfg = {
'host': '::', "host": "::",
'port': int('$UI_PORT'), "port": int(ui_port),
'secret_path': '$SECRET_PATH', "proxy_port": 7928,
'username': '$UI_USERNAME', "secret_path": secret_path,
'password': '$UI_PASSWORD' "username": username,
"password": password,
} }
with open('$AUTH_FILE', 'w', encoding='utf-8') as f: with open(auth_file, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2) json.dump(cfg, f, ensure_ascii=False, indent=2)
" PY
fi fi
# 8. Start service # 8. Start service
@@ -1132,12 +1146,14 @@ SECRET_PATH="EJsW2EeBo9lY"
USERNAME="未配置" USERNAME="未配置"
PASSWORD="未配置" PASSWORD="未配置"
UI_PORT=8787 UI_PORT=8787
PROXY_PORT=7928
AUTH_FILE="${INSTALL_DIR}/vpngate_data/ui_auth.json" AUTH_FILE="${INSTALL_DIR}/vpngate_data/ui_auth.json"
if [ -f "$AUTH_FILE" ]; then if [ -f "$AUTH_FILE" ]; then
SECRET_PATH=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('secret_path', 'EJsW2EeBo9lY'))" 2>/dev/null || echo "EJsW2EeBo9lY") SECRET_PATH=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('secret_path', 'EJsW2EeBo9lY'))" 2>/dev/null || echo "EJsW2EeBo9lY")
USERNAME=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('username', '未配置'))" 2>/dev/null || echo "未配置") USERNAME=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('username', '未配置'))" 2>/dev/null || echo "未配置")
PASSWORD=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('password', '未配置'))" 2>/dev/null || echo "未配置") PASSWORD=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('password', '未配置'))" 2>/dev/null || echo "未配置")
UI_PORT=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('port', 8787))" 2>/dev/null || echo "8787") UI_PORT=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('port', 8787))" 2>/dev/null || echo "8787")
PROXY_PORT=$(python3 -c "import json; print(json.load(open('$AUTH_FILE')).get('proxy_port', 7928))" 2>/dev/null || echo "7928")
fi fi
# Get VPS public IP # Get VPS public IP
@@ -1158,7 +1174,7 @@ if [ -n "$PUBLIC_IPV6" ]; then
fi 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}${BLUE}http://[::1]:7928/${PLAIN}" echo -e " * HTTP/SOCKS5 代理端口: ${BLUE}http://127.0.0.1:${PROXY_PORT}/${PLAIN}${BLUE}http://[::1]:${PROXY_PORT}/${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}"
+150 -40
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import base64
import os
import secrets
import select import select
import socket import socket
import threading import threading
@@ -7,6 +10,15 @@ import urllib.parse
import time import time
from typing import Any from typing import Any
def parse_positive_int(value: str | None, default: int) -> int:
try:
return max(1, int(value or default))
except (TypeError, ValueError):
return default
MAX_PROXY_CONNECTIONS = parse_positive_int(os.environ.get("LOCAL_PROXY_MAX_CONNECTIONS"), 256)
proxy_connection_sem = threading.BoundedSemaphore(MAX_PROXY_CONNECTIONS)
def parse_int(value: Any) -> int: def parse_int(value: Any) -> int:
try: try:
return int(value) return int(value)
@@ -22,18 +34,57 @@ def recv_exact(sock: socket.socket, size: int) -> bytes:
data += chunk data += chunk
return data return data
def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float = 3.0) -> str | None: def parse_host_port(authority: str, default_port: int) -> tuple[str, int]:
try: authority = authority.strip()
socket.inet_aton(host) if authority.startswith("["):
return host host_part, sep, rest = authority.partition("]")
except OSError: host = host_part.lstrip("[")
pass port = default_port
try: if sep and rest.startswith(":"):
socket.inet_pton(socket.AF_INET6, host) port_text = rest[1:]
return host port = parse_int(port_text) or default_port
except OSError: return host, port
pass if authority.count(":") == 1:
host, _, port_text = authority.rpartition(":")
return host, parse_int(port_text) or default_port
return authority, default_port
def get_proxy_credentials() -> tuple[str | None, str | None]:
user = os.environ.get("LOCAL_PROXY_USER") or os.environ.get("LOCAL_PROXY_USERNAME")
password = os.environ.get("LOCAL_PROXY_PASS") or os.environ.get("LOCAL_PROXY_PASSWORD")
if user is None and password is None:
return None, None
return user or "", password or ""
def proxy_auth_enabled() -> bool:
user, password = get_proxy_credentials()
return user is not None and password is not None
def parse_http_basic_auth(lines: list[str]) -> tuple[str | None, str | None]:
for line in lines:
name, sep, value = line.partition(":")
if not sep or name.strip().lower() != "proxy-authorization":
continue
scheme, _, token = value.strip().partition(" ")
if scheme.lower() != "basic" or not token:
return None, None
try:
decoded = base64.b64decode(token, validate=True).decode("utf-8", errors="replace")
except Exception:
return None, None
username, sep, password = decoded.partition(":")
if not sep:
return None, None
return username, password
return None, None
def check_credentials(username: str | None, password: str | None) -> bool:
expected_user, expected_pass = get_proxy_credentials()
if expected_user is None or expected_pass is None:
return True
return secrets.compare_digest(username or "", expected_user) and secrets.compare_digest(password or "", expected_pass)
def dns_query_over_tun0(host: str, qtype: int, dns_server: str, timeout: float) -> str | None:
import random import random
sock = None sock = None
try: try:
@@ -52,7 +103,7 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
qname += len(part_bytes).to_bytes(1, "big") + part_bytes qname += len(part_bytes).to_bytes(1, "big") + part_bytes
qname += b"\x00" qname += b"\x00"
qtype_qclass = b"\x00\x01\x00\x01" qtype_qclass = qtype.to_bytes(2, "big") + b"\x00\x01"
packet = tx_id + flags + questions + rrs + qname + qtype_qclass packet = tx_id + flags + questions + rrs + qname + qtype_qclass
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -66,7 +117,7 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
print("[DNS 绑定失败] [错误代码 3004] DNS 解析绑定 tun0 失败,网卡设备不存在,请检查 VPN 连接!", flush=True) print("[DNS 绑定失败] [错误代码 3004] DNS 解析绑定 tun0 失败,网卡设备不存在,请检查 VPN 连接!", flush=True)
return None return None
sock.sendto(packet, (dns_server, 53)) sock.sendto(packet, (dns_server, 53))
resp, _ = sock.recvfrom(2048) resp, _ = sock.recvfrom(4096)
except Exception: except Exception:
return None return None
finally: finally:
@@ -77,11 +128,8 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
pass pass
try: try:
if len(resp) < 12: if len(resp) < 12 or resp[:2] != tx_id:
return None return None
if resp[:2] != tx_id:
return None
rcode = resp[3] & 0x0F rcode = resp[3] & 0x0F
if rcode != 0: if rcode != 0:
return None return None
@@ -92,17 +140,13 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
if length == 0: if length == 0:
offset += 1 offset += 1
break break
elif (length & 0xC0) == 0xC0: if (length & 0xC0) == 0xC0:
offset += 2 offset += 2
break break
else: offset += 1 + length
offset += 1 + length
offset += 4 offset += 4
answers_count = int.from_bytes(resp[6:8], "big") answers_count = int.from_bytes(resp[6:8], "big")
if answers_count == 0:
return None
for _ in range(answers_count): for _ in range(answers_count):
if offset >= len(resp): if offset >= len(resp):
break break
@@ -111,11 +155,10 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
if length == 0: if length == 0:
offset += 1 offset += 1
break break
elif (length & 0xC0) == 0xC0: if (length & 0xC0) == 0xC0:
offset += 2 offset += 2
break break
else: offset += 1 + length
offset += 1 + length
if offset + 10 > len(resp): if offset + 10 > len(resp):
break break
atype = int.from_bytes(resp[offset : offset + 2], "big") atype = int.from_bytes(resp[offset : offset + 2], "big")
@@ -124,14 +167,30 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
offset += 10 offset += 10
if offset + rdlength > len(resp): if offset + rdlength > len(resp):
break break
if atype == 1 and aclass == 1 and rdlength == 4: record = resp[offset : offset + rdlength]
ip_bytes = resp[offset : offset + 4] if atype == qtype and aclass == 1:
return socket.inet_ntoa(ip_bytes) if qtype == 1 and rdlength == 4:
return socket.inet_ntoa(record)
if qtype == 28 and rdlength == 16:
return socket.inet_ntop(socket.AF_INET6, record)
offset += rdlength offset += rdlength
except Exception: except Exception:
return None return None
return None return None
def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float = 3.0) -> str | None:
try:
socket.inet_aton(host)
return host
except OSError:
pass
try:
socket.inet_pton(socket.AF_INET6, host)
return host
except OSError:
pass
return dns_query_over_tun0(host, 1, dns_server, timeout) or dns_query_over_tun0(host, 28, dns_server, timeout)
def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket: def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket:
host, port = address host, port = address
resolved_ip = resolve_dns_over_tun0(host) resolved_ip = resolve_dns_over_tun0(host)
@@ -178,8 +237,24 @@ def socks5_client(client: socket.socket, first_byte: bytes) -> None:
upstream = None upstream = None
try: try:
methods_count = recv_exact(client, 1)[0] methods_count = recv_exact(client, 1)[0]
recv_exact(client, methods_count) methods = recv_exact(client, methods_count)
client.sendall(b"\x05\x00") if proxy_auth_enabled():
if 2 not in methods:
client.sendall(b"\x05\xff")
return
client.sendall(b"\x05\x02")
auth_version = recv_exact(client, 1)[0]
if auth_version != 1:
client.sendall(b"\x01\x01")
return
username = recv_exact(client, recv_exact(client, 1)[0]).decode("utf-8", errors="replace")
password = recv_exact(client, recv_exact(client, 1)[0]).decode("utf-8", errors="replace")
if not check_credentials(username, password):
client.sendall(b"\x01\x01")
return
client.sendall(b"\x01\x00")
else:
client.sendall(b"\x05\x00")
version, command, _, address_type = recv_exact(client, 4) version, command, _, address_type = recv_exact(client, 4)
if version != 5 or command != 1: if version != 5 or command != 1:
client.sendall(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00") client.sendall(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
@@ -223,12 +298,30 @@ def http_client(client: socket.socket, first_byte: bytes) -> None:
upstream = None upstream = None
try: try:
header = read_http_header(client, first_byte) header = read_http_header(client, first_byte)
if b"\r\n\r\n" not in header:
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
return
head, rest = header.split(b"\r\n\r\n", 1) head, rest = header.split(b"\r\n\r\n", 1)
lines = head.decode("iso-8859-1", errors="replace").split("\r\n") lines = head.decode("iso-8859-1", errors="replace").split("\r\n")
method, target, version = lines[0].split(" ", 2) try:
method, target, version = lines[0].split(" ", 2)
except ValueError:
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
return
if not version.startswith("HTTP/"):
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
return
if proxy_auth_enabled():
username, password = parse_http_basic_auth(lines[1:])
if not check_credentials(username, password):
client.sendall(
b"HTTP/1.1 407 Proxy Authentication Required\r\n"
b"Proxy-Authenticate: Basic realm=\"AimiliVPN Proxy\"\r\n"
b"Content-Length: 0\r\n\r\n"
)
return
if method.upper() == "CONNECT": if method.upper() == "CONNECT":
host, _, port_text = target.partition(":") host, port = parse_host_port(target, 443)
port = parse_int(port_text) or 443
upstream = create_connection((host, port), timeout=20) upstream = create_connection((host, port), timeout=20)
client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n") client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
if rest: if rest:
@@ -236,7 +329,11 @@ def http_client(client: socket.socket, first_byte: bytes) -> None:
relay(client, upstream) relay(client, upstream)
return return
parsed = urllib.parse.urlsplit(target) try:
parsed = urllib.parse.urlsplit(target)
except ValueError:
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
return
hostname = parsed.hostname hostname = parsed.hostname
port = parsed.port port = parsed.port
scheme = parsed.scheme scheme = parsed.scheme
@@ -254,16 +351,15 @@ def http_client(client: socket.socket, first_byte: bytes) -> None:
else: else:
port = None port = None
else: else:
host_part, _, port_part = host_val.partition(":") hostname, parsed_port = parse_host_port(host_val, 0)
hostname = host_part port = parsed_port or None
port = int(port_part) if port_part.isdigit() else None
break break
if not hostname: if not hostname:
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n") client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
return return
port = port or (443 if scheme == "https" else 80) port = port or (443 if scheme == "https" else 80)
path = urllib.parse.urlunsplit(("", "", parsed.path or "/", parsed.query, "")) path = urllib.parse.urlunsplit(("", "", parsed.path or "/", parsed.query, ""))
headers = [line for line in lines[1:] if not line.lower().startswith(("proxy-connection:", "connection:"))] headers = [line for line in lines[1:] if not line.lower().startswith(("proxy-connection:", "connection:", "proxy-authorization:"))]
request = f"{method} {path} {version}\r\n" + "\r\n".join(headers) + "\r\nConnection: close\r\n\r\n" request = f"{method} {path} {version}\r\n" + "\r\n".join(headers) + "\r\nConnection: close\r\n\r\n"
upstream = create_connection((hostname, port), timeout=20) upstream = create_connection((hostname, port), timeout=20)
upstream.sendall(request.encode("iso-8859-1") + rest) upstream.sendall(request.encode("iso-8859-1") + rest)
@@ -355,7 +451,21 @@ def start_proxy_server(host: str, port: int) -> None:
while True: while True:
try: try:
client, address = server.accept() client, address = server.accept()
threading.Thread(target=proxy_client, args=(client, address), daemon=True).start() if not proxy_connection_sem.acquire(blocking=False):
print(f"[代理限流] 当前连接数已达到上限 {MAX_PROXY_CONNECTIONS},拒绝客户端 {address}", flush=True)
try:
client.close()
except OSError:
pass
continue
def run_client() -> None:
try:
proxy_client(client, address)
finally:
proxy_connection_sem.release()
threading.Thread(target=run_client, daemon=True).start()
except Exception as e: except Exception as e:
print(f"[ERROR] Proxy accept failed: {e}", flush=True) print(f"[ERROR] Proxy accept failed: {e}", flush=True)
time.sleep(0.5) time.sleep(0.5)
+73 -40
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from typing import Any from typing import Any
ROOT_DIR = Path(__file__).resolve().parent ROOT_DIR = Path(__file__).resolve().parent
DATA_DIR = ROOT_DIR / "vpngate_data" DATA_DIR = Path(os.environ["VPNGATE_DATA_DIR"]).resolve() if os.environ.get("VPNGATE_DATA_DIR") else ROOT_DIR / "vpngate_data"
IP_CACHE_FILE = DATA_DIR / "ip_cache.json" IP_CACHE_FILE = DATA_DIR / "ip_cache.json"
ip_cache_lock = threading.RLock() ip_cache_lock = threading.RLock()
@@ -92,51 +92,84 @@ def _safe_int(val: Any, default: int = 0) -> int:
except (ValueError, TypeError): except (ValueError, TypeError):
return default return default
def parse_proxy_endpoint(value: str, default_port: int) -> tuple[str | None, int | None]:
value = value.strip()
if not value:
return None, None
if "://" in value:
parsed = urllib.parse.urlsplit(value)
if parsed.hostname:
return parsed.hostname, parsed.port or default_port
return None, None
if value.startswith("["):
host_part, sep, rest = value.partition("]")
host = host_part.lstrip("[")
port = default_port
if sep and rest.startswith(":"):
port = _safe_int(rest[1:], default_port)
return host or None, port
if value.count(":") == 1:
host, _, port_text = value.rpartition(":")
return host or None, _safe_int(port_text, default_port)
return value, default_port
def _proxy_config_from_env(env_name: str, forced_type: str | None = None) -> tuple[str, str, int, str | None, str | None] | None:
val = os.environ.get(env_name)
if not val:
return None
if "://" in val:
try:
parsed = urllib.parse.urlsplit(val)
except Exception:
return None
if not parsed.hostname:
return None
ptype = forced_type or ("socks" if parsed.scheme.startswith("socks") else "http")
username = urllib.parse.unquote(parsed.username) if parsed.username is not None else None
password = urllib.parse.unquote(parsed.password or "") if parsed.username is not None else None
return ptype, parsed.hostname, parsed.port or 10808, username, password
host, port = parse_proxy_endpoint(val, 10808)
if host and port:
return forced_type or "http", host, port, None, None
return None
def get_upstream_proxy_config() -> tuple[str | None, str | None, int | None, str | None, str | None]:
for env_name, forced_type in [
("OPENVPN_UPSTREAM_SOCKS", "socks"),
("OPENVPN_UPSTREAM_HTTP", "http"),
("http_proxy", None),
("HTTP_PROXY", None),
("https_proxy", None),
("HTTPS_PROXY", None),
]:
cfg = _proxy_config_from_env(env_name, forced_type)
if cfg:
ptype, host, port, username, password = cfg
return ptype, host, port, username, password
return None, None, None, None, None
def get_upstream_proxy() -> tuple[str | None, str | None, int | None]: def get_upstream_proxy() -> tuple[str | None, str | None, int | None]:
""" """
Returns (proxy_type, host, port) from environment variables. Returns (proxy_type, host, port) from environment variables.
proxy_type is 'socks' or 'http'. proxy_type is 'socks' or 'http'.
""" """
socks_env = os.environ.get("OPENVPN_UPSTREAM_SOCKS") ptype, host, port, _, _ = get_upstream_proxy_config()
if socks_env: return ptype, host, port
if "://" in socks_env:
parsed = urllib.parse.urlsplit(socks_env)
if parsed.hostname and parsed.port:
return "socks", parsed.hostname, parsed.port
else:
parts = socks_env.split(":")
if len(parts) == 2:
return "socks", parts[0], _safe_int(parts[1], 10808)
elif len(parts) == 1:
return "socks", parts[0], 10808
http_env = os.environ.get("OPENVPN_UPSTREAM_HTTP") def get_upstream_proxy_auth() -> tuple[str | None, str | None]:
if http_env: """
if "://" in http_env: Returns optional (username, password) for the configured upstream proxy.
parsed = urllib.parse.urlsplit(http_env) Supports credentials embedded in proxy URLs and explicit env vars.
if parsed.hostname and parsed.port: """
return "http", parsed.hostname, parsed.port _, _, _, username, password = get_upstream_proxy_config()
else: if username is not None:
parts = http_env.split(":") return username, password or ""
if len(parts) == 2:
return "http", parts[0], _safe_int(parts[1], 10808)
elif len(parts) == 1:
return "http", parts[0], 10808
for env_name in ["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]: user = os.environ.get("OPENVPN_UPSTREAM_USER") or os.environ.get("OPENVPN_UPSTREAM_USERNAME")
val = os.environ.get(env_name) password = os.environ.get("OPENVPN_UPSTREAM_PASS") or os.environ.get("OPENVPN_UPSTREAM_PASSWORD")
if not val: if user is not None:
continue return user, password or ""
if "://" in val: return None, None
parsed = urllib.parse.urlsplit(val)
ptype = "socks" if parsed.scheme.startswith("socks") else "http"
if parsed.hostname and parsed.port:
return ptype, parsed.hostname, parsed.port
else:
parts = val.split(":")
if len(parts) == 2:
return "http", parts[0], _safe_int(parts[1], 10808)
return None, None, None
def is_config_tcp(config_text: str) -> bool: def is_config_tcp(config_text: str) -> bool:
try: try:
@@ -338,7 +371,7 @@ def load_ip_cache() -> dict[str, dict[str, Any]]:
def save_ip_cache(cache: dict[str, dict[str, Any]]) -> None: def save_ip_cache(cache: dict[str, dict[str, Any]]) -> None:
with ip_cache_lock: with ip_cache_lock:
try: try:
DATA_DIR.mkdir(exist_ok=True) DATA_DIR.mkdir(exist_ok=True, parents=True)
IP_CACHE_FILE.write_text(json.dumps(cache, ensure_ascii=False, indent=2), encoding="utf-8") IP_CACHE_FILE.write_text(json.dumps(cache, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception: except Exception:
pass pass
+449 -133
View File
File diff suppressed because it is too large Load Diff