mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
fix(network): preserve remote DNS semantics for SOCKS proxies
This commit is contained in:
@@ -5036,14 +5036,38 @@ fn resolve_proxy_url(
|
||||
.map(|url| url.trim())
|
||||
.filter(|url| !url.is_empty())
|
||||
{
|
||||
validate_execution_proxy_url(proxy_url)?;
|
||||
return Ok(Some(proxy_url.to_string()));
|
||||
return normalize_execution_proxy_url(proxy_url).map(Some);
|
||||
}
|
||||
|
||||
Err(ExecutionRuntimeTransportError::ProxyUnsupported)
|
||||
}
|
||||
|
||||
fn validate_execution_proxy_url(raw_url: &str) -> Result<(), ExecutionRuntimeTransportError> {
|
||||
parse_execution_proxy_url(raw_url).map(|_| ())
|
||||
}
|
||||
|
||||
/// Normalize a configured proxy URL before handing it to reqwest/wreq.
|
||||
///
|
||||
/// `socks5://` has a particularly dangerous ambiguity in a gateway: reqwest
|
||||
/// and wreq interpret it as *local* target-name resolution, while
|
||||
/// `socks5h://` delegates target resolution to the proxy. Local resolution
|
||||
/// would bypass the execution DNS guard (and could turn a rebinding hostname
|
||||
/// into a private address). Keep accepting the established `socks5` config
|
||||
/// syntax for compatibility, but make its runtime semantics the safe remote
|
||||
/// DNS variant. HTTP/HTTPS and already-remote `socks5h` URLs are unchanged.
|
||||
pub(crate) fn normalize_execution_proxy_url(
|
||||
raw_url: &str,
|
||||
) -> Result<String, ExecutionRuntimeTransportError> {
|
||||
let mut parsed = parse_execution_proxy_url(raw_url)?;
|
||||
if parsed.scheme().eq_ignore_ascii_case("socks5") {
|
||||
parsed
|
||||
.set_scheme("socks5h")
|
||||
.map_err(|_| ExecutionRuntimeTransportError::ProxyUnsupported)?;
|
||||
}
|
||||
Ok(parsed.to_string())
|
||||
}
|
||||
|
||||
fn parse_execution_proxy_url(raw_url: &str) -> Result<url::Url, ExecutionRuntimeTransportError> {
|
||||
let parsed =
|
||||
url::Url::parse(raw_url).map_err(|_| ExecutionRuntimeTransportError::ProxyUnsupported)?;
|
||||
if !matches!(parsed.scheme(), "http" | "https" | "socks5" | "socks5h")
|
||||
@@ -5054,7 +5078,7 @@ fn validate_execution_proxy_url(raw_url: &str) -> Result<(), ExecutionRuntimeTra
|
||||
{
|
||||
return Err(ExecutionRuntimeTransportError::ProxyUnsupported);
|
||||
}
|
||||
Ok(())
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub(crate) fn build_request_headers(
|
||||
@@ -5627,6 +5651,25 @@ mod tests {
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_proxy_url_normalizes_local_socks_dns_to_remote_dns() {
|
||||
assert_eq!(
|
||||
super::normalize_execution_proxy_url("socks5://alice:password@proxy.example.test:1080")
|
||||
.expect("socks5 URL should normalize"),
|
||||
"socks5h://alice:password@proxy.example.test:1080"
|
||||
);
|
||||
assert_eq!(
|
||||
super::normalize_execution_proxy_url("socks5h://proxy.example.test:1080")
|
||||
.expect("socks5h URL should remain valid"),
|
||||
"socks5h://proxy.example.test:1080"
|
||||
);
|
||||
assert_eq!(
|
||||
super::normalize_execution_proxy_url("https://proxy.example.test:8443")
|
||||
.expect("https URL should remain valid"),
|
||||
"https://proxy.example.test:8443/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_error_kind_accepts_only_protocol_categories() {
|
||||
assert_eq!(super::sanitize_relay_error_kind("TIMEOUT"), "timeout");
|
||||
|
||||
@@ -1272,7 +1272,25 @@ async fn test_manual_proxy_connectivity_with_probe_url(
|
||||
timeout_secs: u64,
|
||||
) -> Value {
|
||||
let started_at = Instant::now();
|
||||
let proxy = match reqwest::Proxy::all(proxy_url) {
|
||||
// reqwest resolves the destination locally for `socks5://`, which would
|
||||
// bypass the gateway's private-address/DNS-rebinding guard. Normalize
|
||||
// legacy SOCKS URLs to the remote-DNS form before probing; HTTP/HTTPS and
|
||||
// already-normalized `socks5h://` URLs are unchanged.
|
||||
let proxy_url =
|
||||
match crate::execution_runtime::transport::normalize_execution_proxy_url(proxy_url) {
|
||||
Ok(proxy_url) => proxy_url,
|
||||
Err(_) => {
|
||||
return build_proxy_connectivity_result(
|
||||
probe_url,
|
||||
timeout_secs,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
Some("代理 URL 无效".to_string()),
|
||||
);
|
||||
}
|
||||
};
|
||||
let proxy = match reqwest::Proxy::all(&proxy_url) {
|
||||
Ok(proxy) => proxy,
|
||||
Err(error) => {
|
||||
return build_proxy_connectivity_result(
|
||||
|
||||
@@ -23,7 +23,8 @@ use wreq::ws::message::{CloseFrame as WreqCloseFrame, Message as WreqWsMessage};
|
||||
|
||||
use crate::ai_serving::AiExecutionDecision;
|
||||
use crate::execution_runtime::transport::{
|
||||
build_browser_wreq_client, build_request_headers, ExecutionTransportControls,
|
||||
build_browser_wreq_client, build_request_headers, normalize_execution_proxy_url,
|
||||
ExecutionTransportControls,
|
||||
};
|
||||
use crate::frontdoor_loop_guard::gateway_frontdoor_self_loop_guard_error;
|
||||
use crate::handlers::proxy::websocket::session::{
|
||||
@@ -287,10 +288,10 @@ async fn build_websocket_client(
|
||||
builder.build().map_err(|_| errors.client_build_failed)
|
||||
}
|
||||
|
||||
fn resolve_websocket_proxy_url<'a>(
|
||||
proxy: Option<&'a ProxySnapshot>,
|
||||
fn resolve_websocket_proxy_url(
|
||||
proxy: Option<&ProxySnapshot>,
|
||||
errors: UpstreamWebSocketErrorCodes,
|
||||
) -> Result<Option<&'a str>, &'static str> {
|
||||
) -> Result<Option<String>, &'static str> {
|
||||
let Some(proxy) = proxy else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -323,7 +324,9 @@ fn resolve_websocket_proxy_url<'a>(
|
||||
{
|
||||
return Err(errors.proxy_invalid);
|
||||
}
|
||||
return Ok(Some(proxy_url));
|
||||
let normalized =
|
||||
normalize_execution_proxy_url(proxy_url).map_err(|_| errors.proxy_invalid)?;
|
||||
return Ok(Some(normalized));
|
||||
}
|
||||
if proxy.node_id.is_some() || proxy.mode.as_deref() == Some("tunnel") {
|
||||
return Err(errors.tunnel_proxy_unsupported);
|
||||
@@ -976,7 +979,19 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_websocket_proxy_url(Some(&authenticated_node), errors),
|
||||
Ok(Some("http://alice:password@proxy.example:8080"))
|
||||
Ok(Some(
|
||||
"http://alice:password@proxy.example:8080/".to_string()
|
||||
))
|
||||
);
|
||||
|
||||
let socks = ProxySnapshot {
|
||||
enabled: Some(true),
|
||||
url: Some("socks5://proxy.example:1080".to_string()),
|
||||
..ProxySnapshot::default()
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_websocket_proxy_url(Some(&socks), errors),
|
||||
Ok(Some("socks5h://proxy.example:1080".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user