feat: retain NetworkProxy, when appropriate (#11207)

As of this PR, `SessionServices` retains a
`Option<StartedNetworkProxy>`, if appropriate.

Now the `network` field on `Config` is `Option<NetworkProxySpec>`
instead of `Option<NetworkProxy>`.

Over in `Session::new()`, we invoke `NetworkProxySpec::start_proxy()` to
create the `StartedNetworkProxy`, which is a new struct that retains the
`NetworkProxy` as well as the `NetworkProxyHandle`. (Note that `Drop` is
implemented for `NetworkProxyHandle` to ensure the proxies are shutdown
when it is dropped.)

The `NetworkProxy` from the `StartedNetworkProxy` is threaded through to
the appropriate places.


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/11207).
* #11285
* __->__ #11207
This commit is contained in:
Michael Bolin
2026-02-10 02:09:23 -08:00
committed by GitHub
parent 8e240a13be
commit 44ebf4588f
28 changed files with 583 additions and 30 deletions

View File

@@ -8,13 +8,13 @@ use std::net::SocketAddr;
use tracing::warn;
use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct NetworkProxyConfig {
#[serde(default)]
pub network: NetworkProxySettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkProxySettings {
#[serde(default)]
pub enabled: bool,
@@ -205,6 +205,30 @@ fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr> {
}
}
pub fn host_and_port_from_network_addr(value: &str, default_port: u16) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return "<missing>".to_string();
}
let parts = match parse_host_port(trimmed, default_port) {
Ok(parts) => parts,
Err(_) => {
return format_host_and_port(trimmed, default_port);
}
};
format_host_and_port(&parts.host, parts.port)
}
fn format_host_and_port(host: &str, port: u16) -> String {
if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SocketAddressParts {
host: String,
@@ -280,14 +304,13 @@ fn parse_host_port_fallback(input: &str, default_port: u16) -> Result<SocketAddr
// accidentally interpreting unbracketed IPv6 addresses as `host:port`.
if host_port.bytes().filter(|b| *b == b':').count() == 1
&& let Some((host, port)) = host_port.rsplit_once(':')
&& let Ok(port) = port.parse::<u16>()
{
if host.is_empty() {
bail!("missing host in network proxy address: {input}");
}
return Ok(SocketAddressParts {
host: host.to_string(),
port,
port: port.parse::<u16>().ok().unwrap_or(default_port),
});
}
@@ -376,12 +399,25 @@ mod tests {
assert_eq!(
parse_host_port("example.com:notaport", 3128).unwrap(),
SocketAddressParts {
host: "example.com:notaport".to_string(),
host: "example.com".to_string(),
port: 3128,
}
);
}
#[test]
fn host_and_port_from_network_addr_defaults_for_empty_string() {
assert_eq!(host_and_port_from_network_addr("", 1234), "<missing>");
}
#[test]
fn host_and_port_from_network_addr_formats_ipv6() {
assert_eq!(
host_and_port_from_network_addr("http://[::1]:8080", 3128),
"[::1]:8080"
);
}
#[test]
fn resolve_addr_maps_localhost_to_loopback() {
assert_eq!(

View File

@@ -15,6 +15,7 @@ mod upstream;
pub use config::NetworkMode;
pub use config::NetworkProxyConfig;
pub use config::host_and_port_from_network_addr;
pub use network_policy::NetworkDecision;
pub use network_policy::NetworkPolicyDecider;
pub use network_policy::NetworkPolicyRequest;

View File

@@ -363,6 +363,18 @@ impl NetworkProxy {
NetworkProxyBuilder::default()
}
pub fn http_addr(&self) -> SocketAddr {
self.http_addr
}
pub fn socks_addr(&self) -> SocketAddr {
self.socks_addr
}
pub fn admin_addr(&self) -> SocketAddr {
self.admin_addr
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
// Enforce proxying for child processes. We intentionally override existing values so
// command-level environment cannot bypass the managed proxy endpoint.