mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
fix(exec-server): start managed proxy on executor
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
co-authored by
Codex noreply@openai.com
parent
a3f8b0b332
commit
55b9447a17
@@ -42,6 +42,7 @@ use crate::unified_exec::UnifiedExecProcess;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::RemoteNetworkProxyConfig;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -324,6 +325,26 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
));
|
||||
let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);
|
||||
let (env, managed_network_context) = match managed_network {
|
||||
Some(network) if environment_is_remote => {
|
||||
let config = network.current_cfg().await.map_err(|err| {
|
||||
ToolError::Codex(CodexErr::Io(io::Error::other(format!(
|
||||
"failed to read effective network proxy config: {err}"
|
||||
))))
|
||||
})?;
|
||||
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config)
|
||||
.map_err(|err| {
|
||||
ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string())))
|
||||
})?;
|
||||
let allow_local_binding = proxy_config.allow_local_binding;
|
||||
(
|
||||
env,
|
||||
Some(codex_network_proxy::ManagedNetworkSandboxContext {
|
||||
loopback_ports: Vec::new(),
|
||||
allow_local_binding,
|
||||
proxy_config: Some(proxy_config),
|
||||
}),
|
||||
)
|
||||
}
|
||||
Some(network) => {
|
||||
let prepared = network
|
||||
.prepare_for_optional_environment(
|
||||
|
||||
@@ -475,10 +475,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
&self,
|
||||
command: SandboxCommand,
|
||||
options: ExecOptions,
|
||||
network: Option<&NetworkProxy>,
|
||||
environment_id: Option<&str>,
|
||||
) -> Result<crate::sandboxing::ExecRequest, CodexErr> {
|
||||
let network = self.network_proxy(network);
|
||||
let managed_network = command.managed_network.clone();
|
||||
let exec_server_permissions = effective_permission_profile(
|
||||
self.exec_server_permissions,
|
||||
@@ -492,8 +489,8 @@ impl<'a> SandboxAttempt<'a> {
|
||||
// The exec-server must receive the native command, not this host's wrapper.
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
environment_id,
|
||||
network,
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: self.sandbox_cwd,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
|
||||
@@ -232,6 +232,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
};
|
||||
let command = || SandboxCommand {
|
||||
program: "/bin/bash".into(),
|
||||
@@ -246,7 +247,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
|
||||
};
|
||||
let request = attempt
|
||||
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
||||
.env_for_exec_server(command(), options())
|
||||
.expect("prepare remote exec request");
|
||||
|
||||
assert_eq!(
|
||||
@@ -278,7 +279,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
|
||||
attempt.sandbox_requested = false;
|
||||
let request = attempt
|
||||
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
||||
.env_for_exec_server(command(), options())
|
||||
.expect("prepare unsandboxed remote exec request");
|
||||
|
||||
assert_eq!(request.exec_server_sandbox, None);
|
||||
|
||||
@@ -922,7 +922,7 @@ impl UnifiedExecProcessManager {
|
||||
environment: &codex_exec_server::Environment,
|
||||
) -> Result<UnifiedExecProcess, ToolError> {
|
||||
let mut request = if environment.is_remote() {
|
||||
attempt.env_for_exec_server(command, options, network, environment_id)
|
||||
attempt.env_for_exec_server(command, options)
|
||||
} else {
|
||||
attempt.env_for(command, options, network, environment_id)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
};
|
||||
let mut request = ExecRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
|
||||
@@ -598,6 +598,7 @@ mod tests {
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 48081],
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server_protocol::JSONRPCErrorError;
|
||||
use codex_network_proxy::NetworkProxyHandle;
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -97,6 +98,7 @@ struct RunningProcess {
|
||||
termination_requested: bool,
|
||||
sandbox: SandboxType,
|
||||
sandbox_denied: bool,
|
||||
network_proxy_handle: Option<NetworkProxyHandle>,
|
||||
}
|
||||
|
||||
/// Bounded cache of stdin write ids that have already been accepted for one process.
|
||||
@@ -235,7 +237,7 @@ impl LocalProcess {
|
||||
) -> Result<(ExecResponse, watch::Sender<u64>, ExecProcessEventLog), JSONRPCErrorError> {
|
||||
let process_id = params.process_id.clone();
|
||||
let prepared =
|
||||
prepare_exec_request(¶ms, child_env(¶ms), self.runtime_paths.as_ref())?;
|
||||
prepare_exec_request(¶ms, child_env(¶ms), self.runtime_paths.as_ref()).await?;
|
||||
let (program, args) = prepared
|
||||
.command
|
||||
.split_first()
|
||||
@@ -338,6 +340,7 @@ impl LocalProcess {
|
||||
termination_requested: false,
|
||||
sandbox: prepared.sandbox,
|
||||
sandbox_denied: false,
|
||||
network_proxy_handle: prepared.network_proxy_handle,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -823,7 +826,7 @@ async fn watch_exit(
|
||||
output_notify: Arc<Notify>,
|
||||
) {
|
||||
let exit_code = exit_rx.await.unwrap_or(-1);
|
||||
let sandboxed = {
|
||||
let (sandboxed, network_proxy_handle) = {
|
||||
let mut processes = inner.processes.lock().await;
|
||||
match processes.get_mut(&process_id) {
|
||||
Some(ProcessEntry::Running(process)) => {
|
||||
@@ -837,11 +840,16 @@ async fn watch_exit(
|
||||
"error"
|
||||
});
|
||||
}
|
||||
sandboxed
|
||||
(sandboxed, process.network_proxy_handle.take())
|
||||
}
|
||||
Some(ProcessEntry::Starting(_)) | None => false,
|
||||
Some(ProcessEntry::Starting(_)) | None => (false, None),
|
||||
}
|
||||
};
|
||||
if let Some(network_proxy_handle) = network_proxy_handle
|
||||
&& let Err(err) = network_proxy_handle.shutdown().await
|
||||
{
|
||||
tracing::warn!("failed to shut down executor network proxy: {err}");
|
||||
}
|
||||
if sandboxed {
|
||||
let _ = tokio::time::timeout(Duration::from_millis(20), output_notify.notified()).await;
|
||||
}
|
||||
@@ -1310,6 +1318,7 @@ mod tests {
|
||||
termination_requested: false,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_denied: false,
|
||||
network_proxy_handle: None,
|
||||
})),
|
||||
);
|
||||
assert!(previous.is_none());
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_exec_server_protocol::JSONRPCErrorError;
|
||||
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::NetworkProxyHandle;
|
||||
use codex_network_proxy::NetworkProxyState;
|
||||
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
@@ -16,6 +21,7 @@ use codex_utils_path_uri::PathUri;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_params;
|
||||
|
||||
pub(crate) struct PreparedExecRequest {
|
||||
@@ -24,13 +30,16 @@ pub(crate) struct PreparedExecRequest {
|
||||
pub(crate) env: HashMap<String, String>,
|
||||
pub(crate) arg0: Option<String>,
|
||||
pub(crate) sandbox: SandboxType,
|
||||
pub(crate) network_proxy_handle: Option<NetworkProxyHandle>,
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_exec_request(
|
||||
pub(crate) async fn prepare_exec_request(
|
||||
params: &ExecParams,
|
||||
env: HashMap<String, String>,
|
||||
runtime_paths: Option<&ExecServerRuntimePaths>,
|
||||
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
|
||||
let (env, managed_network, network_proxy_handle) =
|
||||
prepare_managed_network(params.managed_network.as_ref(), env).await?;
|
||||
let Some(sandbox_context) = params.sandbox.as_ref() else {
|
||||
return Ok(PreparedExecRequest {
|
||||
command: params.argv.clone(),
|
||||
@@ -38,6 +47,7 @@ pub(crate) fn prepare_exec_request(
|
||||
env,
|
||||
arg0: params.arg0.clone(),
|
||||
sandbox: SandboxType::None,
|
||||
network_proxy_handle,
|
||||
});
|
||||
};
|
||||
let runtime_paths = runtime_paths
|
||||
@@ -62,7 +72,7 @@ pub(crate) fn prepare_exec_request(
|
||||
native_workspace_roots.as_slice()
|
||||
};
|
||||
let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
let managed_mitm_ca_trust_bundle_path = params.managed_network.as_ref().and_then(|_| {
|
||||
let managed_mitm_ca_trust_bundle_path = managed_network.as_ref().and_then(|_| {
|
||||
CUSTOM_CA_ENV_KEYS.iter().find_map(|key| {
|
||||
let path = env.get(*key)?;
|
||||
if !is_managed_mitm_ca_trust_bundle_path(path) {
|
||||
@@ -117,7 +127,7 @@ pub(crate) fn prepare_exec_request(
|
||||
args: args.to_vec(),
|
||||
cwd: params.cwd.clone(),
|
||||
env,
|
||||
managed_network: params.managed_network.clone(),
|
||||
managed_network,
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
@@ -139,9 +149,44 @@ pub(crate) fn prepare_exec_request(
|
||||
env: request.env,
|
||||
arg0: request.arg0,
|
||||
sandbox: request.sandbox,
|
||||
network_proxy_handle,
|
||||
})
|
||||
}
|
||||
|
||||
async fn prepare_managed_network(
|
||||
managed_network: Option<&ManagedNetworkSandboxContext>,
|
||||
env: HashMap<String, String>,
|
||||
) -> Result<
|
||||
(
|
||||
HashMap<String, String>,
|
||||
Option<ManagedNetworkSandboxContext>,
|
||||
Option<NetworkProxyHandle>,
|
||||
),
|
||||
JSONRPCErrorError,
|
||||
> {
|
||||
let Some(proxy_config) = managed_network.and_then(|context| context.proxy_config.clone())
|
||||
else {
|
||||
return Ok((env, managed_network.cloned(), None));
|
||||
};
|
||||
let state = NetworkProxyState::from_static_config(proxy_config.into_network_proxy_config())
|
||||
.map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?;
|
||||
let proxy = NetworkProxy::builder()
|
||||
.state(Arc::new(state))
|
||||
.build()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to build executor network proxy: {err}")))?;
|
||||
let handle = proxy
|
||||
.run()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to start executor network proxy: {err}")))?;
|
||||
let prepared = proxy
|
||||
.prepare_for_optional_environment(env, /*environment_id*/ None)
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to prepare executor network proxy: {err}"))
|
||||
})?;
|
||||
Ok((prepared.env, Some(prepared.sandbox_context), Some(handle)))
|
||||
}
|
||||
|
||||
fn native_path(path: &PathUri, label: &str) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||
path.to_abs_path().map_err(|err| {
|
||||
invalid_params(format!(
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::RemoteNetworkProxyConfig;
|
||||
#[cfg(unix)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::prepare_exec_request;
|
||||
use crate::ExecParams;
|
||||
@@ -17,8 +23,8 @@ use crate::FileSystemSandboxContext;
|
||||
use crate::ProcessId;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
@@ -50,6 +56,7 @@ fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
.await
|
||||
.expect("prepare sandboxed request");
|
||||
|
||||
assert_ne!(prepared.command, params.argv);
|
||||
@@ -82,8 +89,8 @@ fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
@@ -110,10 +117,12 @@ fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
.await
|
||||
.expect("prepare managed-network sandbox request");
|
||||
let policy = prepared
|
||||
.command
|
||||
@@ -124,8 +133,8 @@ fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_request_preserves_native_launch_fields() {
|
||||
#[tokio::test]
|
||||
async fn native_request_preserves_native_launch_fields() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
@@ -147,6 +156,7 @@ fn native_request_preserves_native_launch_fields() {
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, env.clone(), /*runtime_paths*/ None)
|
||||
.await
|
||||
.expect("prepare native request");
|
||||
|
||||
assert_eq!(prepared.command, params.argv);
|
||||
@@ -154,3 +164,70 @@ fn native_request_preserves_native_launch_fields() {
|
||||
assert_eq!(prepared.env, env);
|
||||
assert_eq!(prepared.arg0, params.arg0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_proxy_config_starts_executor_local_proxy() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
.expect("absolute cwd");
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.enabled = true;
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["allowed.example".to_string()]);
|
||||
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config)
|
||||
.expect("supported remote proxy config");
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("process-remote-proxy"),
|
||||
argv: vec!["echo".to_string(), "hello".to_string()],
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: Vec::new(),
|
||||
allow_local_binding: false,
|
||||
proxy_config: Some(proxy_config),
|
||||
}),
|
||||
};
|
||||
let stale_proxy = "http://127.0.0.1:9".to_string();
|
||||
let env = HashMap::from([("HTTP_PROXY".to_string(), stale_proxy.clone())]);
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, env, /*runtime_paths*/ None)
|
||||
.await
|
||||
.expect("prepare request with executor-local proxy");
|
||||
|
||||
let http_proxy = prepared.env.get("HTTP_PROXY").expect("HTTP proxy env");
|
||||
assert_ne!(http_proxy, &stale_proxy);
|
||||
assert!(http_proxy.starts_with("http://127.0.0.1:"));
|
||||
let proxy_addr: SocketAddr = http_proxy
|
||||
.strip_prefix("http://")
|
||||
.expect("HTTP proxy scheme")
|
||||
.parse()
|
||||
.expect("HTTP proxy address");
|
||||
let mut stream = tokio::net::TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.expect("connect to executor proxy");
|
||||
stream
|
||||
.write_all(b"CONNECT blocked.example:443 HTTP/1.1\r\nHost: blocked.example:443\r\n\r\n")
|
||||
.await
|
||||
.expect("write CONNECT request");
|
||||
let mut response = [0_u8; 256];
|
||||
let response_len = timeout(Duration::from_secs(2), stream.read(&mut response))
|
||||
.await
|
||||
.expect("proxy response timeout")
|
||||
.expect("read proxy response");
|
||||
assert!(String::from_utf8_lossy(&response[..response_len]).starts_with("HTTP/1.1 403"));
|
||||
|
||||
prepared
|
||||
.network_proxy_handle
|
||||
.expect("running executor proxy")
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("shut down executor proxy");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ mod network_policy;
|
||||
mod policy;
|
||||
mod proxy;
|
||||
mod reasons;
|
||||
mod remote_config;
|
||||
mod responses;
|
||||
mod runtime;
|
||||
mod socks5;
|
||||
@@ -67,6 +68,7 @@ pub use proxy::PROXY_URL_ENV_KEYS;
|
||||
pub use proxy::PreparedManagedNetwork;
|
||||
pub use proxy::has_proxy_url_env_vars;
|
||||
pub use proxy::proxy_url_env_value;
|
||||
pub use remote_config::RemoteNetworkProxyConfig;
|
||||
pub use runtime::BlockedRequest;
|
||||
pub use runtime::BlockedRequestArgs;
|
||||
pub use runtime::BlockedRequestObserver;
|
||||
|
||||
@@ -348,6 +348,9 @@ pub struct ManagedNetworkSandboxContext {
|
||||
/// Whether the command may bind local sockets and exchange loopback traffic.
|
||||
#[serde(default)]
|
||||
pub allow_local_binding: bool,
|
||||
/// Effective proxy settings for an exec-server that must start its own local listeners.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proxy_config: Option<crate::RemoteNetworkProxyConfig>,
|
||||
}
|
||||
|
||||
/// Environment-specific managed-network settings prepared for one command launch.
|
||||
@@ -727,6 +730,7 @@ impl NetworkProxy {
|
||||
sandbox_context: ManagedNetworkSandboxContext {
|
||||
loopback_ports,
|
||||
allow_local_binding: runtime_settings.allow_local_binding,
|
||||
proxy_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1216,6 +1220,7 @@ mod tests {
|
||||
ManagedNetworkSandboxContext {
|
||||
loopback_ports: expected_ports,
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::NetworkDomainPermissions;
|
||||
use crate::NetworkMode;
|
||||
use crate::NetworkProxyConfig;
|
||||
use crate::NetworkUnixSocketPermissions;
|
||||
|
||||
/// Effective network proxy settings that are safe to send to a remote executor.
|
||||
///
|
||||
/// Listener addresses are deliberately omitted because the executor chooses its own loopback
|
||||
/// ports. MITM, credential injection, and hooks are not represented so their configuration cannot
|
||||
/// cross the exec-server boundary accidentally.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteNetworkProxyConfig {
|
||||
pub enabled: bool,
|
||||
pub enable_socks5: bool,
|
||||
pub enable_socks5_udp: bool,
|
||||
pub allow_upstream_proxy: bool,
|
||||
pub dangerously_allow_all_unix_sockets: bool,
|
||||
pub mode: NetworkMode,
|
||||
pub domains: Option<NetworkDomainPermissions>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
|
||||
pub allow_local_binding: bool,
|
||||
}
|
||||
|
||||
impl RemoteNetworkProxyConfig {
|
||||
pub fn from_effective_config(config: &NetworkProxyConfig) -> Result<Self> {
|
||||
let settings = &config.network;
|
||||
ensure!(
|
||||
!settings.mitm
|
||||
&& !settings.credential_broker
|
||||
&& !settings.dangerously_allow_plaintext_credential_injection
|
||||
&& settings.mitm_hooks.is_empty(),
|
||||
"remote exec-server network proxy does not support MITM, credential injection, or MITM hooks"
|
||||
);
|
||||
Ok(Self {
|
||||
enabled: settings.enabled,
|
||||
enable_socks5: settings.enable_socks5,
|
||||
enable_socks5_udp: settings.enable_socks5_udp,
|
||||
allow_upstream_proxy: settings.allow_upstream_proxy,
|
||||
dangerously_allow_all_unix_sockets: settings.dangerously_allow_all_unix_sockets,
|
||||
mode: settings.mode,
|
||||
domains: settings.domains.clone(),
|
||||
unix_sockets: settings.unix_sockets.clone(),
|
||||
allow_local_binding: settings.allow_local_binding,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_network_proxy_config(self) -> NetworkProxyConfig {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.enabled = self.enabled;
|
||||
config.network.enable_socks5 = self.enable_socks5;
|
||||
config.network.enable_socks5_udp = self.enable_socks5_udp;
|
||||
config.network.allow_upstream_proxy = self.allow_upstream_proxy;
|
||||
config.network.dangerously_allow_all_unix_sockets = self.dangerously_allow_all_unix_sockets;
|
||||
config.network.mode = self.mode;
|
||||
config.network.domains = self.domains;
|
||||
config.network.unix_sockets = self.unix_sockets;
|
||||
config.network.allow_local_binding = self.allow_local_binding;
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "remote_config_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,46 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::RemoteNetworkProxyConfig;
|
||||
use crate::NetworkMode;
|
||||
use crate::NetworkProxyConfig;
|
||||
|
||||
#[test]
|
||||
fn round_trip_preserves_supported_effective_settings() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.enabled = true;
|
||||
config.network.enable_socks5 = false;
|
||||
config.network.enable_socks5_udp = false;
|
||||
config.network.allow_upstream_proxy = false;
|
||||
config.network.dangerously_allow_all_unix_sockets = true;
|
||||
config.network.mode = NetworkMode::Limited;
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["example.com".into()]);
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["blocked.example.com".into()]);
|
||||
config
|
||||
.network
|
||||
.set_allow_unix_sockets(vec!["/var/run/example.sock".into()]);
|
||||
config.network.allow_local_binding = true;
|
||||
|
||||
let remote =
|
||||
RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config");
|
||||
let round_trip = remote.into_network_proxy_config();
|
||||
|
||||
assert_eq!(round_trip, config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mitm_configuration() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.mitm = true;
|
||||
|
||||
let error = RemoteNetworkProxyConfig::from_effective_config(&config)
|
||||
.expect_err("MITM must not cross the remote executor boundary");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"remote exec-server network proxy does not support MITM, credential injection, or MITM hooks"
|
||||
);
|
||||
}
|
||||
@@ -187,6 +187,26 @@ pub trait ConfigReloader: Send + Sync {
|
||||
|
||||
pub type ConfigReloaderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StaticConfigReloader {
|
||||
state: ConfigState,
|
||||
}
|
||||
|
||||
impl ConfigReloader for StaticConfigReloader {
|
||||
fn source_label(&self) -> String {
|
||||
"static config state".to_string()
|
||||
}
|
||||
|
||||
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
|
||||
Box::pin(async { Ok(None) })
|
||||
}
|
||||
|
||||
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
|
||||
let state = self.state.clone();
|
||||
Box::pin(async move { Ok(state) })
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BlockedRequestObserver: Send + Sync + 'static {
|
||||
fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_>;
|
||||
}
|
||||
@@ -257,6 +277,15 @@ impl Clone for NetworkProxyState {
|
||||
}
|
||||
|
||||
impl NetworkProxyState {
|
||||
/// Builds immutable runtime state from an already-computed effective configuration.
|
||||
pub fn from_static_config(config: NetworkProxyConfig) -> Result<Self> {
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default())?;
|
||||
Ok(Self::with_reloader(
|
||||
state.clone(),
|
||||
Arc::new(StaticConfigReloader { state }),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn with_reloader(state: ConfigState, reloader: Arc<dyn ConfigReloader>) -> Self {
|
||||
Self::with_reloader_and_audit_metadata(
|
||||
state,
|
||||
|
||||
@@ -269,6 +269,7 @@ fn prepared_managed_network_context_allows_only_its_proxy_ports() {
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 48081],
|
||||
allow_local_binding: false,
|
||||
proxy_config: None,
|
||||
};
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
||||
Reference in New Issue
Block a user