Improve tunnel proxy diagnostics and request body spooling

This commit is contained in:
fawney19
2026-04-18 21:13:37 +08:00
parent 3363592751
commit f2a3836877
5 changed files with 712 additions and 260 deletions

View File

@@ -913,28 +913,51 @@ fn proxy_reference_matches_node_id(value: Option<&Value>, node_id: &str) -> bool
.is_some_and(|value| value == node_id)
}
fn build_proxy_connectivity_result(
probe_url: &str,
timeout_secs: u64,
success: bool,
latency_ms: Option<u64>,
exit_ip: Option<String>,
error: Option<String>,
) -> Value {
json!({
"success": success,
"latency_ms": latency_ms,
"exit_ip": exit_ip,
"error": error,
"probe_url": probe_url.trim(),
"timeout_secs": timeout_secs,
})
}
async fn test_proxy_node_connectivity(
state: &AdminAppState<'_>,
node: &aether_data::repository::proxy_nodes::StoredProxyNode,
) -> Value {
let probe_url = proxy_connectivity_probe_url();
if node.is_manual {
let Some(proxy_url) = node.proxy_url.as_deref() else {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": "手动节点缺少 proxy_url",
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some("手动节点缺少 proxy_url".to_string()),
);
};
let endpoint = match parse_manual_proxy_endpoint(proxy_url, "proxy_url") {
Ok(endpoint) => endpoint,
Err(detail) => {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": detail,
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some(detail),
);
}
};
let proxy_url = proxy_url_with_auth(
@@ -947,83 +970,118 @@ async fn test_proxy_node_connectivity(
}
if !node.tunnel_mode {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": "non-tunnel mode is no longer supported, please upgrade aether-proxy to use tunnel mode",
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some(
"non-tunnel mode is no longer supported, please upgrade aether-proxy to use tunnel mode"
.to_string(),
),
);
}
if !node.status.eq_ignore_ascii_case("online") || !node.tunnel_connected {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": "tunnel 未连接",
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some("tunnel 未连接".to_string()),
);
}
let probe_url = proxy_connectivity_probe_url();
match probe_tunnel_proxy_connectivity(state.app(), &node.id, &probe_url).await {
match probe_tunnel_proxy_connectivity(
state.app(),
&node.id,
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
)
.await
{
Ok(result) => {
if let Ok(status) = reqwest::StatusCode::from_u16(result.status) {
if status.is_success() {
return json!({
"success": true,
"latency_ms": result.latency_ms,
"exit_ip": parse_proxy_probe_exit_ip(&result.body),
"error": null,
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
true,
Some(result.latency_ms),
parse_proxy_probe_exit_ip(&result.body),
None,
);
}
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_proxy_probe_status_error(status, &result.body)),
});
return build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some(sanitize_proxy_error(&format_proxy_probe_status_error(
status,
&result.body,
))),
);
}
json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": format!("代理探测返回非法状态码: {}", result.status),
})
build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some(format!("代理探测返回非法状态码: {}", result.status)),
)
}
Err(error) => json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&error),
}),
Err(error) => build_proxy_connectivity_result(
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
false,
None,
None,
Some(sanitize_proxy_error(&error)),
),
}
}
async fn test_manual_proxy_connectivity(proxy_url: &str) -> Value {
let probe_url = proxy_connectivity_probe_url();
test_manual_proxy_connectivity_with_probe_url(proxy_url, &probe_url).await
test_manual_proxy_connectivity_with_probe_url(
proxy_url,
&probe_url,
PROXY_CONNECTIVITY_TIMEOUT_SECS,
)
.await
}
async fn test_manual_proxy_connectivity_with_probe_url(proxy_url: &str, probe_url: &str) -> Value {
async fn test_manual_proxy_connectivity_with_probe_url(
proxy_url: &str,
probe_url: &str,
timeout_secs: u64,
) -> Value {
let started_at = Instant::now();
let proxy = match reqwest::Proxy::all(proxy_url) {
Ok(proxy) => proxy,
Err(error) => {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_upstream_request_error(&error)),
});
return build_proxy_connectivity_result(
probe_url,
timeout_secs,
false,
None,
None,
Some(sanitize_proxy_error(&format_upstream_request_error(&error))),
);
}
};
let mut builder = reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(PROXY_CONNECTIVITY_TIMEOUT_SECS))
.timeout(Duration::from_secs(timeout_secs))
.proxy(proxy)
.user_agent("aether-gateway/proxy-connectivity");
if proxy_url
@@ -1036,54 +1094,66 @@ async fn test_manual_proxy_connectivity_with_probe_url(proxy_url: &str, probe_ur
let client = match builder.build() {
Ok(client) => client,
Err(error) => {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_upstream_request_error(&error)),
});
return build_proxy_connectivity_result(
probe_url,
timeout_secs,
false,
None,
None,
Some(sanitize_proxy_error(&format_upstream_request_error(&error))),
);
}
};
let response = match client.get(probe_url).send().await {
Ok(response) => response,
Err(error) => {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_upstream_request_error(&error)),
});
return build_proxy_connectivity_result(
probe_url,
timeout_secs,
false,
None,
None,
Some(sanitize_proxy_error(&format_upstream_request_error(&error))),
);
}
};
let status = response.status();
let body = match response.text().await {
Ok(body) => body,
Err(error) => {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_upstream_request_error(&error)),
});
return build_proxy_connectivity_result(
probe_url,
timeout_secs,
false,
None,
None,
Some(sanitize_proxy_error(&format_upstream_request_error(&error))),
);
}
};
if !status.is_success() {
return json!({
"success": false,
"latency_ms": null,
"exit_ip": null,
"error": sanitize_proxy_error(&format_proxy_probe_status_error(status, &body)),
});
return build_proxy_connectivity_result(
probe_url,
timeout_secs,
false,
None,
None,
Some(sanitize_proxy_error(&format_proxy_probe_status_error(
status, &body,
))),
);
}
json!({
"success": true,
"latency_ms": started_at.elapsed().as_millis() as u64,
"exit_ip": parse_proxy_probe_exit_ip(&body),
"error": null,
})
build_proxy_connectivity_result(
probe_url,
timeout_secs,
true,
Some(started_at.elapsed().as_millis() as u64),
parse_proxy_probe_exit_ip(&body),
None,
)
}
struct TunnelConnectivityProbeResult {
@@ -1096,6 +1166,7 @@ async fn probe_tunnel_proxy_connectivity(
state: &crate::AppState,
node_id: &str,
probe_url: &str,
timeout_secs: u64,
) -> Result<TunnelConnectivityProbeResult, String> {
let trimmed_node_id = node_id.trim();
if trimmed_node_id.is_empty() {
@@ -1103,7 +1174,13 @@ async fn probe_tunnel_proxy_connectivity(
}
if state.tunnel.has_local_proxy(trimmed_node_id) {
return probe_tunnel_proxy_connectivity_locally(state, trimmed_node_id, probe_url).await;
return probe_tunnel_proxy_connectivity_locally(
state,
trimmed_node_id,
probe_url,
timeout_secs,
)
.await;
}
if let Some(owner) = state
@@ -1117,6 +1194,7 @@ async fn probe_tunnel_proxy_connectivity(
state,
trimmed_node_id,
probe_url,
timeout_secs,
&owner.relay_base_url,
&owner.gateway_instance_id,
)
@@ -1130,18 +1208,19 @@ async fn probe_tunnel_proxy_connectivity(
.map_err(|err| format!("clear stale local tunnel attachment failed: {err}"))?;
}
probe_tunnel_proxy_connectivity_locally(state, trimmed_node_id, probe_url).await
probe_tunnel_proxy_connectivity_locally(state, trimmed_node_id, probe_url, timeout_secs).await
}
async fn probe_tunnel_proxy_connectivity_locally(
state: &crate::AppState,
node_id: &str,
probe_url: &str,
timeout_secs: u64,
) -> Result<TunnelConnectivityProbeResult, String> {
let started_at = Instant::now();
let result = state
.tunnel
.probe_node_url_with_response(node_id, probe_url, PROXY_CONNECTIVITY_TIMEOUT_SECS)
.probe_node_url_with_response(node_id, probe_url, timeout_secs)
.await?;
Ok(TunnelConnectivityProbeResult {
status: result.status,
@@ -1154,6 +1233,7 @@ async fn probe_tunnel_proxy_connectivity_via_owner(
state: &crate::AppState,
node_id: &str,
probe_url: &str,
timeout_secs: u64,
relay_base_url: &str,
owner_instance_id: &str,
) -> Result<TunnelConnectivityProbeResult, String> {
@@ -1171,8 +1251,8 @@ async fn probe_tunnel_proxy_connectivity_via_owner(
state.tunnel.local_instance_id(),
)
.header(TUNNEL_RELAY_OWNER_INSTANCE_HEADER, owner_instance_id)
.timeout(Duration::from_secs(PROXY_CONNECTIVITY_TIMEOUT_SECS))
.body(build_tunnel_probe_relay_envelope(probe_url)?)
.timeout(Duration::from_secs(timeout_secs))
.body(build_tunnel_probe_relay_envelope(probe_url, timeout_secs)?)
.send()
.await
.map_err(|error| format!("owner tunnel relay probe failed: {error}"))?;
@@ -1195,12 +1275,15 @@ async fn probe_tunnel_proxy_connectivity_via_owner(
})
}
fn build_tunnel_probe_relay_envelope(probe_url: &str) -> Result<Vec<u8>, String> {
fn build_tunnel_probe_relay_envelope(
probe_url: &str,
timeout_secs: u64,
) -> Result<Vec<u8>, String> {
let meta = crate::tunnel::tunnel_protocol::RequestMeta {
method: "GET".to_string(),
url: probe_url.trim().to_string(),
headers: std::collections::HashMap::new(),
timeout: PROXY_CONNECTIVITY_TIMEOUT_SECS,
timeout: timeout_secs,
follow_redirects: Some(false),
http1_only: false,
};

View File

@@ -943,6 +943,11 @@ async fn gateway_tests_disconnected_tunnel_proxy_nodes_locally() {
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], false);
assert_eq!(payload["error"], "tunnel 未连接");
assert_eq!(
payload["probe_url"],
"https://www.cloudflare.com/cdn-cgi/trace"
);
assert_eq!(payload["timeout_secs"], 10);
gateway_handle.abort();
}
@@ -1072,6 +1077,8 @@ async fn gateway_tests_connected_tunnel_proxy_nodes_with_active_probe() {
assert_eq!(payload["success"], true);
assert!(payload["latency_ms"].is_u64());
assert_eq!(payload["exit_ip"], "203.0.113.10");
assert_eq!(payload["probe_url"], "https://probe.example/cdn-cgi/trace");
assert_eq!(payload["timeout_secs"], 10);
gateway_handle.abort();
}