Signs of Triviality

Opinions, mostly my own, on the importance of being and other things.
[homepage] [index] [jschauma@netmeister.org] [@jschauma] [RSS]

Creating AWS IPv4/IPv6 Dual Stack EC2 Instances

May 19th, 2020

There's no place like ::1.AWS EC2 is a wonderful tool for teaching System Administration: you can quickly and easily spin up instances running different operating systems in different regions across the globe and play around with them in a matter of minutes. Unfortunately, by default, all of them come up as IPv4 only.

When it comes to IPv6 support, AWS has been surprisingly slow in its adoption. EC2 only gained support for IPv6 in 2017 (!), and Elastic IPs are still IPv4 only. Even basic dualstack support for EC2 instances requires the creation of a VPC and a number of configuration steps that are not trivial for novice users to implement.

If you've had your AWS account for long enough (i.e., you're on "EC2-Classic"), you don't even have a default VPC and thus can't enable IPv6 on that. For that reason, I've been going back to using an IPv6 tunnelbroker to enable IPv6 on my default EC2 instances when I wanted to create a dualstack environment.[1]

Anyway, since I'll forget how to do this or where to find the right commands, here are my notes on creating a dualstack VPC in the hopes that somebody else may find them useful. At the bottom, you'll also find a single script to run all commands in one go:


Create a new VPC

In the UI, go to 'Services'->'Network & Content Delivery' -> 'VPC' -> 'Your VPCs'. Select 'Create VPC'.

Let's call it "dualstack", give it an RFC1918 CIDR of your liking (let's say "10.10.0.0/24"), select "Amazon provided IPv6 CIDR block", and "Default" tenancy.

AWS Console:
Create VPC

After creation of the VPC, select it and 'Enable DNS hostnames' and 'Enable DNS resolution'. (Note: DNS hostnames are only added with IPv4 addresses. As of May 2020, Amazon says: We do not support IPv6 DNS hostnames for your instance.)

Via the command-line, we'll use JSON output as the default (output = json in ~/.aws/config) and then utilize the versatile jq(1) command. To create the VPC, you'd then run:

$ export VPCID="$(aws ec2 create-vpc --cidr-block 10.10.0.0/24 --amazon-provided-ipv6-cidr-block | jq -r '.Vpc.VpcId')"
$ aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${VPCID}"
$ aws ec2 modify-vpc-attribute --enable-dns-hostnames --vpc-id "${VPCID}"
$ aws ec2 modify-vpc-attribute --enable-dns-support --vpc-id "${VPCID}"

Create a new Internet Gateway

Hosts on our VPC want to talk to the internet, so let's create an internet gateway. To create a new Internet Gateway using the UI, go to 'Services'->'Network & Content Delivery' -> 'VPC' -> 'Internet Gateways'. Select 'Create Internet Gateway'. For consistency, let's name it "dualstack" as well.

AWS Console:
Create Internet Gateway

Via the command-line, that'd be:

$ export IGW="$(aws ec2 create-internet-gateway | jq -r '.InternetGateway.InternetGatewayId')"
$ aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${IGW}"

Attach the Gateway to your VPC

In the UI, select the newly created Internet Gateway, then select 'Actions'->'Attach to VPC' and select the newly created VPC.

AWS Console:
Attach Internet Gateway to VPC

Via the command-line, that'd be:

$ aws ec2 attach-internet-gateway --vpc-id "${VPCID}" --internet-gateway-id "${IGW}"

Create a new Subnet

Our hosts need to live somewhere in our VPC, so let's create a new subnet. This will be a dualstack subnet with both IPv4 and IPv6 addresses and connectivity to the internet.

In the UI, go to 'Services'->'Network & Content Delivery' -> 'VPC' -> 'Subnets'. Select 'Create subnet', give it the "dualstack" name tag, and pick your newly created VPC "dualstack". Next, carve out a suitable CIDR from your /24 for this subnet; let's say, a /26 (i.e., "10.10.0.0/26"). Select 'Custom IPv6' and fill in '00' for the IPv6 /64 CIDR.

AWS Console:
Create Subnet

Next, select the newly created subnet and choose "Modify auto-assign IP settings" to enable a public IPv4 and a public IPv6 address:

AWS Console:
Modify Subnet

Via the command-line, we first need to determine the IPv6 CIDR associated with your VPC. AWS assigns a /56 by default, so we'll carve that up into a reasonably sized /64:

$ export V6CIDR=$(aws ec2 describe-vpcs --vpc-id "${VPCID}" | \
         jq -r '.Vpcs[].Ipv6CidrBlockAssociationSet[].Ipv6CidrBlock')
$ export SUBNET="$(aws ec2 create-subnet --cidr-block 10.10.0.0/26 \
        --ipv6-cidr-block ${V6CIDR%/56}/64 \
        --vpc-id "${VPCID}" | jq -r '.Subnet.SubnetId')"
$ aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${SUBNET}"
$ aws ec2 modify-subnet-attribute --subnet-id "${SUBNET}" --assign-ipv6-address-on-creation
$ aws ec2 modify-subnet-attribute --subnet-id "${SUBNET}" --map-public-ip-on-launch

Add a new Route Table

Subnets are all nice and well, but in order to be able to talk to anything beyond the local network, you need some sort of routing. So let's create a new Route Table.

In the UI, go to 'Services'->'Network & Content Delivery' -> 'VPC' -> 'Route Tables'. Select 'Create route table', name it "dualstack", and attach it to the "dualstack" VPC you created above.

AWS Console:
Create Route Table

Via the command-line, that'd be:

$ export RTB="$(aws ec2 create-route-table --vpc-id "${VPCID}" | \
        jq -r '.RouteTable.RouteTableId')"
$ aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${RTB}"

Associate your Route Table with your Subnet

The Route Table needs to be associated with the subnet you created above. In the UI, select the newly created Route Table, then select 'Actions' -> 'Edit Subnet Associations', select your "dualstack" subnet and save.

AWS Console:
Edit Route Table Subnet Associations

Via the command-line, that'd be:

$ aws ec2 associate-route-table --route-table-id "${RTB}" \
        --subnet-id "${SUBNET}" | jq '.AssociationId'

Create Route Table Routes

A Route Table is all nice and well, but it's not very useful without any routes. Let's add some!

In the UI, select the route table you just created, then select 'Actions'->'Edit routes', then 'Add route' and enter "0.0.0.0/0" and select the Internet Gateway you created above as the target. Then repeat the same for IPv6 by using "::/0" as the destination with the same Internet Gateway.

AWS Console:
Edit Routes

Via the command-line, that'd be:

$ aws ec2 create-route --route-table-id "${RTB}" --destination-ipv6-cidr-block ::/0 --gateway-id "${IGW}"
$ aws ec2 create-route --route-table-id "${RTB}" --destination-cidr-block 0.0.0.0/0 --gateway-id "${IGW}"

Create a security group

We need to create a new security group for our EC2 instances. Since security groups are stateful, we don't need to create rules for established TCP connections as you would have to for network ACLs and can only worry about which ports we want to allow traffic into.

In this example, since we are looking to demonstrate what a full dualstack host looks like on the internet, we want to allow any and all traffic. For your purposes, you may wish to restrict the traffic you allow to come in, but you'll have to remember to add rules for both IPv4 and IPv6!

In the UI, go to 'Services' -> 'Network & Content Delivery' -> 'VPC' -> 'Security Groups'. Select 'Create security group', name it "dualstack", give it a description, and associate it with your newly created VPC. After that, select 'Actions'->'Edit inbound rules'->'Add rule' and permit TCP traffic:

AWS Console:
Create Security Group

AWS Console:
Edit Security Group Rules

Via the command-line, that'd be:

$ export SG="$(aws ec2 create-security-group --description "Default security group for dualstack instances" \
        --group-name "dualstack" \
        --vpc-id "${VPCID}" | jq -r '.GroupId')"
$ aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${SG}"
$ aws ec2 authorize-security-group-ingress --group-id "${SG}" \
        --ip-permissions '[{"IpProtocol": "-1", "IpRanges" : [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges" : [{"CidrIpv6": "::/0"}]}]'

Launch an instance

You're now ready to launch a dualstack instance. To do so, you need to remember to specify the correct security group as well as subnet. For example, to launch a FreeBSD 12.1 instance:

$ export ID=$(aws ec2 run-instances --image-id ami-0de268ac2498ba33d \
        --instance-type t2.micro --count 1 --security-group-ids "${SG}" \
        --subnet-id "${SUBNET}" | \
        jq -r '.Instances[].InstanceId')
$ aws ec2 describe-instances --instance-id "${ID}" | \
        jq -r '.Reservations[].Instances[] |
                "\(.PublicDnsName) \(.PublicIpAddress) \(.NetworkInterfaces[].Ipv6Addresses[].Ipv6Address)"'
ec2-54-210-62-255.compute-1.amazonaws.com 54.210.62.255 2600:1f18:400c:b800:380f:b42a:505d:4fab

Wait for it to come up, and low and behold:

$ ping6 -c 3 2600:1f18:400c:b800:380f:b42a:505d:4fab
PING6(56=40+8+8 bytes) 2001:470:30:84:e276:63ff:fe72:3900 --> 2600:1f18:400c:b800:380f:b42a:505d:4fab
16 bytes from 2600:1f18:400c:b800:380f:b42a:505d:4fab, icmp_seq=0 hlim=45 time=7.943 ms
16 bytes from 2600:1f18:400c:b800:380f:b42a:505d:4fab, icmp_seq=1 hlim=45 time=12.318 ms
16 bytes from 2600:1f18:400c:b800:380f:b42a:505d:4fab, icmp_seq=2 hlim=45 time=8.776 ms

--- 2600:1f18:400c:b800:380f:b42a:505d:4fab ping6 statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 7.943/9.679/12.318/2.323 ms

$ ssh ec2-user@ec2-54-210-62-255.compute-1.amazonaws.com "ifconfig xn0"
xn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 9001
        options=503<RXCSUM,TXCSUM,TSO4,LRO>
        ether 06:48:62:d7:64:6f
        inet6 fe80::448:62ff:fed7:646f%xn0 prefixlen 64 scopeid 0x2
        inet6 2600:1f18:400c:b800:380f:b42a:505d:4fab prefixlen 128
        inet 10.10.0.38 netmask 0xffffffc0 broadcast 10.10.0.63
        media: Ethernet manual
        status: active
        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
$ 

Supporting multiple regions

So far, so good. But this is still a pain, because while with the (IPv4-only) defaults, you can trivially launch an instance in any region so long as you know the AMI ID, but if you want to launch a dualstack instance in a given region, you do of course have to repeat the above setup for that region, and then specify the correct subnet and security group again.

For example, suppose your default region is us-east-1, but you wish to launch an instance in sa-east-1, you'd first have to run through all the steps above to create the "dualstack" VPC in that region. Fortunately, this is a one-time thing.

But we're lazy, so let's create a simple script so we can easily run this for any region we wish to support:

$ cat > ~/bin/create-dualstack-vpc <<"EOF"
#! /bin/sh

set -eu

export VPCID="$(aws ec2 create-vpc --cidr-block 10.10.0.0/24 --amazon-provided-ipv6-cidr-block | jq -r '.Vpc.VpcId')"
aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${VPCID}"
aws ec2 modify-vpc-attribute --enable-dns-hostnames --vpc-id "${VPCID}"
aws ec2 modify-vpc-attribute --enable-dns-support --vpc-id "${VPCID}"

export IGW="$(aws ec2 create-internet-gateway | jq -r '.InternetGateway.InternetGatewayId')"
aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${IGW}"
aws ec2 attach-internet-gateway --vpc-id "${VPCID}" \
        --internet-gateway-id "${IGW}"

export V6CIDR=$(aws ec2 describe-vpcs --vpc-id "${VPCID}" | \
        jq -r '.Vpcs[].Ipv6CidrBlockAssociationSet[].Ipv6CidrBlock')
export SUBNET="$(aws ec2 create-subnet --cidr-block 10.10.0.0/26 \
        --ipv6-cidr-block ${V6CIDR%/56}/64 \
        --vpc-id "${VPCID}" | jq -r '.Subnet.SubnetId')"
aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${SUBNET}"
aws ec2 modify-subnet-attribute --subnet-id "${SUBNET}" --assign-ipv6-address-on-creation
aws ec2 modify-subnet-attribute --subnet-id "${SUBNET}" --map-public-ip-on-launch

export RTB="$(aws ec2 create-route-table --vpc-id "${VPCID}" | \
        jq -r '.RouteTable.RouteTableId')"
aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${RTB}"
aws ec2 associate-route-table --route-table-id "${RTB}" \
        --subnet-id "${SUBNET}" >/dev/null
aws ec2 create-route --route-table-id "${RTB}" --destination-ipv6-cidr-block ::/0 --gateway-id "${IGW}" >/dev/null
aws ec2 create-route --route-table-id "${RTB}" --destination-cidr-block 0.0.0.0/0 --gateway-id "${IGW}" >/dev/null

export SG="$(aws ec2 create-security-group --description "Default security group for dualstack instances" \
        --group-name "dualstack" \
        --vpc-id "${VPCID}" | jq -r '.GroupId')"
aws ec2 create-tags --tags Key=Name,Value=dualstack --resources "${SG}"
aws ec2 authorize-security-group-ingress --group-id "${SG}" \
        --ip-permissions '[{"IpProtocol": "-1", "IpRanges" : [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges" : [{"CidrIpv6": "::/0"}]}]'
EOF
$ chmod a+rx ~/bin/create-dualstack-vpc

Now the next annoying thing is that you now have different subnet- and security-group IDs in each region, so if you want to spin up a dualstack instance, you need to remember which subnet goes with which region. What a pain. Good thing we labeled our resources consistently, so that we can now on-demand search for "dualstack" subnets or security groups and create a shell function to grab the right id from that label. For example:

$ cat >> ~/.bashrc <<"EOF"

startInstance() {
        subnet=$(aws ec2 describe-subnets | jq -r '.Subnets[] | select( .Tags[]? | select(.Value == "dualstack")).SubnetId')
        sg=$(aws ec2 describe-security-groups | jq -r '.SecurityGroups [] | select( .GroupName == "dualstack").GroupId')
        aws ec2 run-instances --security-group-ids "${sg}" \
                 --subnet-id "${subnet}" \
                 --image-id $@
}
 
alias start-freebsd="startInstance ami-0de268ac2498ba33d --instance-type t2.micro"
alias start-freebsd-sa="startInstance ami-0c01daaa164ea42de --instance-type t3.micro"
EOF
$ 

With all those bits in place, we can now create dualstack FreeBSD instances in our default region as well as in sa-east-1:

$ aws configure set region sa-east-1
$ ~/bin/create-dualstack-vpc
$ start-freebsd-sa | jq -r '.Instances[].InstanceId'
i-0d88d896a110bdc7b
$ aws configure set region us-east-1
$ start-freebsd | jq -r '.Instances[].InstanceId'
i-0657d197cf9a1ac4a
$ 

And there you have it. Spinning up a dualstack EC2 instance is now somewhat easier. Perhaps in another 15 years IPv6 will finally be a first-class citizen on the internet, including AWS...

May 19th, 2020

[1] Fun Fact: HE.net tunnelbroker blocks ingress TCP port 25 on the IPv6 address by default and requires you to pass their "IPv6 Sage" certification to unblock.


See also:


[Browser Startup Comparison] [Index]