亚马逊AWS官方博客

在亚马逊云科技 VPC 中构建外部可访问的沙盒——以 Firecracker 为例,基于 IPv6 前缀委派为每个沙盒分配公网地址

摘要:本文详细介绍如何在 AWS VPC 中基于 IPv6 前缀委派构建 Firecracker 沙盒


一、前言

沙盒是运行不受信任代码的常用方式,典型场景包括在线代码执行、临时开发环境、多租户 playground 等;近来 AI Agent 也成为一个热门用例—Agent 生成并执行的代码同样需要隔离运行。Firecracker microVM 凭借毫秒级启动和基于硬件虚拟化的隔离,成为构建沙盒的常见选择。

现有沙盒方案大部分仅提供有限的 HTTP 外部访问。在一些场景中,用户需要从外部直接访问沙盒,用于管理、调试、承载复杂应用等。仅提供 HTTP 访问的沙盒无法满足多样化的网络需求。本方案提供一个在 AWS VPC 中创建外部可以直接访问沙盒的解决方案。

本方案使用 IPv6 满足沙盒的外部访问需求。在 AWS VPC 中,一个子网可以获取一个 /64 Ipv6 前缀,而通过弹性网络接口(Elastic Network Interface, ENI)的前缀委派(Prefix Delegation)功能,一个 ENI 可以获得一个或多个 /80 前缀,即 2⁴⁸ 个地址。EC2 宿主机可以自主把其中任意 /128 分配给沙盒,无需为每个沙盒调用 AWS API。考虑到大量服务仍然只支持 IPv4,我们仍为沙盒配置一个私网 IPv4,由宿主做 NAT 出站。

本文将详细介绍如何在 AWS VPC 中基于 IPv6 前缀委派构建 Firecracker 沙盒,实现以下目标:

  • 每个沙盒自动获得一个来自委派 /80 的 IPv6 /128 地址,同一沙盒每次启动地址不变;
  • 沙盒中运行的任何服务都可以从公网直接访问,无需端口映射;
  • 沙盒具备 IPv4 出站能力;
  • 沙盒中的代码无法访问宿主本机和实例元数据服务(Instance Metadata Service, IMDS),外部也无法经沙盒所在的 ENI 访问宿主。

⚠️ 本文中所有示例基于 Amazon Linux 2023 操作系统和 r8i.large 实例,其它发行版请根据实际情况调整相关命令。命令中与环境相关的值都集中在变量中定义,输出示例中的 2001:db8::/32、203.0.113.0/24 地址仅为示意。

图 1 - 整体架构

[图 1 – 整体架构]

二、AWS VPC 与 IPv6 前缀委派

2.1 前缀委派的网络行为

在这里有必要对委派前缀在 VPC 中的网络行为进行说明,因为它直接决定了宿主上的网络配置。

1. 委派的 /80 是 ENI 合法拥有的地址空间。

AWS 知道这段 /80 位于这个 ENI 之后,因此我们既不需要关闭 ENI 的源/目标检查(Source/Destination Check),也不需要修改子网路由表。反过来,正因为源/目标检查保持开启,源地址属于这段 /80 的报文必须从这个 ENI 发出;如果从同一实例的其它 ENI 发出,会被直接丢弃。这一点在双 ENI 设计中需要通过策略路由来保证(见“六、配置策略路由”)。

2. AWS VPC 路由器不对委派前缀做邻居发现。

在传统 IPv6 网络中,上游路由器会通过邻居发现(Neighbor Discovery Protocol, NDP)询问“谁拥有这个 /128”(对应 IPv4 的 ARP),宿主需要用 ndppd 之类的工具代答。VPC 不需要这一步:发往委派前缀的报文会被直接投递到 ENI。

3. 委派前缀的长度固定为 /80。

IPv6 无状态地址自动配置(SLAAC)要求前缀长度为 /64,因此在 /80 下不可用。我们使用 DHCPv6 有状态模式从地址池中逐个下发 /128,路由通告(Router Advertisement, RA)仍然需要发送,用于通告默认路由,并通过 M 标志告诉沙盒使用 DHCPv6 获取地址。

2.2 沙盒网络设计

沙盒中会运行什么服务、使用哪些端口,事先无法预知,所以承载沙盒流量的 ENI 需要一个入站放开的安全组;但这种放开绝不能波及宿主的管理面。基于以上原因,在本文中我们为实例配置两个 ENI:

  • 主 ENI(eth0):宿主的管理面,安全组只放行管理端口;
  • 专用 ENI(eth1):承载委派的 /80,安全组入站放开,ENI 自身不分配 IPv6 地址。

在宿主内部,我们创建一个网桥 br0 作为所有沙盒的二层网络和三层网关,每个 Firecracker microVM 通过一个 TAP 设备接入 br0;dnsmasq 只在 br0 上提供 RA、DHCPv6、DHCPv4 和 DNS 服务。

br0 上需要配置一个委派前缀内的地址,本文使用 prefix::1/80:dnsmasq 据此在 br0 上启用 /80 地址池,内核据此生成指向 br0 的 /80 路由,它也是下发给沙盒的 DNS 服务器地址。需要注意的是,沙盒的 IPv6 默认路由实际指向 br0 的 link-local 地址,因为 RA 总是从 link-local 地址发出。

⚠️ “专用 ENI 不分配 IPv6 地址“并不等于“宿主在这个 ENI 上不可达”。prefix::1 是宿主的本机地址,从 eth1 进入、发往 prefix::1 的报文会交给本机处理,sshd 默认又监听在 [::]:22。因此,宿主上的防火墙规则才是保护宿主的主防线(见“七、配置防火墙”)。

三、配置流程

整体配置流程如下:

  • 为 VPC 和子网开启 IPv6,创建两个安全组分别用于管理和沙盒;
  • 创建支持嵌套虚拟化(或裸金属)的实例,并为其附加带有委派前缀的专用 ENI;
  • 在宿主上安装软件、准备沙盒镜像;
  • 配置网桥、策略路由、防火墙和 dnsmasq;
  • 启动沙盒并验证。

步骤一至三在安装并配置了 AWS CLI 的电脑上或 CloudShell 里执行(本文假设 AWS CLI 访问凭证已经正确配置并分配了足够的 IAM 权限),步骤四及之后在宿主上以 root 身份执行。首先在工作站上设置以下变量,这是整个流程中唯一需要手工填写的部分:

# ==== 请按您的环境修改 ====
VPC=vpc-xxxxxxxx              # 已有的 VPC
SUBNET=subnet-xxxxxxxx        # 公有子网
RTB=rtb-xxxxxxxx              # 该子网关联的路由表
IGW=igw-xxxxxxxx              # VPC 的互联网网关
MGMT_CIDR=x.x.x.x/x           # 允许 SSH 登录宿主的管理网段
KEY_NAME=xxxxxx               # EC2 密钥对名称

其余的资源 ID 和地址都由后续命令自动获取并保存到变量中。

3.1 准备 VPC

本方案要求 VPC 已具备 IPv4 出网能力:公有子网(0.0.0.0/0 指向互联网网关 Internet Gateway, IGW),或者私有子网配合 NAT 网关(NAT Gateway)。沙盒的 IPv4 出站依赖宿主自身的出网能力。

对于一个只有 IPv4 的 VPC,执行以下命令开启 IPv6:

# 为 VPC 关联 Amazon 提供的 /56
VPC_ASSOC=$(aws ec2 associate-vpc-cidr-block --vpc-id $VPC --amazon-provided-ipv6-cidr-block \
    --query Ipv6CidrBlockAssociation.AssociationId --output text)
sleep 10      # 等待关联完成
VPC_V6=$(aws ec2 describe-vpcs --vpc-ids $VPC \
    --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock' --output text)
echo $VPC_V6
# 2001:db8:abcd:1200::/56

# 取 /56 中的第一个 /64 分配给子网
SUBNET_ASSOC=$(aws ec2 associate-subnet-cidr-block --subnet-id $SUBNET \
    --ipv6-cidr-block ${VPC_V6%/56}/64 \
    --query Ipv6CidrBlockAssociation.AssociationId --output text)

# 子网路由表添加 IPv6 默认路由
aws ec2 create-route --route-table-id $RTB --destination-ipv6-cidr-block ::/0 --gateway-id $IGW

子网的“自动分配 IPv6 地址”保持关闭即可。VPC_ASSOC 和 SUBNET_ASSOC 在清理资源时会用到。

3.2 创建安全组

分别为两个 ENI 创建安全组:

SG_MGMT=$(aws ec2 create-security-group --vpc-id $VPC --group-name sandbox-mgmt \
    --description "host management (eth0)" --query GroupId --output text)
SG_SANDBOX=$(aws ec2 create-security-group --vpc-id $VPC --group-name sandbox-eni \
    --description "sandbox ENI (eth1)" --query GroupId --output text)

# 管理面:仅放行来自管理网段的 SSH
aws ec2 authorize-security-group-ingress --group-id $SG_MGMT \
    --ip-permissions "IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=$MGMT_CIDR}]"
# 沙盒 ENI:放行所有 IPv6 入站
aws ec2 authorize-security-group-ingress --group-id $SG_SANDBOX \
    --ip-permissions 'IpProtocol=-1,Ipv6Ranges=[{CidrIpv6=::/0}]'

⚠️ 沙盒 ENI 的安全组只放行 IPv6,不要添加 IPv4 0.0.0.0/0,这样该 ENI 的主私网 IPv4 地址保持关闭。在测试阶段,建议先将 ::/0 收紧为测试客户端的 /128。

3.3 创建实例与专用 ENI

r8i 实例支持嵌套虚拟化,在普通实例中即可使用 /dev/kvm 运行 Firecracker。也可以使用 .metal 裸金属实例。可以通过以下命令确认:

aws ec2 describe-instance-types --instance-types r8i.large \
    --query 'InstanceTypes[0].ProcessorInfo.SupportedFeatures'
# [ "nested-virtualization", "metal-compatibility" ]

嵌套虚拟化默认关闭,需要在创建实例时通过 CPU 选项开启:

INSTANCE=$(aws ec2 run-instances --instance-type r8i.large \
    --cpu-options NestedVirtualization=enabled \
    --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
    --key-name $KEY_NAME \
    --network-interfaces "DeviceIndex=0,SubnetId=$SUBNET,Groups=$SG_MGMT,AssociatePublicIpAddress=true,Ipv6AddressCount=1" \
    --query 'Instances[0].InstanceId' --output text)
aws ec2 wait instance-running --instance-ids $INSTANCE

然后在同一子网创建专用 ENI,创建时直接委派一个 /80 前缀,并附加到实例:

ENI=$(aws ec2 create-network-interface --subnet-id $SUBNET --groups $SG_SANDBOX \
    --ipv6-prefix-count 1 --query NetworkInterface.NetworkInterfaceId --output text)
aws ec2 attach-network-interface --network-interface-id $ENI --instance-id $INSTANCE --device-index 1

aws ec2 describe-network-interfaces --network-interface-ids $ENI \
    --query 'NetworkInterfaces[0].[Ipv6Prefixes,Ipv6Addresses]'
# [ [ { "Ipv6Prefix": "2001:db8:abcd:1234:aaaa::/80" } ], [] ]

以上输出表明这个 ENI 只获得了一个 /80 前缀,没有任何 IPv6 地址。请记下这个前缀,下一步在宿主上会用到。

3.4 安装软件并准备沙盒镜像

登录宿主,首先设置宿主上用到的变量:

# ==== 在宿主上设置 ====
PREFIX=xxxx:xxxx:xxxx:xxxx:xxxx   # 步骤三得到的委派前缀,去掉末尾的 ::/80
MGMT_IF=enp39s0                  # 主 ENI 的接口名
SBX_IF=enp40s0                   # 专用 ENI 的接口名
BR=br0; V4NET=172.16.0.0/24

接口名可以通过 ip -br link 查看,在 Amazon Linux 2023 的 r8i 实例中通常为 enp39s0 和 enp40s0,下文统一称为 eth0 和 eth1。后续步骤中的命令和配置文件都引用这些变量,请在同一个 root shell 中依次执行(或者使用 sudo)。

然后安装软件,并下载 Firecracker 及官方提供的 guest 内核与 Ubuntu rootfs:

dnf install -y dnsmasq nftables squashfs-tools e2fsprogs

mkdir -p /opt/fc && cd /opt/fc
curl -fsSL https://github.com/firecracker-microvm/firecracker/releases/download/v1.17.0/firecracker-v1.17.0-x86_64.tgz | tar -xz
install -m 0755 release-v1.17.0-x86_64/firecracker-v1.17.0-x86_64 /usr/local/bin/firecracker
curl -fsSL -o vmlinux         https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/x86_64/vmlinux-6.1.155
curl -fsSL -o ubuntu.squashfs https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/x86_64/ubuntu-24.04.squashfs
unsquashfs ubuntu.squashfs      # 解出到 /opt/fc/squashfs-root

如需使用更新的版本,Firecracker 可以在 GitHub Releases 页面获取;guest 镜像位于 spec.ccfc.min 存储桶的 firecracker-ci/<版本>/x86_64/ 目录下。CI 镜像目录通常落后于 Firecracker 的发布版本,应选择存储桶中最新的版本目录,而不是直接套用 Release 版本号。

官方 rootfs 自带的 fcnet.service 会根据 MAC 地址推算一个静态 IP,我们需要将其替换为 systemd-networkd 的 DHCP,并放入登录沙盒用的 SSH 公钥:

R=/opt/fc/squashfs-root
ln -sf /dev/null $R/etc/systemd/system/fcnet.service       # 屏蔽 fcnet
cat > $R/etc/systemd/network/10-eth0.network <<'EOF'
[Match]
Name=eth0

[Network]
DHCP=yes
IPv6AcceptRA=yes

[DHCPv4]
ClientIdentifier=mac

[DHCPv6]
DUIDType=link-layer
EOF
ln -sf /usr/lib/systemd/system/systemd-networkd.service \
       $R/etc/systemd/system/multi-user.target.wants/systemd-networkd.service
rm -f $R/etc/resolv.conf; echo 'nameserver 172.16.0.1' > $R/etc/resolv.conf   # 镜像中没有 systemd-resolved
: > $R/etc/machine-id                                      # 克隆镜像需清空 machine-id

mkdir -p -m 700 /root/.ssh && ssh-keygen -q -t ed25519 -N '' -f /root/.ssh/fc_guest
mkdir -p $R/root/.ssh && cp /root/.ssh/fc_guest.pub $R/root/.ssh/authorized_keys

truncate -s 1G rootfs.ext4 && mkfs.ext4 -q -d $R -F rootfs.ext4

⚠️ 所有沙盒都从同一个镜像复制而来。systemd-networkd 默认使用 machine-id 派生 DHCPv6 的 DUID,如果不做处理,所有沙盒的 DUID 相同,彼此会争抢同一个地址。DUIDType=link-layer 让 DUID 改由 MAC 地址派生(00:03:00:01:<MAC>),只要每个沙盒的 MAC 唯一,地址就唯一,并且同一个 MAC 每次启动都会获得同一个 /128;ClientIdentifier=mac 对 DHCPv4 起到同样的作用。

3.5 配置网桥

首先开启 IP 转发:

cat > /etc/sysctl.d/99-sandbox.conf <<EOF
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
sysctl -p /etc/sysctl.d/99-sandbox.conf

然后通过 systemd-networkd 声明网桥 br0,使配置在重启后自动恢复:

cat > /etc/systemd/network/20-br0.netdev <<EOF
[NetDev]
Name=br0
Kind=bridge
MACAddress=02:fc:00:00:00:01
EOF

cat > /etc/systemd/network/20-br0.network <<EOF
[Match]
Name=br0

[Link]
RequiredForOnline=no

[Network]
ConfigureWithoutCarrier=yes
IPv6AcceptRA=no
LinkLocalAddressing=ipv6

[Address]
Address=${PREFIX}::1/80
DuplicateAddressDetection=none

[Address]
Address=172.16.0.1/24

[Route]
Destination=${PREFIX}::/80
Table=100
EOF

networkctl reload

几个参数值得说明:

  • MACAddress 固定网桥的 MAC。Linux 网桥默认取所有端口中最小的 MAC,TAP 增减时网桥 MAC 会随之变化,沙盒缓存的网关 MAC 也就失效了;
  • ConfigureWithoutCarrier 与 DuplicateAddressDetection=none:配置生效无需检查是否有端口加入网桥;
  • 最后的 [Route] 在路由表 100 中加入 /80 指向 br0 的路由,供下一步的策略路由使用。

确认网桥地址:

$ ip -br addr show br0
br0              DOWN           172.16.0.1/24 2001:db8:abcd:1234:aaaa::1/80

在还没有沙盒接入时,br0 显示为 DOWN 是正常的。

3.6 配置策略路由

宿主上有两个 ENI,main 路由表中 eth0 的默认路由优先级更高。如果不做额外配置,外部访问沙盒时,沙盒回复报文会按 main 表从 eth0 发出。源地址属于 eth1 的委派前缀,从 eth0 发出即被源/目标检查丢弃,客户端只能看到连接超时。

解决方法是为源自 /80 的流量建立独立的路由表,让其默认路由经 eth1 发出。Amazon Linux 2023 的 amazon-ec2-net-utils 已经为 eth1 生成了 systemd-networkd 配置,我们以 drop-in 的方式追加策略路由:

mkdir -p /etc/systemd/network/70-${SBX_IF}.network.d
cat > /etc/systemd/network/70-${SBX_IF}.network.d/50-sandbox-pd.conf <<EOF
[RoutingPolicyRule]
From=${PREFIX}::/80
Table=100
Priority=1000
Family=ipv6

[Route]
Gateway=_ipv6ra
Table=100
EOF

# 为路由表命名(可选)。AL2023 默认没有 /etc/iproute2,先复制系统自带的文件
mkdir -p /etc/iproute2 && cp -n /usr/share/iproute2/rt_tables /etc/iproute2/
echo "100 sandbox" >> /etc/iproute2/rt_tables
networkctl reload && networkctl reconfigure $SBX_IF

70-${SBX_IF}.network 是 amazon-ec2-net-utils 为 eth1 生成的配置文件,Gateway=_ipv6ra 表示使用经 RA 学到的路由器地址,避免硬编码 VPC 路由器的 link-local 地址。

确认策略路由:

$ ip -6 rule
1000:   from 2001:db8:abcd:1234:aaaa::/80 lookup sandbox proto static
$ ip -6 route show table 100
2001:db8:abcd:1234:aaaa::/80 dev br0 proto static metric 1024 pref medium
default via fe80::b9:98ff:fe43:3e1b dev enp40s0 proto ra metric 513 expires 1793sec pref medium

⚠️ 请不要用 ip rule / ip route 命令手工添加这些规则。systemd-networkd 默认会管理其接口上的“外来”规则和路由[注 1],一次 networkctl reconfigure、DHCP 续租或重启就可能将它们清除,沙盒的入站访问随即中断。

3.7 配置防火墙

防火墙是本方案安全设计的核心,我们使用 nftables 实现以下规则:

  • 沙盒访问宿主:只放行 NDP(路由请求、邻居请求与通告,缺少它们沙盒将找不到网关)、ping、DHCP 和 DNS;
  • 经 eth1 访问宿主:只放行与 VPC 路由器之间的 NDP、RA 以及 eth1 自身的 DHCPv4 回包,其余全部丢弃,以保护 prefix::1;
  • 沙盒访问 宿主 IMDS:全部丢弃。防止沙盒内的任何代码(例如被提示注入的 AI Agent 执行的命令)借此获取实例角色凭证 [注 2];
  • IPv4 出站:沙盒的私网 IPv4 经 eth0 做 masquerade。

在宿主上执行以下命令:

cat > /etc/nftables/sandbox.nft <<EOF
table inet sandbox
delete table inet sandbox
table inet sandbox {
    chain input {
        type filter hook input priority 0; policy accept;
        ct state established,related accept
        iifname "$BR"     jump in_from_sandbox
        iifname "$SBX_IF" jump in_from_sbx_eni
    }

    # 沙盒 -> 宿主本机:只放行 NDP/ping/DHCP/DNS
    chain in_from_sandbox {
        icmpv6 type { nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert, echo-request } accept
        icmp type echo-request accept
        udp dport { 53, 67, 547 } accept
        tcp dport 53 accept
        counter drop
    }

    # 专用 ENI -> 宿主本机:只放行 NDP/RA 与 DHCPv4 回包
    chain in_from_sbx_eni {
        icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
        udp sport 67 udp dport 68 accept
        counter drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        iifname "$BR" ip  daddr 169.254.169.254 counter drop     # IMDS v4
        iifname "$BR" ip6 daddr fd00:ec2::254   counter drop     # IMDS v6
        iifname "$BR" accept                                     # 沙盒出站
        iifname "$SBX_IF" oifname "$BR" ip6 daddr ${PREFIX}::/80 accept   # 外部 -> 沙盒 /128
        counter
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        ip saddr $V4NET oifname "$MGMT_IF" masquerade
    }
}
EOF
nft -f /etc/nftables/sandbox.nft
echo 'include "/etc/nftables/sandbox.nft"' >> /etc/sysconfig/nftables.conf
systemctl enable nftables

我们使用独立的 inet sandbox 表,而不是 flush ruleset,以免清除 firewalld、Docker 等其它组件的规则。文件开头的 table / delete table 两行使该文件可以重复加载。

3.8 配置 dnsmasq

创建 dnsmasq 配置文件:

cat > /etc/dnsmasq.d/sandbox.conf <<EOF
interface=${BR}
# 允许 br0 晚于 dnsmasq 出现
bind-dynamic
# 上游使用 Amazon DNS
no-resolv
server=169.254.169.253

# DHCPv4
dhcp-range=172.16.0.10,172.16.0.250,255.255.255.0,12h
dhcp-option=option:router,172.16.0.1
dhcp-option=option:dns-server,172.16.0.1

# RA + DHCPv6 有状态模式:显式地址范围,前缀长度 80
enable-ra
dhcp-range=${PREFIX}::10,${PREFIX}::ffff,80,12h
# [::] 表示本机在 br0 上的地址,即 prefix::1
dhcp-option=option6:dns-server,[::]
# 路由器生存期 600 秒,默认仅为通告间隔的 3 倍
ra-param=${BR},high,10,600
EOF

Amazon Linux 2023 自带的 /etc/dnsmasq.conf 中已经启用了 bind-interfaces,它与 bind-dynamic 互斥,需要先将其注释掉,然后启动 dnsmasq:

sed -i 's/^bind-interfaces$/#bind-interfaces/' /etc/dnsmasq.conf
systemctl enable --now dnsmasq

3.9 启动沙盒

以下脚本为一个沙盒创建 TAP 设备、生成 Firecracker 配置并启动 microVM。每个沙盒的 MAC 由编号派生,以保证唯一。将其保存为 /opt/fc/sandbox-up.sh:

#!/bin/bash
# sandbox-up.sh <ID>
set -euo pipefail
ID=$1
TAP="tap${ID}"
MAC=$(printf 'AA:FC:00:00:%02X:%02X' $((ID >> 8)) $((ID & 255)))
RUN=/run/fc; mkdir -p $RUN

ip tuntap add $TAP mode tap
ip link set $TAP master br0
ip link set $TAP up

cp --sparse=always /opt/fc/rootfs.ext4 $RUN/rootfs-${ID}.ext4   # 每个沙盒独立的可写 rootfs

cat > $RUN/sandbox${ID}.json <<EOF
{
  "boot-source": {
    "kernel_image_path": "/opt/fc/vmlinux",
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
  },
  "drives": [
    { "drive_id": "rootfs", "path_on_host": "$RUN/rootfs-${ID}.ext4",
      "is_root_device": true, "is_read_only": false }
  ],
  "machine-config": { "vcpu_count": 1, "mem_size_mib": 512 },
  "network-interfaces": [
    { "iface_id": "eth0", "host_dev_name": "$TAP", "guest_mac": "$MAC" }
  ]
}
EOF

rm -f $RUN/sandbox${ID}.sock
setsid firecracker --config-file $RUN/sandbox${ID}.json --api-sock $RUN/sandbox${ID}.sock \
    </dev/null >$RUN/sandbox${ID}.log 2>&1 &
echo "sandbox ${ID} started: tap=${TAP} mac=${MAC} pid=$!"

启动两个沙盒:

cd /opt/fc && chmod +x sandbox-up.sh
./sandbox-up.sh 1
./sandbox-up.sh 2

沙盒收到带有 M 标志的 RA 后,systemd-networkd 会自动发起 DHCPv6 请求,同时完成 DHCPv4。dnsmasq 的日志中可以看到地址分配过程:

DHCPACK(br0) 172.16.0.123 aa:fc:00:00:00:01 ubuntu-fc-uvm
DHCPSOLICIT(br0) 00:03:00:01:aa:fc:00:00:00:01
DHCPREPLY(br0) 2001:db8:abcd:1234:aaaa::8531 00:03:00:01:aa:fc:00:00:00:01 ubuntu-fc-uvm

如需更强的隔离,可以使用 Firecracker 提供的 jailer 启动 microVM,本文不展开。

3.10 验证

本文示例中,沙盒 1 获得 172.16.0.123 和 2001:db8:abcd:1234:aaaa::8531,沙盒 2 获得 172.16.0.124 和 2001:db8:abcd:1234:aaaa::19a5,外部测试客户端的地址为 2001:db8:ffff::2。以下命令中的沙盒地址请以 dnsmasq 日志中实际分配的地址为准。

1. 确认沙盒自动获得地址

在宿主上登录沙盒 1,查看地址和路由:

$ ssh -i /root/.ssh/fc_guest root@172.16.0.123
root@ubuntu-fc-uvm:~# ip -br addr show eth0
eth0  UP  172.16.0.123/24 2001:db8:abcd:1234:aaaa::8531/128 fe80::a8fc:ff:fe00:1/64
root@ubuntu-fc-uvm:~# ip -6 route
2001:db8:abcd:1234:aaaa::/80 dev eth0 proto ra metric 512 expires 43197sec mtu 1500 hoplimit 64 pref high
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::fc:ff:fe00:1 dev eth0 proto ra metric 512 expires 597sec mtu 1500 hoplimit 64 pref high

以上输出表明沙盒已经通过 DHCPv6 获得了 /128 地址,IPv6 默认路由指向 br0 的 link-local 地址。停止沙盒后重新启动,沙盒仍会获得相同的 IPv4 和 IPv6 地址。

2. 创建简单的 web 页面

在沙盒 1 中执行:

echo "Hello from Firecracker sandbox!" > index.html
nohup python3 -m http.server 80 --bind :: >/dev/null 2>&1 &

3. 验证外部访问

在外部测试客户端上执行:

$ curl -g -6 'http://[2001:db8:abcd:1234:aaaa::8531]/'
Hello from Firecracker sandbox!
$ ping6 -c 3 2001:db8:abcd:1234:aaaa::19a5
64 bytes from 2001:db8:abcd:1234:aaaa::19a5: icmp_seq=1 ttl=55 time=30.2 ms
$ nc -6 -vz -w 4 2001:db8:abcd:1234:aaaa::1 22
Ncat: Connection timed out.

以上输出表明外部可以直接访问沙盒中的服务,而访问宿主的 prefix::1 被防火墙丢弃。

4. 验证沙盒出站与安全边界

在沙盒 1 中分别执行以下操作,结果如下:

getent ahosts aws.amazon.com                       OK                    ← DNS 经 dnsmasq
ping -6 ipv6.google.com                            OK
curl -6 https://api64.ipify.org                    OK   | ...aaaa::8531
curl -4 https://checkip.amazonaws.com              OK   | 203.0.113.25
-> 宿主 sshd [prefix::1]:22 / 172.16.0.1:22        FAIL                  ← INPUT 丢弃
-> IMDS 169.254.169.254 / [fd00:ec2::254]          FAIL | timed out      ← FORWARD 丢弃

以上输出表明沙盒可以正常解析域名并经 IPv6、IPv4 出站,外部看到的 IPv6 源地址就是沙盒自己的 /128;访问宿主本机和 IMDS 均被拒绝。可以通过 nft list ruleset 查看各条丢弃规则的计数器,确认流量被哪条规则拦截。

3.11 清理资源

在工作站上,使用前面保存的变量执行:

aws ec2 terminate-instances --instance-ids $INSTANCE
aws ec2 wait instance-terminated --instance-ids $INSTANCE
aws ec2 delete-network-interface --network-interface-id $ENI   # 专用 ENI 需单独删除,委派前缀随之释放
aws ec2 delete-security-group --group-id $SG_SANDBOX
aws ec2 delete-security-group --group-id $SG_MGMT

# 如需将 VPC 恢复为仅 IPv4,按以下顺序执行
aws ec2 delete-route --route-table-id $RTB --destination-ipv6-cidr-block ::/0
aws ec2 disassociate-subnet-cidr-block --association-id $SUBNET_ASSOC
aws ec2 disassociate-vpc-cidr-block --association-id $VPC_ASSOC

四、总结与讨论

本博客介绍了如何在 AWS VPC 中利用 ENI 的 IPv6 前缀委派,为每个 Firecracker 沙盒分配一个外部可直接访问的 IPv6 /128 地址,并为沙盒提供 IPv4 出站能力。该方案不需要关闭源/目标检查、不需要修改子网路由表、也不需要 NDP 代答;地址由宿主上的 dnsmasq 通过 DHCPv6 自动下发,沙盒的创建不涉及任何 AWS API 调用。安全上,我们通过双 ENI 将入站放开的安全组与宿主管理面分离,并依靠宿主防火墙阻止沙盒和外部访问宿主本机,阻止沙盒访问 IMDS。

4.1 讨论

1. 沙盒之间的隔离

RA 通告的 /80 和沙盒的 IPv4 /24 在沙盒中都是直连(on-link)网段,因此默认情况下沙盒之间可以直接互访,流量在网桥内二层转发,不经过宿主的 FORWARD 链。这适合同一用户的多个沙盒协作,例如一个沙盒运行服务、另一个沙盒对其进行测试。如果沙盒属于不同的用户或租户,需要在二层和三层同时隔离:在 sandbox-up.sh 中为 TAP 开启端口隔离,

bridge link set dev $TAP isolated on

并在防火墙 forward 链的 iifname “$BR” accept 之前加入以下规则,防止沙盒以 br0 为网关绕行:

iifname "$BR" oifname "$BR" counter drop

2. 私有子网与仅 VPC 内访问

如果宿主位于私有子网,IPv4 出站可以经 NAT 网关完成。但 IPv6 如果使用仅出口互联网网关(Egress-only Internet Gateway),外部将无法主动连入沙盒。如果沙盒只需要在 VPC 内、或经 VPC 对等连接(VPC Peering)、Transit Gateway 被访问,子网无需 IPv6 默认路由,宿主和沙盒的配置完全相同。

3. 规模化

沙盒负载通常具有数量多、生命周期短、突发性强的特点。一个 /80 包含 2⁴⁸ 个地址,创建沙盒只是在本地创建一个 TAP 并完成一次 DHCP,瓶颈在于宿主的 CPU 和内存,而不是地址。如需更多前缀,可以为 ENI 委派更多的 /80,并相应增加 br0 地址、dnsmasq 地址池、策略路由规则和防火墙放行规则。

4. 可公开访问的 IPv4

AWS VPC 同样支持 IPv4 的前缀委派,可以用类似的方法实现使用 IPv4 地址从外部访问沙盒。与 IPv6 相比,使用 IPv4 有几个明显的限制:

  • IPv4 前缀委派仅支持/28 前缀长度,即使可以分配多个连续的前缀,总的可用地址数量与 IPv6 相比也非常有限;
  • IPv4 前缀委派必须使用子网内的私有 IP 地址,虽然可以通过关联弹性公网 IP 的方式承载来自公网的直接访问,但需要额外的配置步骤,并且需要承担公网 IP 的费用。相比之下,IPv6 地址完全免费。

4.2 注释

[注 1] systemd-networkd 的 ManageForeignRoutingPolicyRules= 与 ManageForeignRoutes= 默认开启,参见 man networkd.conf。

[注 2] 实例元数据服务的响应跳数限制(HttpPutResponseHopLimit)。可以通过 aws ec2 describe-instances –query ‘Reservations[].Instances[].MetadataOptions’ 查看。参见 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-options.html

4.3 参考资料:

➡️ 下一步行动:

相关产品:

相关文章:

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

刘俊辉

亚马逊云科技高级解决方案架构师,在通信、互联网、云计算行业有多年从业经验,对云原生产品技术及解决方案有非常深入的理解。长期跟踪云计算前沿技术动态,目前专注于亚马逊云科技初创生态。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用