use std::fmt;
use serde_json::{json, Value};
use crate::{
setup_resource_tags, Network, NetworkSettings, ResourceEntry, Sandbox, SandboxEgress, Stack,
};
pub const SANDBOX_EGRESS_GATEWAY_ID: &str = "sandbox-egress-gateway";
pub const SANDBOX_EGRESS_REPLICA_SECRET: &str = "sandbox-egress-replica-secret";
pub const EMBEDDED_REPLICA_SECRET_GATEWAY_DIGESTS: &[&str] = &[
"sha256:dbac794afe2185a8ce025d92ec05a5878d6367ae9380eccc2c3dfc4d55f1c59c",
"sha256:1dc14128e34426b84c7a02228a6de790ea092b156776c3f2688b9843b26fc924",
"sha256:7a3a66b0894c05fca445cbf5fd71e351cd8a9a98c40744acad4496b59574bdbd",
"sha256:f0e47d06eea327bc27fdc1238dae6a8938572db0c1d139495d00dd5623b657dd",
"sha256:b252a1b423b95385e5df0c57a37daae6937daa274e3c7783c71704584b18483d",
];
pub const NO_CONFIG_FILE_GATEWAY_DIGESTS: &[&str] =
&["sha256:8538880f99141888e54bedc7d290194dfee5bfe11910266b309dde53de2601b0"];
fn is_pinned_to(image: &str, digests: &[&str]) -> bool {
digests
.iter()
.any(|digest| image.ends_with(&format!("@{digest}")))
}
pub fn gateway_image_reads_replica_secret_source(image: &str) -> bool {
!is_pinned_to(image, EMBEDDED_REPLICA_SECRET_GATEWAY_DIGESTS)
}
pub fn gateway_image_reads_config_files(image: &str) -> bool {
gateway_image_reads_replica_secret_source(image)
&& !is_pinned_to(image, NO_CONFIG_FILE_GATEWAY_DIGESTS)
}
pub fn has_sandbox_gateway(stack: &Stack) -> bool {
stack
.resources
.get(SANDBOX_EGRESS_GATEWAY_ID)
.is_some_and(|entry| entry.config.downcast_ref::<crate::Container>().is_some())
}
pub fn require_internet_sandbox_gateway(stack: &Stack) -> crate::Result<()> {
if has_sandbox_gateway(stack) {
return Ok(());
}
match stack.resources().find(|(_, entry)| {
entry
.config
.downcast_ref::<Sandbox>()
.is_some_and(|sandbox| matches!(sandbox.egress, SandboxEgress::Internet))
}) {
Some((id, _)) => Err(alien_error::AlienError::new(
crate::ErrorData::SandboxInternetGatewayMissing {
sandbox_id: id.clone(),
},
)),
None => Ok(()),
}
}
pub fn sandbox_egress_key_owner(stack: &Stack) -> Option<&str> {
stack
.resources()
.filter(|(_, entry)| {
entry
.config
.downcast_ref::<Sandbox>()
.is_some_and(|sandbox| matches!(sandbox.egress, SandboxEgress::Internet))
})
.min_by_key(|(id, entry)| (entry.lifecycle != crate::ResourceLifecycle::Frozen, *id))
.map(|(id, _)| id.as_str())
}
pub fn sandbox_needs_connector(stack: &Stack, sandbox: &Sandbox) -> bool {
sandbox_plans_connector(sandbox)
&& (sandbox.egress != SandboxEgress::Internet || has_sandbox_gateway(stack))
}
pub fn sandbox_plans_connector(sandbox: &Sandbox) -> bool {
matches!(
sandbox.cloud_egress(),
SandboxEgress::None | SandboxEgress::AllowDomains { .. }
) || sandbox.egress == SandboxEgress::Internet
}
pub fn aws_sandbox_agent_environment(
stack: &Stack,
sandbox: &Sandbox,
) -> std::collections::BTreeMap<String, String> {
let mut environment = sandbox.supervisor_environment();
if sandbox.image_user.is_some() {
environment.insert("ALIEN_SANDBOX_IMAGE_COMMAND".into(), "true".into());
environment.insert("ALIEN_SANDBOX_EGRESS_RUN_HOOK".into(), "true".into());
if sandbox.privileged_supervisor.is_none() && sandbox.egress == SandboxEgress::None {
environment.insert("ALIEN_SANDBOX_EGRESS".into(), "\"none\"".into());
}
}
if matches!(sandbox.egress, SandboxEgress::Internet) && has_sandbox_gateway(stack) {
environment.insert(
"ALIEN_SANDBOX_EGRESS".into(),
serde_json::to_string(&sandbox.egress).expect("egress serializes"),
);
environment.insert("ALIEN_SANDBOX_EGRESS_RUN_HOOK".into(), "true".into());
}
environment
}
pub const SANDBOX_EGRESS_POLICY_NAME: &str = "sandbox-egress-connector";
pub const NETWORK_CONNECTOR_TYPE_NAME: &str = "AWS::Lambda::NetworkConnector";
pub const LOOPBACK_ONLY_CIDR: &str = "127.0.0.1/32";
pub fn sandbox_egress_name(resource_prefix: &str, sandbox_id: &str) -> String {
format!("{resource_prefix}-{sandbox_id}-egress")
}
pub fn sandbox_egress_connector_name(resource_prefix: &str, sandbox_id: &str) -> String {
format!("{resource_prefix}-{sandbox_id}")
}
pub fn sandbox_egress_operator_trust_policy() -> Value {
json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
})
}
pub fn sandbox_egress_operator_policy(partition: &str, account_id: &str, region: &str) -> Value {
let arn = |kind: &str| format!("arn:{partition}:ec2:{region}:{account_id}:{kind}/*");
json!({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateENI",
"Effect": "Allow",
"Action": "ec2:CreateNetworkInterface",
"Resource": [arn("network-interface"), arn("subnet"), arn("security-group")]
},
{
"Sid": "TagENI",
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": arn("network-interface"),
"Condition": {
"StringEquals": {
"ec2:CreateAction": "CreateNetworkInterface",
"ec2:ManagedResourceOperator": "network-connectors.lambda.amazonaws.com"
}
}
}
]
})
}
#[derive(Debug, Clone, bon::Builder)]
pub struct SandboxEgressConnector<'a> {
resource_prefix: &'a str,
sandbox_id: &'a str,
operator_role_arn: &'a str,
private_subnet_ids: &'a [String],
security_group_id: &'a str,
}
impl SandboxEgressConnector<'_> {
pub fn desired_state(&self) -> Value {
let tags: Vec<Value> = setup_resource_tags(
self.resource_prefix,
self.sandbox_id,
Sandbox::RESOURCE_TYPE.as_ref(),
)
.into_iter()
.map(|(key, value)| json!({ "Key": key, "Value": value }))
.collect();
json!({
"Name": sandbox_egress_connector_name(self.resource_prefix, self.sandbox_id),
"OperatorRole": self.operator_role_arn,
"Configuration": {
"VpcEgressConfiguration": {
"AssociatedComputeResourceTypes": ["MicroVm"],
"NetworkProtocol": "IPv4",
"SubnetIds": self.private_subnet_ids,
"SecurityGroupIds": [self.security_group_id]
}
},
"Tags": tags
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxEgressVpc {
Created,
BroughtByCustomer,
}
#[derive(Debug, Clone, Copy)]
pub struct SandboxEgressNetwork<'a> {
pub id: &'a str,
pub entry: &'a ResourceEntry,
pub vpc: SandboxEgressVpc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxEgressRefusal {
NoNetwork,
DefaultVpc,
OtherCloudNetwork,
}
impl fmt::Display for SandboxEgressRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an AWS sandbox routes session traffic through a VPC egress connector")?;
f.write_str(match self {
Self::NoNetwork => ", and this stack declares no network for it to attach to",
Self::DefaultVpc => {
", which needs private subnets, and the account's default VPC has only public \
subnets. Set the network to create or byo-vpc-aws"
}
Self::OtherCloudNetwork => ", and this stack's network settings are for another cloud",
})
}
}
pub fn sandbox_egress_network(
stack: &Stack,
) -> Result<SandboxEgressNetwork<'_>, SandboxEgressRefusal> {
let (id, entry, network) = stack
.resources()
.find_map(|(id, entry)| Some((id, entry, entry.config.downcast_ref::<Network>()?)))
.ok_or(SandboxEgressRefusal::NoNetwork)?;
let vpc = match &network.settings {
NetworkSettings::Create { .. } => SandboxEgressVpc::Created,
NetworkSettings::ByoVpcAws { .. } => SandboxEgressVpc::BroughtByCustomer,
NetworkSettings::UseDefault => return Err(SandboxEgressRefusal::DefaultVpc),
NetworkSettings::ByoVpcGcp { .. } | NetworkSettings::ByoVnetAzure { .. } => {
return Err(SandboxEgressRefusal::OtherCloudNetwork)
}
};
Ok(SandboxEgressNetwork { id, entry, vpc })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ResourceLifecycle, SandboxCode, SandboxEgress, SandboxLifecyclePolicy};
use SandboxEgressRefusal::{DefaultVpc, NoNetwork, OtherCloudNetwork};
use SandboxEgressVpc::{BroughtByCustomer, Created};
fn outcome(
egress: SandboxEgress,
network: Option<NetworkSettings>,
) -> Result<SandboxEgressVpc, SandboxEgressRefusal> {
let sandbox = Sandbox::new("agents".to_string())
.code(SandboxCode::Image {
image: "s3://acme/agents/bundle.zip".to_string(),
})
.egress(egress.clone())
.lifecycle(SandboxLifecyclePolicy {
max_lifetime_seconds: None,
idle_pause_seconds: None,
})
.build();
let mut stack = Stack::new("acme".to_string()).add(sandbox, ResourceLifecycle::Frozen);
if let Some(settings) = network {
stack = stack.add(
Network::new("net".to_string()).settings(settings).build(),
ResourceLifecycle::Frozen,
);
}
let stack = stack.build();
let found = sandbox_egress_network(&stack)?;
assert_eq!(found.id, "net");
Ok(found.vpc)
}
#[test]
fn a_deny_sandbox_attaches_to_the_stacks_first_network() {
let create = || NetworkSettings::Create {
cidr: None,
availability_zones: 2,
};
let byo_aws = || NetworkSettings::ByoVpcAws {
vpc_id: "vpc-1".to_string(),
public_subnet_ids: vec!["subnet-pub".to_string()],
private_subnet_ids: vec!["subnet-priv".to_string()],
security_group_ids: vec![],
};
for (first, second, expected) in [
(create(), byo_aws(), Created),
(byo_aws(), create(), BroughtByCustomer),
] {
let stack = Stack::new("acme".to_string())
.add(
Network::new("first".to_string()).settings(first).build(),
ResourceLifecycle::Frozen,
)
.add(
Network::new("second".to_string()).settings(second).build(),
ResourceLifecycle::Frozen,
)
.build();
let found =
sandbox_egress_network(&stack).expect("a created or brought VPC is attachable");
assert_eq!((found.id, found.vpc), ("first", expected));
}
}
#[test]
fn the_operator_role_tags_only_the_interface_it_is_creating() {
let policy = sandbox_egress_operator_policy("aws", "123456789012", "us-east-1");
let tagging = policy["Statement"]
.as_array()
.expect("statements")
.iter()
.find(|statement| statement["Action"] == "ec2:CreateTags")
.expect("the role tags the interfaces it creates");
assert_eq!(
tagging["Condition"],
json!({
"StringEquals": {
"ec2:CreateAction": "CreateNetworkInterface",
"ec2:ManagedResourceOperator": "network-connectors.lambda.amazonaws.com"
}
})
);
}
#[test]
fn every_egress_mode_and_network_setting_resolves_to_one_outcome() {
let create = || NetworkSettings::Create {
cidr: None,
availability_zones: 2,
};
let byo_aws = || NetworkSettings::ByoVpcAws {
vpc_id: "vpc-1".to_string(),
public_subnet_ids: vec!["subnet-pub".to_string()],
private_subnet_ids: vec!["subnet-priv".to_string()],
security_group_ids: vec![],
};
let byo_gcp = || NetworkSettings::ByoVpcGcp {
network_name: "vpc".to_string(),
subnet_name: "subnet".to_string(),
region: "us-central1".to_string(),
};
let cases = [
(SandboxEgress::Internet, None, Err(NoNetwork)),
(
SandboxEgress::Internet,
Some(NetworkSettings::UseDefault),
Err(DefaultVpc),
),
(
SandboxEgress::Internet,
Some(byo_gcp()),
Err(OtherCloudNetwork),
),
(SandboxEgress::None, None, Err(NoNetwork)),
(SandboxEgress::None, Some(create()), Ok(Created)),
(SandboxEgress::None, Some(byo_aws()), Ok(BroughtByCustomer)),
(
SandboxEgress::None,
Some(NetworkSettings::UseDefault),
Err(DefaultVpc),
),
(SandboxEgress::None, Some(byo_gcp()), Err(OtherCloudNetwork)),
];
for (egress, network, expected) in cases {
let label = format!("{egress:?} on {network:?}");
assert_eq!(outcome(egress, network), expected, "{label}");
}
}
}