Skip to content
Open
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
22 changes: 21 additions & 1 deletion crates/openshell-core/src/google_cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
/// Hostname served by the GCE metadata emulator via proxy interception.
pub const METADATA_HOST: &str = "gcp.metadata.openshell.internal";

/// Loopback address for the GCE metadata server inside sandbox namespaces.
/// Reserved loopback destination relayed to the supervisor metadata emulator.
/// Go's metadata client dials this directly (bypasses `HTTP_PROXY`).
pub const METADATA_LOOPBACK_ADDR: &str = "127.0.0.1:8174";

/// Match only the reserved metadata service, never a host cloud metadata IP.
pub fn is_metadata_destination(destination: std::net::SocketAddr) -> bool {
destination == std::net::SocketAddr::from(([127, 0, 0, 1], 8174))
}

// ── Env var alias arrays ────────────────────────────────────────────────────

/// Env vars that carry the GCP project ID inside sandboxes.
Expand Down Expand Up @@ -85,6 +90,21 @@ mod tests {
use super::*;
use std::collections::HashSet;

#[test]
fn metadata_destination_matches_only_reserved_loopback_endpoint() {
assert!(is_metadata_destination(
METADATA_LOOPBACK_ADDR.parse().unwrap()
));
for address in [
"127.0.0.1:8175",
"127.0.0.2:8174",
"169.254.169.254:80",
"[::1]:8174",
] {
assert!(!is_metadata_destination(address.parse().unwrap()));
}
}

#[test]
fn static_config_keys_matches_alias_arrays_and_vertex_vars() {
let expected: HashSet<&str> = PROJECT_ID_ENV_VARS
Expand Down
20 changes: 20 additions & 0 deletions crates/openshell-core/src/provider_credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,26 @@ impl ProviderCredentialState {
Ok(revision)
}

/// Read current provider configuration only when explicitly classified non-secret.
///
/// Local metadata adapters must not unwrap credential values just because their
/// environment names match a conventional configuration key.
pub fn current_non_secret_environment_value(&self, key: &str) -> Option<String> {
let inner = self
.inner
.read()
.expect("provider credential state poisoned");
if !inner.non_secret_environment_keys.contains(key) {
return None;
}
let placeholder = inner.current.child_env.get(key)?;
inner
.current_resolver
.as_ref()?
.resolve_placeholder(placeholder)
.map(str::to_string)
}

/// Return the GCP token placeholder and its remaining lifetime in seconds.
///
/// Searches `google_cloud::TOKEN_ENV_KEYS` in priority order (SA before
Expand Down
75 changes: 74 additions & 1 deletion crates/openshell-sandbox/src/network_broker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,11 @@ fn connect_socket(
entry.release_preconnect();
return listener.respond_value(notification.id, 0);
}
if destination.ip().is_loopback() {
// The metadata service lives in the supervisor, even though SDKs address
// it through loopback. Relay it before the ordinary local socket path.
if destination.ip().is_loopback()
&& !openshell_core::google_cloud::is_metadata_destination(destination)
{
let mut registry = lock(&registry);
let entry = registry.resolve_mut(notification.tid, fd)?;
connect_exact(entry.retained_preconnect()?.as_raw_fd(), destination)?;
Expand Down Expand Up @@ -2090,6 +2094,75 @@ mod tests {
);
}

#[test]
fn metadata_reservation_preserves_other_loopback_and_rejects_udp() {
let (launcher, listener) =
openshell_isolation_interface::linux::workload_launcher::start().unwrap();
let _broker = NetworkBroker::start_for_test(listener).unwrap();
let local_server = TcpListener::bind("127.0.0.1:0").unwrap();
let address = local_server.local_addr().unwrap();
let connection = launcher
.execute(move || TcpStream::connect(address))
.unwrap()
.unwrap();
assert_eq!(connection.peer_addr().unwrap(), address);
let error = launcher
.execute(|| {
let socket = UdpSocket::bind("127.0.0.1:0")?;
socket.connect(openshell_core::google_cloud::METADATA_LOOPBACK_ADDR)
})
.unwrap()
.unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::EACCES));
}

#[test]
fn metadata_loopback_connect_is_relayed_to_supervisor() {
use std::io::{Read as _, Write as _};
let (launcher, listener) =
openshell_isolation_interface::linux::workload_launcher::start().unwrap();
let broker = NetworkBroker::start_for_test(listener).unwrap();
let client = std::thread::spawn(move || {
launcher
.execute(|| {
let mut stream =
TcpStream::connect(openshell_core::google_cloud::METADATA_LOOPBACK_ADDR)?;
stream.write_all(b"metadata-probe")?;
let mut reply = [0; 2];
stream.read_exact(&mut reply)?;
Ok::<_, io::Error>(reply)
})
.unwrap()
});
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};

let pending = tokio::time::timeout(Duration::from_secs(30), broker.accept())
.await
.unwrap()
.unwrap();
assert!(openshell_core::google_cloud::is_metadata_destination(
pending.destination
));
let stream = pending
.complete(TcpOpenDecision::RelayReady)
.await
.unwrap()
.unwrap();
stream.set_nonblocking(true).unwrap();
let mut stream = tokio::net::TcpStream::from_std(stream).unwrap();
let mut probe = [0; 14];
stream.read_exact(&mut probe).await.unwrap();
assert_eq!(&probe, b"metadata-probe");
stream.write_all(b"ok").await.unwrap();
});
assert_eq!(&client.join().unwrap().unwrap(), b"ok");
}

#[test]
fn external_connect_times_out_when_supervisor_retains_the_decision() {
let (launcher, listener) = openshell_isolation_interface::linux::workload_launcher::start()
Expand Down
Loading
Loading