Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/nono-proxy/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,50 @@ async fn handle_connection(mut stream: tokio::net::TcpStream, state: &ProxyState
}
}

// Decide whether the upstream leg should chain through
// the corporate proxy. Mirrors the bypass logic used for
// transparent CONNECT below.
let upstream_proxy =
if let Some(ref ext_config) = state.config.external_proxy {
let bypassed = !state.bypass_matcher.is_empty()
&& state.bypass_matcher.matches(&host);
if bypassed {
debug!("tls_intercept: bypassing upstream proxy for {}", host);
None
} else if ext_config.auth.is_some() {
// Auth is configured but not yet implemented.
// Fail loudly rather than silently connecting
// without auth — the corporate proxy would
// reject anyway.
let msg = "external proxy authentication is configured \
but not yet implemented; remove the auth \
section from the external proxy config or \
wait for a future release";
audit::log_denied(
Some(&state.audit_log),
audit::ProxyMode::ConnectIntercept,
&audit::EventContext {
route_id,
..audit::EventContext::default()
},
&host,
port,
msg,
);
let response =
"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n";
stream.write_all(response.as_bytes()).await?;
return Err(ProxyError::ExternalProxy(msg.to_string()));
} else {
Some(tls_intercept::InterceptUpstreamProxy {
proxy_addr: &ext_config.address,
proxy_auth_header: None,
})
}
} else {
None
};

let ctx = tls_intercept::InterceptCtx {
route_id,
host: &host,
Expand All @@ -832,6 +876,7 @@ async fn handle_connection(mut stream: tokio::net::TcpStream, state: &ProxyState
tls_connector: &state.tls_connector,
filter: &state.filter,
audit_log: Some(&state.audit_log),
upstream_proxy,
};
return tls_intercept::handle_intercept_connect(&mut stream, ctx).await;
}
Expand Down
106 changes: 103 additions & 3 deletions crates/nono-proxy/src/tls_intercept/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,41 @@ use zeroize::Zeroizing;
/// memory ceiling consistent.
const MAX_HEADER_SIZE: usize = 64 * 1024;

/// Resolved upstream proxy for the intercept path.
///
/// When `Some`, the upstream leg of the intercepted request must chain
/// through the corporate proxy via CONNECT instead of connecting directly.
/// The caller ([`crate::server::handle_connection`]) is responsible for
/// deciding whether the target host should use the upstream proxy or route
/// direct (based on the bypass list).
pub struct InterceptUpstreamProxy<'a> {
/// `host:port` of the corporate proxy (e.g. `"proxy.corporate.com:80"`).
pub proxy_addr: &'a str,
/// Literal value for `Proxy-Authorization` sent to the corporate proxy,
/// or `None` for unauthenticated proxies.
pub proxy_auth_header: Option<&'a str>,
}

/// Select the upstream strategy based on whether an upstream proxy is
/// configured for this intercepted request.
///
/// When `upstream_proxy` is `Some`, returns #[`UpstreamStrategy::ExternalProxy`]
/// to chain through the corporate proxy. Otherwise returns
/// [`UpstreamStrategy::Direct`] with the caller-provided resolved addresses.
pub fn select_upstream_strategy<'a>(
upstream_proxy: &'a Option<InterceptUpstreamProxy<'a>>,
resolved_addrs: &'a [std::net::SocketAddr],
) -> UpstreamStrategy<'a> {
if let Some(proxy) = upstream_proxy {
UpstreamStrategy::ExternalProxy {
proxy_addr: proxy.proxy_addr,
proxy_auth_header: proxy.proxy_auth_header,
}
} else {
UpstreamStrategy::Direct { resolved_addrs }
}
}

/// Per-connection context passed to [`handle_intercept_connect`].
pub struct InterceptCtx<'a> {
pub route_id: Option<&'a str>,
Expand All @@ -44,6 +79,9 @@ pub struct InterceptCtx<'a> {
pub tls_connector: &'a tokio_rustls::TlsConnector,
pub filter: &'a ProxyFilter,
pub audit_log: Option<&'a audit::SharedAuditLog>,
/// When `Some`, the upstream leg chains through an enterprise proxy
/// instead of connecting directly to the target.
pub upstream_proxy: Option<InterceptUpstreamProxy<'a>>,
}
Comment thread
caiocdcs marked this conversation as resolved.

/// Handle a CONNECT request that matched a route requiring L7 visibility.
Expand Down Expand Up @@ -395,13 +433,12 @@ where
let connector = route
.and_then(|r| r.tls_connector.as_ref())
.unwrap_or(ctx.tls_connector);
let strategy = select_upstream_strategy(&ctx.upstream_proxy, &check.resolved_addrs);
let upstream_spec = UpstreamSpec {
scheme: UpstreamScheme::Https,
host: ctx.host,
port: ctx.port,
strategy: UpstreamStrategy::Direct {
resolved_addrs: &check.resolved_addrs,
},
strategy,
tls_connector: connector,
};
let audit_ctx = AuditCtx {
Expand Down Expand Up @@ -507,4 +544,67 @@ mod tests {
assert!(parse_request_line("malformed").is_err());
assert!(parse_request_line("").is_err());
}

#[test]
fn upstream_strategy_selects_external_proxy_when_configured() {
// When InterceptUpstreamProxy is set, the strategy must be
// ExternalProxy, not Direct. Regression test for #1048.
let proxy = InterceptUpstreamProxy {
proxy_addr: "proxy.corp:80",
proxy_auth_header: None,
};
let some_proxy = Some(proxy);
let strategy = select_upstream_strategy(&some_proxy, &[]);
match strategy {
UpstreamStrategy::ExternalProxy {
proxy_addr,
proxy_auth_header,
} => {
assert_eq!(proxy_addr, "proxy.corp:80");
assert!(proxy_auth_header.is_none());
}
UpstreamStrategy::Direct { .. } => {
panic!("expected ExternalProxy strategy, got Direct");
}
}
}

#[test]
fn upstream_strategy_selects_direct_when_no_proxy() {
// When upstream_proxy is None, the strategy must fall back to
// Direct (pre-existing behaviour).
let addrs: Vec<std::net::SocketAddr> = vec![];
let strategy = select_upstream_strategy(&None, &addrs);
match strategy {
UpstreamStrategy::Direct { resolved_addrs } => {
assert!(resolved_addrs.is_empty());
}
UpstreamStrategy::ExternalProxy { .. } => {
panic!("expected Direct strategy, got ExternalProxy");
}
}
}

#[test]
fn upstream_strategy_external_proxy_with_auth_header() {
// When auth header is provided, it must be carried through.
let proxy = InterceptUpstreamProxy {
proxy_addr: "proxy.corp:3128",
proxy_auth_header: Some("Basic dXNlcjpwYXNz"),
};
let some_proxy = Some(proxy);
let strategy = select_upstream_strategy(&some_proxy, &[]);
match strategy {
UpstreamStrategy::ExternalProxy {
proxy_addr,
proxy_auth_header,
} => {
assert_eq!(proxy_addr, "proxy.corp:3128");
assert_eq!(proxy_auth_header, Some("Basic dXNlcjpwYXNz"));
}
UpstreamStrategy::Direct { .. } => {
panic!("expected ExternalProxy strategy, got Direct");
}
}
}
Comment thread
caiocdcs marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion crates/nono-proxy/src/tls_intercept/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ pub use acceptor::build_server_config;
pub use bundle::{BundleInputs, write_bundle};
pub use ca::EphemeralCa;
pub use cert_cache::CertCache;
pub use handle::{InterceptCtx, handle_intercept_connect};
pub use handle::{InterceptCtx, InterceptUpstreamProxy, handle_intercept_connect};