mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: aether-proxy TLS 双栈支持与自签名证书自动生成
- aether-proxy 新增 TLS 模块:自签名证书生成、TLS acceptor 构建、证书 SHA-256 指纹计算 - 代理服务器支持 HTTP+TLS 双栈模式,通过 peek 首字节区分 TLS ClientHello 与普通 HTTP - 注册与心跳上报 tls_enabled 和 tls_cert_fingerprint 字段 - Python 侧 httpx 代理适配:TLS 代理使用 httpx.Proxy + CERT_NONE ssl_context - ProxyNode 模型新增 tls_enabled/tls_cert_fingerprint 字段及对应迁移
This commit is contained in:
@@ -50,6 +50,8 @@ def _node_to_dict(node: ProxyNode) -> dict[str, Any]:
|
||||
"active_connections": node.active_connections,
|
||||
"total_requests": node.total_requests,
|
||||
"avg_latency_ms": node.avg_latency_ms,
|
||||
"tls_enabled": bool(node.tls_enabled),
|
||||
"tls_cert_fingerprint": node.tls_cert_fingerprint,
|
||||
"remote_config": node.remote_config,
|
||||
"config_version": node.config_version,
|
||||
"created_at": node.created_at,
|
||||
@@ -75,6 +77,12 @@ class ProxyNodeRegisterRequest(BaseModel):
|
||||
total_requests: int | None = Field(None, ge=0, description="累计请求数")
|
||||
avg_latency_ms: float | None = Field(None, ge=0, description="平均延迟(毫秒)")
|
||||
|
||||
# TLS
|
||||
tls_enabled: bool = Field(False, description="是否启用 TLS 加密")
|
||||
tls_cert_fingerprint: str | None = Field(
|
||||
None, max_length=128, description="TLS 证书 SHA-256 指纹"
|
||||
)
|
||||
|
||||
@field_validator("ip")
|
||||
@classmethod
|
||||
def validate_ip(cls, v: str) -> str:
|
||||
@@ -275,6 +283,8 @@ class AdminRegisterProxyNodeAdapter(AdminApiAdapter):
|
||||
node.status = ProxyNodeStatus.ONLINE
|
||||
node.last_heartbeat_at = now
|
||||
node.heartbeat_interval = req.heartbeat_interval
|
||||
node.tls_enabled = req.tls_enabled
|
||||
node.tls_cert_fingerprint = req.tls_cert_fingerprint
|
||||
if req.active_connections is not None:
|
||||
node.active_connections = req.active_connections
|
||||
if req.total_requests is not None:
|
||||
@@ -295,6 +305,8 @@ class AdminRegisterProxyNodeAdapter(AdminApiAdapter):
|
||||
active_connections=req.active_connections or 0,
|
||||
total_requests=req.total_requests or 0,
|
||||
avg_latency_ms=req.avg_latency_ms,
|
||||
tls_enabled=req.tls_enabled,
|
||||
tls_cert_fingerprint=req.tls_cert_fingerprint,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -496,7 +508,9 @@ def _build_test_proxy_url(node: ProxyNode) -> str:
|
||||
# aether-proxy: 使用 HMAC 认证构建代理 URL
|
||||
from src.clients.http_client import _build_hmac_proxy_url
|
||||
|
||||
return _build_hmac_proxy_url(node.ip, node.port, node.id)
|
||||
return _build_hmac_proxy_url(
|
||||
node.ip, node.port, node.id, tls_enabled=bool(node.tls_enabled)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -634,9 +648,14 @@ class AdminTestProxyNodeAdapter(AdminApiAdapter):
|
||||
test_url = "https://1.1.1.1/cdn-cgi/trace"
|
||||
start = _time.monotonic()
|
||||
|
||||
# TLS 代理需要 proxy_ssl_context
|
||||
from src.clients.http_client import _make_proxy_param
|
||||
|
||||
proxy_param = _make_proxy_param(proxy_url)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
proxy=proxy_url,
|
||||
proxy=proxy_param,
|
||||
timeout=httpx.Timeout(15.0, connect=10.0),
|
||||
) as client:
|
||||
response = await client.get(test_url)
|
||||
|
||||
@@ -73,7 +73,12 @@ def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
||||
"password": node.proxy_password,
|
||||
}
|
||||
else:
|
||||
value = {"ip": node.ip, "port": node.port}
|
||||
value = {
|
||||
"ip": node.ip,
|
||||
"port": node.port,
|
||||
"tls_enabled": bool(node.tls_enabled),
|
||||
"tls_cert_fingerprint": node.tls_cert_fingerprint,
|
||||
}
|
||||
|
||||
_proxy_node_cache[node_id] = (value, now + _PROXY_NODE_CACHE_TTL_SECONDS)
|
||||
return value
|
||||
@@ -81,12 +86,14 @@ def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
||||
db.close()
|
||||
|
||||
|
||||
def _build_hmac_proxy_url(ip: str, port: int, node_id: str) -> str:
|
||||
def _build_hmac_proxy_url(ip: str, port: int, node_id: str, *, tls_enabled: bool = False) -> str:
|
||||
"""
|
||||
构建带 HMAC BasicAuth 的 httpx proxy URL
|
||||
|
||||
格式: http://hmac:{timestamp}.{signature}@{ip}:{port}
|
||||
格式: http(s)://hmac:{timestamp}.{signature}@{ip}:{port}
|
||||
signature = HMAC-SHA256(PROXY_HMAC_KEY, "{timestamp}\\n{node_id}") 的 hex
|
||||
|
||||
当 tls_enabled=True 时使用 https:// scheme。
|
||||
"""
|
||||
if not config.proxy_hmac_key:
|
||||
raise ProxyNodeUnavailableError(
|
||||
@@ -102,7 +109,8 @@ def _build_hmac_proxy_url(ip: str, port: int, node_id: str) -> str:
|
||||
).hexdigest()
|
||||
|
||||
host = f"[{ip}]" if ":" in ip else ip
|
||||
return f"http://hmac:{timestamp}.{signature}@{host}:{int(port)}"
|
||||
scheme = "https" if tls_enabled else "http"
|
||||
return f"{scheme}://hmac:{timestamp}.{signature}@{host}:{int(port)}"
|
||||
|
||||
|
||||
# 系统默认代理缓存
|
||||
@@ -150,9 +158,11 @@ def get_system_proxy_config() -> dict[str, Any] | None:
|
||||
db.close()
|
||||
|
||||
|
||||
def resolve_ops_proxy(connector_config: dict[str, Any] | None) -> str | None:
|
||||
def resolve_ops_proxy(
|
||||
connector_config: dict[str, Any] | None,
|
||||
) -> str | httpx.Proxy | None:
|
||||
"""
|
||||
从 ops connector.config 中解析代理 URL(含系统默认回退)
|
||||
从 ops connector.config 中解析代理参数(含系统默认回退)
|
||||
|
||||
优先级:
|
||||
1. connector_config.proxy_node_id(新格式)
|
||||
@@ -163,14 +173,15 @@ def resolve_ops_proxy(connector_config: dict[str, Any] | None) -> str | None:
|
||||
connector_config: connector 的 config 字典
|
||||
|
||||
Returns:
|
||||
代理 URL 字符串,或 None
|
||||
httpx 可接受的代理参数(str 或 httpx.Proxy),或 None
|
||||
"""
|
||||
if connector_config:
|
||||
# 新格式:proxy_node_id → 通过 build_proxy_url 解析
|
||||
node_id = connector_config.get("proxy_node_id")
|
||||
if isinstance(node_id, str) and node_id.strip():
|
||||
try:
|
||||
return build_proxy_url({"node_id": node_id.strip(), "enabled": True})
|
||||
url = build_proxy_url({"node_id": node_id.strip(), "enabled": True})
|
||||
return _make_proxy_param(url)
|
||||
except Exception as exc:
|
||||
logger.warning("解析 proxy_node_id={} 失败,回退到直连: {}", node_id, exc)
|
||||
return None
|
||||
@@ -184,7 +195,8 @@ def resolve_ops_proxy(connector_config: dict[str, Any] | None) -> str | None:
|
||||
system_proxy = get_system_proxy_config()
|
||||
if system_proxy:
|
||||
try:
|
||||
return build_proxy_url(system_proxy)
|
||||
url = build_proxy_url(system_proxy)
|
||||
return _make_proxy_param(url)
|
||||
except Exception as exc:
|
||||
logger.warning("构建系统默认代理 URL 失败: {}", exc)
|
||||
return None
|
||||
@@ -281,7 +293,12 @@ def build_proxy_url(proxy_config: dict[str, Any]) -> str | None:
|
||||
return manual_url
|
||||
|
||||
# aether-proxy 节点:使用 HMAC 认证
|
||||
return _build_hmac_proxy_url(node_info["ip"], node_info["port"], node_id)
|
||||
return _build_hmac_proxy_url(
|
||||
node_info["ip"],
|
||||
node_info["port"],
|
||||
node_id,
|
||||
tls_enabled=node_info.get("tls_enabled", False),
|
||||
)
|
||||
|
||||
proxy_url: str | None = proxy_config.get("url")
|
||||
if not proxy_url:
|
||||
@@ -308,6 +325,26 @@ def build_proxy_url(proxy_config: dict[str, Any]) -> str | None:
|
||||
return proxy_url
|
||||
|
||||
|
||||
def _make_proxy_param(proxy_url: str | None) -> str | httpx.Proxy | None:
|
||||
"""
|
||||
根据代理 URL 返回 httpx 可接受的 proxy 参数。
|
||||
|
||||
对于 https:// scheme 的代理 URL(TLS aether-proxy 节点),返回 httpx.Proxy
|
||||
并附带 proxy_ssl_context(CERT_NONE,因为使用自签名证书)。
|
||||
其他情况返回普通 URL 字符串。
|
||||
"""
|
||||
if not proxy_url:
|
||||
return None
|
||||
|
||||
# https:// 代理需要 ssl_context(自签名证书场景)
|
||||
if proxy_url.startswith("https://"):
|
||||
from src.utils.ssl_utils import get_proxy_ssl_context
|
||||
|
||||
return httpx.Proxy(url=proxy_url, ssl_context=get_proxy_ssl_context())
|
||||
|
||||
return proxy_url
|
||||
|
||||
|
||||
class HTTPClientPool:
|
||||
"""
|
||||
全局HTTP客户端池单例
|
||||
@@ -521,8 +558,9 @@ class HTTPClientPool:
|
||||
|
||||
# 添加代理配置
|
||||
proxy_url = build_proxy_url(proxy_config) if proxy_config else None
|
||||
if proxy_url:
|
||||
client_config["proxy"] = proxy_url
|
||||
proxy_param = _make_proxy_param(proxy_url)
|
||||
if proxy_param:
|
||||
client_config["proxy"] = proxy_param
|
||||
|
||||
client = httpx.AsyncClient(**client_config) # type: ignore[arg-type]
|
||||
cls._proxy_clients[cache_key] = (client, time.time())
|
||||
@@ -629,10 +667,15 @@ class HTTPClientPool:
|
||||
pool=config.http_pool_timeout,
|
||||
)
|
||||
|
||||
# 无特定代理时,回退到系统默认代理(与 get_proxy_client 行为一致)
|
||||
if proxy_config is None:
|
||||
proxy_config = get_system_proxy_config()
|
||||
|
||||
# 添加代理配置
|
||||
proxy_url = build_proxy_url(proxy_config) if proxy_config else None
|
||||
if proxy_url:
|
||||
client_config["proxy"] = proxy_url
|
||||
proxy_param = _make_proxy_param(proxy_url)
|
||||
if proxy_param:
|
||||
client_config["proxy"] = proxy_param
|
||||
logger.debug(f"创建带代理的HTTP客户端(一次性): {proxy_config.get('url', 'unknown')}")
|
||||
|
||||
client_config.update(kwargs)
|
||||
|
||||
@@ -842,6 +842,12 @@ class ProxyNode(Base):
|
||||
total_requests = Column(BigInteger, default=0, nullable=False)
|
||||
avg_latency_ms = Column(Float, nullable=True)
|
||||
|
||||
# TLS 加密
|
||||
tls_enabled = Column(Boolean, default=False, nullable=False, comment="是否启用 TLS 加密")
|
||||
tls_cert_fingerprint = Column(
|
||||
String(128), nullable=True, comment="TLS 证书 SHA-256 指纹(hex)"
|
||||
)
|
||||
|
||||
# 管理端远程配置(通过心跳下发给 aether-proxy)
|
||||
remote_config = Column(
|
||||
JSON,
|
||||
|
||||
@@ -213,7 +213,7 @@ def _parse_session_user_id(cookie_input: str) -> tuple[str | None, str | None]:
|
||||
|
||||
|
||||
async def _get_acw_cookie(
|
||||
base_url: str, timeout: float = 10, proxy: str | None = None
|
||||
base_url: str, timeout: float = 10, proxy: str | httpx.Proxy | None = None
|
||||
) -> str | None:
|
||||
"""
|
||||
获取 acw_sc__v2 Cookie
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProviderConnector(ABC):
|
||||
# 代理配置(支持 proxy_node_id 和旧的 proxy URL)
|
||||
from src.clients.http_client import resolve_ops_proxy
|
||||
|
||||
self._proxy: str | None = resolve_ops_proxy(self.config)
|
||||
self._proxy: str | httpx.Proxy | None = resolve_ops_proxy(self.config)
|
||||
|
||||
# HTTP 客户端配置
|
||||
self._timeout = self.config.get("timeout", 30)
|
||||
|
||||
@@ -5,6 +5,8 @@ SSL 工具函数
|
||||
|
||||
import ssl
|
||||
|
||||
from loguru import logger
|
||||
|
||||
try:
|
||||
import certifi
|
||||
|
||||
@@ -12,6 +14,8 @@ try:
|
||||
except ImportError:
|
||||
_SSL_CONTEXT = ssl.create_default_context()
|
||||
|
||||
_PROXY_SSL_CONTEXT: ssl.SSLContext | None = None
|
||||
|
||||
|
||||
def get_ssl_context() -> ssl.SSLContext:
|
||||
"""
|
||||
@@ -24,3 +28,30 @@ def get_ssl_context() -> ssl.SSLContext:
|
||||
ssl.SSLContext: SSL 上下文
|
||||
"""
|
||||
return _SSL_CONTEXT
|
||||
|
||||
|
||||
def get_proxy_ssl_context(expected_fingerprint: str | None = None) -> ssl.SSLContext:
|
||||
"""
|
||||
获取用于代理连接的 SSL 上下文(连接 aether-proxy TLS 端口)
|
||||
|
||||
当前使用 CERT_NONE(不验证证书),因为 aether-proxy 使用自签名证书。
|
||||
expected_fingerprint 参数预留供未来实现指纹校验。
|
||||
|
||||
Args:
|
||||
expected_fingerprint: 预期的证书 SHA-256 指纹(hex,预留参数)
|
||||
|
||||
Returns:
|
||||
ssl.SSLContext: 代理专用 SSL 上下文
|
||||
"""
|
||||
global _PROXY_SSL_CONTEXT
|
||||
|
||||
if expected_fingerprint:
|
||||
logger.warning("TLS 证书指纹校验尚未实现, fingerprint={} 被忽略", expected_fingerprint)
|
||||
|
||||
if _PROXY_SSL_CONTEXT is None:
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
_PROXY_SSL_CONTEXT = ctx
|
||||
# TODO: 实现基于 expected_fingerprint 的证书指纹校验
|
||||
return _PROXY_SSL_CONTEXT
|
||||
|
||||
Reference in New Issue
Block a user