feat(proxy): 实现代理节点批量升级回滚、隧道重定向跟随及远程配置管理

核心功能:
- 新增代理节点批量升级回滚工作流,支持分批升级、健康探针、跳过/重试/取消等操作
- proxy 隧道流处理器支持 HTTP 重定向跟随(最多 10 跳),区分 307/308 可重播与不可重播请求体
- proxy 协议新增 follow_redirects / http1_only 字段,网关侧同步支持
- 新增代理节点远端配置变更接口(名称、允许端口、调度状态、升级目标等)
- 新增代理节点注册/反注册/心跳的 Admin API,及节点过期清理维护任务
- gateway 隧道 owner-relay 支持流式代理大请求体,新增 5 MiB 默认限制
- 新增 ProxyNodeRegistrationMutation / ProxyNodeRemoteConfigMutation 数据类型
- proxy 配置新增重定向重播预算、心跳间隔等参数,TUI 安装向导同步更新
- 前端 ProxyNodes 页面新增批量升级操作面板及滚动进度展示
This commit is contained in:
fawney19
2026-04-12 16:02:38 +08:00
parent 7c5bb7f383
commit 9703840a36
83 changed files with 11832 additions and 1520 deletions

View File

@@ -51,6 +51,7 @@ use crate::{
use axum::body::{to_bytes, Body, Bytes};
use axum::extract::{ConnectInfo, Request, State};
use axum::http::{self, header::HeaderName, header::HeaderValue, Response};
use sha2::{Digest, Sha256};
use std::time::Instant;
use tracing::{debug, info, warn};
@@ -96,6 +97,141 @@ fn execution_runtime_candidate_header_value(decision: &GatewayControlDecision) -
}
}
fn extract_management_token_bearer(headers: &http::HeaderMap) -> Option<String> {
let header = crate::headers::header_value_str(headers, http::header::AUTHORIZATION.as_str())?;
let token = header
.strip_prefix("Bearer ")
.or_else(|| header.strip_prefix("bearer "))?
.trim()
.to_string();
(!token.is_empty() && token.starts_with("ae_")).then_some(token)
}
fn hash_management_token(value: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
format!("{:x}", hasher.finalize())
}
fn remote_ip_allowed(allowed_ips: Option<&serde_json::Value>, remote_ip: std::net::IpAddr) -> bool {
let Some(allowed_ips) = allowed_ips else {
return true;
};
let Some(items) = allowed_ips.as_array() else {
return false;
};
if items.is_empty() {
return false;
}
items
.iter()
.filter_map(serde_json::Value::as_str)
.any(|value| ip_or_cidr_matches(value, remote_ip))
}
fn ip_or_cidr_matches(pattern: &str, remote_ip: std::net::IpAddr) -> bool {
let pattern = pattern.trim();
if pattern.is_empty() {
return false;
}
if let Ok(ip) = pattern.parse::<std::net::IpAddr>() {
return ip == remote_ip;
}
let Some((network, prefix)) = pattern.split_once('/') else {
return false;
};
let Ok(prefix) = prefix.trim().parse::<u8>() else {
return false;
};
match (network.trim().parse::<std::net::IpAddr>(), remote_ip) {
(Ok(std::net::IpAddr::V4(network)), std::net::IpAddr::V4(remote)) if prefix <= 32 => {
let mask = if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
};
(u32::from(network) & mask) == (u32::from(remote) & mask)
}
(Ok(std::net::IpAddr::V6(network)), std::net::IpAddr::V6(remote)) if prefix <= 128 => {
let mask = if prefix == 0 {
0
} else {
u128::MAX << (128 - prefix)
};
(u128::from(network) & mask) == (u128::from(remote) & mask)
}
_ => false,
}
}
async fn maybe_promote_management_token_admin_principal(
state: &AppState,
remote_addr: &std::net::SocketAddr,
headers: &http::HeaderMap,
trace_id: &str,
request_context: &mut GatewayPublicRequestContext,
) -> Result<(), GatewayError> {
let Some(decision) = request_context.control_decision.as_mut() else {
return Ok(());
};
if decision.route_class.as_deref() != Some("admin_proxy") || decision.admin_principal.is_some()
{
return Ok(());
}
let Some(token) = extract_management_token_bearer(headers) else {
return Ok(());
};
let token_hash = hash_management_token(&token);
let Some(token_with_user) = state
.get_management_token_with_user_by_hash(&token_hash)
.await?
else {
return Ok(());
};
if !token_with_user.token.is_active {
return Ok(());
}
if token_with_user
.token
.expires_at_unix_secs
.is_some_and(|value| value <= chrono::Utc::now().timestamp().max(0) as u64)
{
return Ok(());
}
if !remote_ip_allowed(token_with_user.token.allowed_ips.as_ref(), remote_addr.ip()) {
return Ok(());
}
let Some(user) = state.find_user_auth_by_id(&token_with_user.user.id).await? else {
return Ok(());
};
if !user.is_active || user.is_deleted || !user.role.eq_ignore_ascii_case("admin") {
return Ok(());
}
decision.admin_principal = Some(crate::control::GatewayAdminPrincipalContext {
user_id: user.id.clone(),
user_role: user.role.clone(),
session_id: None,
management_token_id: Some(token_with_user.token.id.clone()),
});
let remote_ip = remote_addr.ip().to_string();
if let Err(err) = state
.record_management_token_usage(&token_with_user.token.id, Some(remote_ip.as_str()))
.await
{
warn!(
trace_id = %trace_id,
token_id = %token_with_user.token.id,
error = ?err,
"gateway failed to record management token usage"
);
}
Ok(())
}
async fn maybe_forward_public_request_to_tunnel_owner(
state: &AppState,
remote_addr: &std::net::SocketAddr,
@@ -379,7 +515,7 @@ pub(crate) async fn proxy_request(
));
}
let request_context_started_at = Instant::now();
let request_context = crate::control::resolve_public_request_context(
let mut request_context = crate::control::resolve_public_request_context(
&state,
&parts.method,
&parts.uri,
@@ -387,6 +523,14 @@ pub(crate) async fn proxy_request(
&trace_id,
)
.await?;
maybe_promote_management_token_admin_principal(
&state,
&remote_addr,
&parts.headers,
&trace_id,
&mut request_context,
)
.await?;
let request_context_ms = request_context_started_at.elapsed().as_millis() as u64;
if request_context
.control_decision