mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: delegate 客户端替换为 hyper 原生实现,新增代理管理功能
aether-proxy: - 用 hyper-util Client + 自定义 InstrumentedConnector 替换 reqwest delegate 客户端 - 支持 HTTP/HTTPS 自动 TLS,ALPN h2 协商,connect/tls 分阶段计时 - ConnectTiming 通过 hyper extensions 传递,上游响应细分 connect_ms/tls_ms/ttfb_ms - upgrade 命令在非 root 下跳过 systemd restart 并提示手动操作 后端: - 新增 /admin/proxy-nodes/test-url 接口,支持直接测试代理 URL 连通性 - 新增 /admin/proxy-nodes/hmac-key 接口,获取 HMAC Key 供部署使用 - 提取 _test_proxy_connectivity 公共函数,消除 test_node 中的重复代码 - candidate_resolver 在 extra_data 中输出 needs_conversion/provider_api_format - stats_aggregator 小时聚合增加 IntegrityError 冲突重试 前端: - 请求时间线组件展示代理 timing 细分(DNS/连接/TLS/TTFB/上游处理) - 请求时间线增加格式转换分界标记和 conversion badge - ProxyNodes 页面新增代理 URL 测试和 HMAC Key 复制功能 - HardwareTooltip 从 Popover 改为 Tooltip 组件
This commit is contained in:
@@ -97,26 +97,7 @@ pub async fn run(mut config: Config) -> anyhow::Result<()> {
|
||||
let dynamic = Arc::new(RwLock::new(DynamicConfig::from_config(&config)));
|
||||
|
||||
// Build delegate HTTP client (for proxy-initiated upstream requests).
|
||||
// No overall timeout — SSE streams can last indefinitely.
|
||||
// Connect timeout limits connection establishment; Aether controls
|
||||
// first-byte / idle timeouts on its own side.
|
||||
let mut delegate_builder = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(config.delegate_connect_timeout_secs))
|
||||
.pool_max_idle_per_host(config.delegate_pool_max_idle_per_host)
|
||||
.pool_idle_timeout(Duration::from_secs(config.delegate_pool_idle_timeout_secs))
|
||||
.tcp_nodelay(config.delegate_tcp_nodelay);
|
||||
|
||||
if config.delegate_tcp_keepalive_secs > 0 {
|
||||
delegate_builder = delegate_builder.tcp_keepalive(Some(Duration::from_secs(
|
||||
config.delegate_tcp_keepalive_secs,
|
||||
)));
|
||||
} else {
|
||||
delegate_builder = delegate_builder.tcp_keepalive(None);
|
||||
}
|
||||
|
||||
let delegate_client = delegate_builder
|
||||
.build()
|
||||
.expect("failed to create delegate HTTP client");
|
||||
let delegate_client = proxy::delegate_client::build_delegate_client(&config);
|
||||
|
||||
// Build shared application state
|
||||
let state = Arc::new(AppState {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::error::Error as StdError;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use futures_util::StreamExt;
|
||||
use http_body_util::{BodyExt, Full, Limited, StreamBody};
|
||||
use hyper::body::{Frame, Incoming};
|
||||
use hyper::{Request, Response};
|
||||
use hyper::header::{HeaderName, HeaderValue};
|
||||
use hyper::{Method, Request, Response, Uri};
|
||||
use tracing::{debug, warn};
|
||||
use url::Url;
|
||||
|
||||
use super::BoxBody;
|
||||
use crate::auth;
|
||||
use crate::config::Config;
|
||||
use crate::proxy::delegate_client::{ConnectTiming, DelegateClient};
|
||||
use crate::proxy::target_filter::{self, DnsCache};
|
||||
|
||||
/// Handle delegation requests: Aether sends a full request description,
|
||||
@@ -35,7 +38,7 @@ pub async fn handle_delegate(
|
||||
allowed_ports: &HashSet<u16>,
|
||||
timestamp_tolerance: u64,
|
||||
dns_cache: &DnsCache,
|
||||
http_client: &reqwest::Client,
|
||||
http_client: &DelegateClient,
|
||||
) -> Response<BoxBody> {
|
||||
let total_start = Instant::now();
|
||||
|
||||
@@ -150,7 +153,7 @@ pub async fn handle_delegate(
|
||||
debug!(method = %method_str, url = %target_url, is_gzip, "delegate request");
|
||||
|
||||
// ── Build upstream request ──
|
||||
let method = match method_str.parse::<reqwest::Method>() {
|
||||
let method = match method_str.parse::<Method>() {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!(error = %e, method = %method_str, "delegate invalid HTTP method");
|
||||
@@ -158,34 +161,32 @@ pub async fn handle_delegate(
|
||||
}
|
||||
};
|
||||
|
||||
let mut upstream_req = http_client.request(method, &target_url);
|
||||
|
||||
// Set headers (skip `host` — reqwest sets it from the URL automatically,
|
||||
// and a duplicate Host header can confuse certain upstreams)
|
||||
for (name, value) in &upstream_headers {
|
||||
if name.eq_ignore_ascii_case("host") {
|
||||
continue;
|
||||
let uri = match target_url.parse::<Uri>() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
warn!(error = %e, url = %target_url, "delegate invalid target URI");
|
||||
return error_response(400, "bad_request", &format!("invalid URL: {}", e));
|
||||
}
|
||||
upstream_req = upstream_req.header(name.as_str(), value.as_str());
|
||||
}
|
||||
};
|
||||
|
||||
// ── Stream body passthrough ──
|
||||
// When body is gzip-compressed, forward it directly to upstream with
|
||||
// Content-Encoding: gzip header — no collect/decompress needed.
|
||||
// All major AI API providers (Anthropic, OpenAI, Google) accept gzip request bodies.
|
||||
let wire_size: u64;
|
||||
let upstream_body: BoxBody;
|
||||
if is_gzip {
|
||||
// Passthrough: stream the gzip body directly to upstream
|
||||
upstream_req = upstream_req.header("content-encoding", "gzip");
|
||||
let body_stream = req.into_body();
|
||||
let byte_stream = http_body_util::BodyStream::new(body_stream).filter_map(|result| async {
|
||||
match result {
|
||||
Ok(frame) => frame.into_data().ok().map(Ok),
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
});
|
||||
let reqwest_body = reqwest::Body::wrap_stream(byte_stream);
|
||||
upstream_req = upstream_req.body(reqwest_body);
|
||||
let body_stream =
|
||||
http_body_util::BodyStream::new(req.into_body()).filter_map(|result| async {
|
||||
match result {
|
||||
Ok(frame) => frame.into_data().ok().map(|data| {
|
||||
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(Frame::data(data))
|
||||
}),
|
||||
Err(e) => Some(Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)),
|
||||
}
|
||||
});
|
||||
let stream_body = StreamBody::new(body_stream);
|
||||
upstream_body = BodyExt::boxed(stream_body);
|
||||
// wire_size will be reported from Content-Length if available, otherwise 0
|
||||
wire_size = req_content_length;
|
||||
} else {
|
||||
@@ -199,41 +200,85 @@ pub async fn handle_delegate(
|
||||
}
|
||||
};
|
||||
wire_size = body_bytes.len() as u64;
|
||||
if !body_bytes.is_empty() {
|
||||
upstream_req = upstream_req.body(body_bytes.to_vec());
|
||||
let body = Full::new(body_bytes)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { match e {} })
|
||||
.boxed();
|
||||
upstream_body = body;
|
||||
}
|
||||
|
||||
let mut upstream_req = Request::new(upstream_body);
|
||||
*upstream_req.method_mut() = method;
|
||||
*upstream_req.uri_mut() = uri;
|
||||
|
||||
{
|
||||
let headers = upstream_req.headers_mut();
|
||||
// Set headers (skip `host` — hyper sets it from the URI automatically,
|
||||
// and a duplicate Host header can confuse certain upstreams)
|
||||
for (name, value) in &upstream_headers {
|
||||
if name.eq_ignore_ascii_case("host") {
|
||||
continue;
|
||||
}
|
||||
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
warn!(header = %name, "delegate invalid header name");
|
||||
return error_response(400, "bad_request", "invalid header name");
|
||||
}
|
||||
};
|
||||
let header_value = match HeaderValue::from_str(value) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
warn!(header = %name, "delegate invalid header value");
|
||||
return error_response(400, "bad_request", "invalid header value");
|
||||
}
|
||||
};
|
||||
headers.insert(header_name, header_value);
|
||||
}
|
||||
if is_gzip {
|
||||
headers.insert(
|
||||
hyper::header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static("gzip"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send upstream request ──
|
||||
// NOTE: We intentionally do NOT set a per-request timeout here.
|
||||
// reqwest's `.timeout()` caps the *entire* request including body streaming,
|
||||
// which would truncate long-lived SSE streams. The delegate_client already
|
||||
// has a configured connect_timeout for connection establishment, and Aether controls
|
||||
// Connect timeout limits connection establishment; Aether controls
|
||||
// first-byte / idle timeouts on its own side via asyncio.
|
||||
let upstream_start = Instant::now();
|
||||
let upstream_resp = match upstream_req.send().await {
|
||||
let upstream_resp = match http_client.request(upstream_req).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!(url = %target_url, error = %e, "delegate upstream request failed");
|
||||
let safe_detail = sanitize_upstream_error(&e.to_string());
|
||||
if e.is_timeout() {
|
||||
let safe_detail = sanitize_upstream_error(&root_error_message(&e));
|
||||
if is_timeout_error(&e) {
|
||||
return error_response(504, "upstream_timeout", &safe_detail);
|
||||
}
|
||||
return error_response(502, "upstream_connection_failed", &safe_detail);
|
||||
}
|
||||
};
|
||||
let upstream_ms = upstream_start.elapsed().as_millis() as u64;
|
||||
let ttfb_ms = upstream_start.elapsed().as_millis() as u64;
|
||||
|
||||
// ── Build response ──
|
||||
let status = upstream_resp.status().as_u16();
|
||||
let resp_headers = upstream_resp.headers().clone();
|
||||
let (connect_ms, tls_ms) = upstream_resp
|
||||
.extensions()
|
||||
.get::<ConnectTiming>()
|
||||
.map(|t| (t.connect_ms, t.tls_ms))
|
||||
.unwrap_or((0, 0));
|
||||
let upstream_processing_ms = ttfb_ms.saturating_sub(connect_ms.saturating_add(tls_ms));
|
||||
let total_ms = total_start.elapsed().as_millis() as u64;
|
||||
|
||||
debug!(
|
||||
url = %target_url,
|
||||
status,
|
||||
dns_ms,
|
||||
upstream_ms,
|
||||
connect_ms,
|
||||
tls_ms,
|
||||
ttfb_ms,
|
||||
upstream_processing_ms,
|
||||
total_ms,
|
||||
wire_size,
|
||||
is_gzip,
|
||||
@@ -246,16 +291,18 @@ pub async fn handle_delegate(
|
||||
"wire_size": wire_size,
|
||||
"passthrough": is_gzip,
|
||||
"dns_ms": dns_ms,
|
||||
"upstream_ms": upstream_ms,
|
||||
"connect_ms": connect_ms,
|
||||
"tls_ms": tls_ms,
|
||||
"ttfb_ms": ttfb_ms,
|
||||
"upstream_ms": ttfb_ms,
|
||||
"upstream_processing_ms": upstream_processing_ms,
|
||||
"total_ms": total_ms,
|
||||
});
|
||||
|
||||
let body_stream = upstream_resp
|
||||
.bytes_stream()
|
||||
.map_ok(Frame::data)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) });
|
||||
|
||||
let stream_body: BoxBody = BodyExt::boxed(StreamBody::new(body_stream));
|
||||
let stream_body: BoxBody = upstream_resp
|
||||
.into_body()
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })
|
||||
.boxed();
|
||||
|
||||
let mut builder = Response::builder().status(status);
|
||||
for (name, value) in resp_headers.iter() {
|
||||
@@ -271,6 +318,30 @@ pub async fn handle_delegate(
|
||||
})
|
||||
}
|
||||
|
||||
fn root_error_message(err: &dyn StdError) -> String {
|
||||
let mut current = err;
|
||||
while let Some(source) = current.source() {
|
||||
current = source;
|
||||
}
|
||||
current.to_string()
|
||||
}
|
||||
|
||||
fn is_timeout_error(err: &(dyn StdError + 'static)) -> bool {
|
||||
if err.is::<tokio::time::error::Elapsed>() {
|
||||
return true;
|
||||
}
|
||||
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
|
||||
if io_err.kind() == std::io::ErrorKind::TimedOut {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(source) = err.source() {
|
||||
// source() returns &(dyn Error + 'static), so this is safe
|
||||
return is_timeout_error(source);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── Sanitisation ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Strip full URLs from error messages to prevent leaking upstream API keys,
|
||||
|
||||
282
aether-proxy/src/proxy/delegate_client.rs
Normal file
282
aether-proxy/src/proxy/delegate_client.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use hyper::rt;
|
||||
use hyper::Uri;
|
||||
use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
|
||||
use rustls::ClientConfig;
|
||||
use rustls_pki_types::ServerName;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::proxy::BoxBody;
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
type DelegateStream = MaybeHttpsStream<TokioIo<tokio::net::TcpStream>>;
|
||||
|
||||
type DelegateConn = TimedConn<DelegateStream>;
|
||||
|
||||
pub(crate) type DelegateClient = Client<InstrumentedConnector, BoxBody>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct ConnectTiming {
|
||||
pub connect_ms: u64,
|
||||
pub tls_ms: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn build_delegate_client(config: &Config) -> DelegateClient {
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
http.set_connect_timeout(Some(Duration::from_secs(
|
||||
config.delegate_connect_timeout_secs,
|
||||
)));
|
||||
http.set_nodelay(config.delegate_tcp_nodelay);
|
||||
if config.delegate_tcp_keepalive_secs > 0 {
|
||||
http.set_keepalive(Some(Duration::from_secs(
|
||||
config.delegate_tcp_keepalive_secs,
|
||||
)));
|
||||
} else {
|
||||
http.set_keepalive(None);
|
||||
}
|
||||
|
||||
let connector = InstrumentedConnector {
|
||||
http,
|
||||
tls_config: build_tls_config(),
|
||||
};
|
||||
|
||||
let mut builder = Client::builder(TokioExecutor::new());
|
||||
builder.pool_max_idle_per_host(config.delegate_pool_max_idle_per_host);
|
||||
builder.pool_idle_timeout(Duration::from_secs(config.delegate_pool_idle_timeout_secs));
|
||||
builder.pool_timer(TokioTimer::new());
|
||||
|
||||
builder.build::<_, BoxBody>(connector)
|
||||
}
|
||||
|
||||
fn build_tls_config() -> Arc<ClientConfig> {
|
||||
let root_store =
|
||||
rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let mut config = ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct InstrumentedConnector {
|
||||
http: HttpConnector,
|
||||
tls_config: Arc<ClientConfig>,
|
||||
}
|
||||
|
||||
impl Service<Uri> for InstrumentedConnector {
|
||||
type Response = DelegateConn;
|
||||
type Error = BoxError;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, BoxError>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.http.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, dst: Uri) -> Self::Future {
|
||||
let scheme = dst.scheme_str().map(|s| s.to_ascii_lowercase());
|
||||
let tls_config = self.tls_config.clone();
|
||||
let connecting = self.http.call(dst.clone());
|
||||
let connect_start = Instant::now();
|
||||
|
||||
Box::pin(async move {
|
||||
match scheme.as_deref() {
|
||||
Some("http") => {
|
||||
let tcp = connecting.await.map_err(|e| Box::new(e) as BoxError)?;
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
Ok(TimedConn::new(
|
||||
MaybeHttpsStream::Http(tcp),
|
||||
ConnectTiming {
|
||||
connect_ms,
|
||||
tls_ms: 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
Some("https") => {
|
||||
let server_name = resolve_server_name(&dst)?;
|
||||
let tcp = connecting.await.map_err(|e| Box::new(e) as BoxError)?;
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
|
||||
let tls_start = Instant::now();
|
||||
let tls_stream = TlsConnector::from(tls_config)
|
||||
.connect(server_name, TokioIo::new(tcp))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let tls_ms = tls_start.elapsed().as_millis() as u64;
|
||||
|
||||
Ok(TimedConn::new(
|
||||
MaybeHttpsStream::Https(TokioIo::new(tls_stream)),
|
||||
ConnectTiming { connect_ms, tls_ms },
|
||||
))
|
||||
}
|
||||
Some(other) => {
|
||||
Err(std::io::Error::other(format!("unsupported scheme {other}")).into())
|
||||
}
|
||||
None => Err(std::io::Error::other("missing scheme").into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_server_name(uri: &Uri) -> Result<ServerName<'static>, BoxError> {
|
||||
let host = uri.host().ok_or("missing host")?;
|
||||
let host = host.trim_start_matches('[').trim_end_matches(']');
|
||||
Ok(ServerName::try_from(host.to_string())?)
|
||||
}
|
||||
|
||||
pub(crate) struct TimedConn<T> {
|
||||
inner: T,
|
||||
timing: ConnectTiming,
|
||||
}
|
||||
|
||||
impl<T> TimedConn<T> {
|
||||
fn new(inner: T, timing: ConnectTiming) -> Self {
|
||||
Self { inner, timing }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Connection> Connection for TimedConn<T> {
|
||||
fn connected(&self) -> Connected {
|
||||
self.inner.connected().extra(self.timing)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: rt::Read + Unpin> rt::Read for TimedConn<T> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: rt::ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: rt::Write + Unpin> rt::Write for TimedConn<T> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.inner.is_write_vectored()
|
||||
}
|
||||
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum MaybeHttpsStream<T> {
|
||||
Http(T),
|
||||
Https(TokioIo<tokio_rustls::client::TlsStream<TokioIo<T>>>),
|
||||
}
|
||||
|
||||
impl<T: rt::Read + rt::Write + Connection + Unpin> Connection for MaybeHttpsStream<T> {
|
||||
fn connected(&self) -> Connected {
|
||||
match self {
|
||||
Self::Http(stream) => stream.connected(),
|
||||
Self::Https(stream) => {
|
||||
let (tcp, tls) = stream.inner().get_ref();
|
||||
if tls.alpn_protocol() == Some(b"h2") {
|
||||
tcp.inner().connected().negotiated_h2()
|
||||
} else {
|
||||
tcp.inner().connected()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: rt::Read + rt::Write + Unpin> rt::Read for MaybeHttpsStream<T> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: rt::ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match Pin::get_mut(self) {
|
||||
Self::Http(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||
Self::Https(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: rt::Write + rt::Read + Unpin> rt::Write for MaybeHttpsStream<T> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
match Pin::get_mut(self) {
|
||||
Self::Http(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||
Self::Https(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
|
||||
match Pin::get_mut(self) {
|
||||
Self::Http(stream) => Pin::new(stream).poll_flush(cx),
|
||||
Self::Https(stream) => Pin::new(stream).poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match Pin::get_mut(self) {
|
||||
Self::Http(stream) => Pin::new(stream).poll_shutdown(cx),
|
||||
Self::Https(stream) => Pin::new(stream).poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
match self {
|
||||
Self::Http(stream) => stream.is_write_vectored(),
|
||||
Self::Https(stream) => stream.is_write_vectored(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_write_vectored(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
match Pin::get_mut(self) {
|
||||
Self::Http(stream) => Pin::new(stream).poll_write_vectored(cx, bufs),
|
||||
Self::Https(stream) => Pin::new(stream).poll_write_vectored(cx, bufs),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod connect;
|
||||
pub mod delegate;
|
||||
pub mod delegate_client;
|
||||
pub mod server;
|
||||
pub mod target_filter;
|
||||
pub mod tls;
|
||||
|
||||
@@ -343,9 +343,15 @@ pub async fn cmd_upgrade(version: Option<String>) -> anyhow::Result<()> {
|
||||
|
||||
// Restart systemd service if running
|
||||
if super::service::is_service_active() {
|
||||
eprintln!(" Restarting systemd service...");
|
||||
super::service::run_cmd("systemctl", &["restart", "aether-proxy"])?;
|
||||
eprintln!(" Service restarted.");
|
||||
if super::service::is_root() {
|
||||
eprintln!(" Restarting systemd service...");
|
||||
super::service::run_cmd("systemctl", &["restart", "aether-proxy"])?;
|
||||
eprintln!(" Service restarted.");
|
||||
} else {
|
||||
eprintln!(" Systemd service is active, but restart requires root.");
|
||||
eprintln!(" Run: sudo aether-proxy restart");
|
||||
eprintln!(" Skipping restart.");
|
||||
}
|
||||
} else {
|
||||
eprintln!(" No active systemd service detected, skipping restart.");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::hardware::HardwareInfo;
|
||||
use crate::proxy::delegate_client::DelegateClient;
|
||||
use crate::proxy::target_filter::DnsCache;
|
||||
use crate::registration::client::AetherClient;
|
||||
use crate::runtime::SharedDynamicConfig;
|
||||
@@ -26,8 +27,8 @@ pub struct AppState {
|
||||
pub public_ip: String,
|
||||
pub tls_fingerprint: Option<String>,
|
||||
pub tls_acceptor: Option<TlsAcceptor>,
|
||||
/// Shared reqwest client for delegate mode (proxy issues upstream requests directly).
|
||||
pub delegate_client: reqwest::Client,
|
||||
/// Shared delegate client for proxy-initiated upstream requests.
|
||||
pub delegate_client: DelegateClient,
|
||||
/// Active connection count for metrics reporting.
|
||||
pub active_connections: Arc<AtomicU64>,
|
||||
/// Connection concurrency limiter.
|
||||
|
||||
Reference in New Issue
Block a user