Creating IPv6-only VM instances and enabling NAT64/DNS64

1. Introduction

One of the main challenges with migrating to IPv6 is maintaining reachability to IPv4-only endpoints and networks. A leading technology to address this challenge is combining the use of DNS64 (defined in RFC6147) to translate A records into AAAA records for clients, this is then combined with NAT64 (defined in RFC6146) to translate specially formatted IPv6 addresses into IPv4 where the IPv4 address is embedded into the special IPv6 address. This codelab guides the user to configuring both features on a Google Cloud Platform (GCP) Virtual Private Cloud (VPC). When configured together, GCP NAT64 and DNS64 allow IPv6-only instances to communicate with IPv4-only servers on the internet.

In this lab, you will set up a VPC with different types of IPv6 subnets and instances : IPv6-only GUA (Global Unicast Address), IPv6-only ULA (Unique Local Address) and dual-stack ULA. You will then configure and test Google Cloud's managed DNS64 and NAT64 services in order to access IPv4-only websites from them.

2. What you'll learn

  • How to create IPv6-only subnets and instances
  • How to enable Google Cloud's managed DNS64 service for a VPC .
  • How to create a Google Cloud NAT gateway configured for NAT64 .
  • How to test DNS64 resolution from IPv6-only instances to IPv4-only destinations.
  • How DNS64 and NAT64 behavior differs between single-stack and dual-stack instances.
  • How to configure a NAT gateway for NAT64.
  • How to test NAT64 connectivity from IPv6-only instances to IPv4-only destinations.

3. Before you begin

Update the project to support the codelab

This Codelab makes use of $variables to aid gcloud configuration implementation in Cloud Shell.

Inside Cloud Shell, perform the following

gcloud config list project
gcloud config set project [YOUR-PROJECT-ID]
export projectname=$(gcloud config list --format="value(core.project)")
export zonename=[COMPUTE ZONE NAME]
export regionname=[REGION NAME]

Overall Lab Architecture

63e4293e033da8d3.png

To demonstrate how NAT64 and DNS64 interact with different IPv6 subnet types, you will create a single VPC with IPv6 subnets in both GUA and ULA flavors. You will also create a dual-stack subnet (using ULA addressing) to demonstrate how DNS64 and NAT64 do not apply to dual-stack VMs.

You will then configure DNS64 and NAT64 and test connectivity to IPv6 and IPv4 destinations on the internet.

4. Preparation steps

First, set up the necessary service account, IAM, network infrastructure and instances in your Google Cloud project.

Create Service Account and IAM Bindings

We start with creating a new service account to allow the instances to SSH to each other using gcloud. We will need this ability because we cannot use IAP to access the GUA IPv6-only instance and cloudshell doesn't allow direct IPv6 access yet. Run the following command(s) in cloudshell.

gcloud iam service-accounts create ipv6-codelab \
     --description="temporary service account for a codelab" \
     --display-name="ipv6codelabSA" \
     --project $projectname

gcloud projects add-iam-policy-binding  $projectname \
--member=serviceAccount:ipv6-codelab@$projectname.iam.gserviceaccount.com \
--role=roles/compute.instanceAdmin.v1

gcloud iam service-accounts add-iam-policy-binding \
    ipv6-codelab@$projectname.iam.gserviceaccount.com \
--member=serviceAccount:ipv6-codelab@$projectname.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountUser

Create a VPC and enable ULA

Create a VPC network with custom subnet mode and ULA internal IPv6 enabled by running the following command(s) in cloudshell.

gcloud compute networks create ipv6-only-vpc \
--project=$projectname \
--subnet-mode=custom \
--mtu=1500 --bgp-routing-mode=global \
--enable-ula-internal-ipv6

Create firewall rules

Create firewall rules to allow SSH access. One rule allows SSH from the overall ULA range (fd20::/20). Two more rules allow traffic from the IAP predefined IPv6 and IPv4 ranges (2600:2d00:1:7::/64, 35.235.240.0/20 respectively ).

Run the following command(s) in cloudshell:

gcloud compute firewall-rules create allow-v6-ssh-ula \
--direction=INGRESS --priority=200 \
--network=ipv6-only-vpc --action=ALLOW \
--rules=tcp:22 --source-ranges=fd20::/20 \
--project=$projectname 

gcloud compute firewall-rules create allow-v6-iap \
--direction=INGRESS --priority=300 \
--network=ipv6-only-vpc --action=ALLOW \
--rules=tcp --source-ranges=2600:2d00:1:7::/64 \
--project=$projectname 

gcloud compute firewall-rules create allow-v4-iap \
--direction=INGRESS --priority=300 \
--network=ipv6-only-vpc --action=ALLOW \
--rules=tcp --source-ranges=35.235.240.0/20 \
--project=$projectname 

Create subnets

Create a GUA v6-only subnet, a ULA v6-only subnet, and a dual-stack ULA subnet. Run the following command(s) in cloudshell:

gcloud compute networks subnets create gua-v6only-subnet \
--network=ipv6-only-vpc \
--project=$projectname \
--stack-type=IPV6_ONLY \
--ipv6-access-type=external \
--region=$regionname 

gcloud compute networks subnets create ula-v6only-subnet  \
--network=ipv6-only-vpc \
--project=$projectname \
--stack-type=IPV6_ONLY \
--ipv6-access-type=internal \
--enable-private-ip-google-access \
--region=$regionname

gcloud compute networks subnets create ula-dualstack-subnet  \
--network=ipv6-only-vpc \
--project=$projectname \
--stack-type=IPV4_IPV6 \
--range=10.120.0.0/16 \
--ipv6-access-type=internal \
--region=$regionname 

Create instances

Create instances in each of the subnets you just created. The IPv6-only ULA instance is specified with the cloud-platform to allow us to use it as a jumpbox to the GUA IPv6-only instance. Run the following command(s) in cloudshell:

gcloud compute instances create gua-instance \
--subnet gua-v6only-subnet \
--stack-type IPV6_ONLY \
--zone $zonename \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--service-account=ipv6-codelab@$projectname.iam.gserviceaccount.com \
--project=$projectname

gcloud compute instances create ula-instance \
--subnet ula-v6only-subnet \
--stack-type IPV6_ONLY \
--zone $zonename \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--service-account=ipv6-codelab@$projectname.iam.gserviceaccount.com \
--project=$projectname

gcloud compute instances create dualstack-ula-instance \
--subnet ula-dualstack-subnet \
--stack-type IPV4_IPV6 \
--zone $zonename \
--project=$projectname

Initial Instance Access and Setup

SSH to the ULA instance which will use IAP by default. Use the following command in cloudshell to SSH to the ULA instance:

gcloud compute ssh ula-instance --project $projectname --zone $zonename

<username>@ula-instance:~$ 

We will also use the ULA instance as a jumpbox for the GUA instance (because IAP doesn't work with GUA instances and Cloudshell VMs cannot access IPv6 destinations).

While still inside the ULA instance shell. Attempt to SSH to the GUA instance using the following gcloud command.

The first time you run an SSH command inside an instance it will prompt you to setup an SSH keypair. Keep pressing enter until the key is created an the SSH command is executed; this creates a new keypair with no passphrase.

ula-instance:~$ gcloud compute ssh gua-instance

WARNING: The private SSH key file for gcloud does not exist.
WARNING: The public SSH key file for gcloud does not exist.
WARNING: You do not have an SSH key for gcloud.
WARNING: SSH keygen will be executed to generate a key.
Generating public/private rsa key pair.

Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/galhabian/.ssh/google_compute_engine
Your public key has been saved in /home/galhabian/.ssh/google_compute_engine.pub
The key fingerprint is:
SHA256:5PYzydjcpWYiFtzetYCBI6vmy9dqyLsxgDORkB9ynqY galhabian@ula-instance
The key's randomart image is:
+---[RSA 3072]----+
|..               |
|+.o      .       |
|o= o  . + .      |
| o=    * o o     |
|+o.   . S o . o  |
|Eo . . . O + = . |
|   .=. .+ @ * .  |
|   +ooo... *     |
|    **..         |
+----[SHA256]-----+

If successful, the SSH command will be successful and you will have SSH'd successfully to the GUA instance:

Updating instance ssh metadata...done.                                                                                                                                                                                                                                                                                            
Waiting for SSH key to propagate.
Warning: Permanently added 'compute.3639038240056074485' (ED25519) to the list of known hosts.
Linux gua-instance 6.1.0-34-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.135-1 (2025-04-25) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

<username>@gua-instance:~$ 

5. Examine IPv6-only instances.

Let's examine both IPv6-only instances by SSH'ing to them and examining their routing tables.

Examine GUA instance

SSH into the "gua-instance" by jumping through the "ula-instance" instance first.

gcloud compute ssh ula-instance --project $projectname --zone $zonename
<username>@ula-instance:~$ gcloud compute ssh gua-instance

Let's look at the instance's IPv6 routing table using the following command

<username>@gua-instance:~$ ip -6 route

2600:1900:4041:461::/65 via fe80::56:11ff:fef9:88c1 dev ens4 proto ra metric 100 expires 81sec pref medium
fe80::/64 dev ens4 proto kernel metric 256 pref medium
default via fe80::56:11ff:fef9:88c1 dev ens4 proto ra metric 100 expires 81sec mtu 1500 pref medium

We notice three entries in the routing table

  1. A /65 route for the GUA subnet the instance belongs to with a derived next-hop using a link-local address for the default gateway. Remember that the upper /65 is reserved for IPv6 Pass-Through Network Load Balancers
  2. A built-in /64 route for the link-local unicast prefix fe80::/64
  3. A default route pointing at the link-local address for the subnet's default gateway.

Let's look at the IPv4 routing table by issuing this command

<username>@gua-instance:~$ ip -4 route

default via 169.254.1.1 dev ens4 proto dhcp src 169.254.1.2 metric 100 
169.254.1.1 dev ens4 proto dhcp scope link src 169.254.1.2 metric 100 
169.254.169.254 via 169.254.1.1 dev ens4 proto dhcp src 169.254.1.2 metric 100

Surprising? In fact we do maintain an IPv4 routing table in IPv6-only instances strictly to allow access to the Compute Metadata server (169.254.169.154) since it's still an IPv4-only endpoint.

Since the instance takes on the IP 169.254.1.2 when it's an IPv6-only instance. This IP is not routable anywhere but to the Compute Metadata server, so the instance is effectively isolated from all IPv4 networks.

Curl Tests

Let's test actual connectivity to v4-only and v6-only websites using curl.

<username>@gua-instance:~$ curl -vv --connect-timeout 10 v6.ipv6test.app
<username>@gua-instance:~$ curl -vv --connect-timeout 10 v4.ipv6test.app

The below is a sample output.

<username>@gua-instance:~$ curl -vv --connect-timeout 10 v6.ipv6test.app
*   Trying [2600:9000:20be:cc00:9:ec55:a1c0:93a1]:80...
* Connected to v6.ipv6test.app (2600:9000:20be:cc00:9:ec55:a1c0:93a1) port 80 (#0)
> GET / HTTP/1.1
> Host: v6.ipv6test.app
> User-Agent: curl/7.88.1
> Accept: */*
> 
< HTTP/1.1 200 OK
!! Rest of output truncated

<username>@gua-instance:~$ curl -vv --connect-timeout 10 v4.ipv6test.app
*   Trying 3.163.165.4:80...
* ipv4 connect timeout after 4985ms, move on!
*   Trying 3.163.165.50:80...
* ipv4 connect timeout after 2492ms, move on!
*   Trying 3.163.165.127:80...
* ipv4 connect timeout after 1246ms, move on!
*   Trying 3.163.165.37:80...
* ipv4 connect timeout after 1245ms, move on!
* Failed to connect to v4.ipv6test.app port 80 after 10000 ms: Timeout was reached
* Closing connection 0
curl: (28) Failed to connect to v4.ipv6test.app port 80 after 10000 ms: Timeout was reached

As expected, there is no reachability to an IPv4 internet endpoint from an IPv6-only instance. Without provisioning DNS64 and NAT64 the IPv6-only instance has no path to an IPv4 destination. Reachability to an IPv6 destination works normally as the instance has a GUA IPv6 address.

Examine ULA instance

SSH into the "ula-instance" instance (uses IAP by default).

gcloud compute ssh ula-instance --project $projectname --zone $zonename

Let's look at the instance's IPv6 routing table using the following command

<username>@ula-instance:~$ ip -6 route

fd20:f06:2e5e:2000::/64 via fe80::55:82ff:fe6b:1d7 dev ens4 proto ra metric 100 expires 84sec pref medium
fe80::/64 dev ens4 proto kernel metric 256 pref medium
default via fe80::55:82ff:fe6b:1d7 dev ens4 proto ra metric 100 expires 84sec mtu 1500 pref medium

We notice three entries in the routing table, similar to the GUA instance, with the exception of the mask being /64 instead of /65. And the subnet route belongs to a ULA range. (under the fd20::/20 aggregate)

Let's look at the IPv4 routing table by issuing this command

<username>@ula-instance:~$ ip -4 route

default via 169.254.1.1 dev ens4 proto dhcp src 169.254.1.2 metric 100 
169.254.1.1 dev ens4 proto dhcp scope link src 169.254.1.2 metric 100 
169.254.169.254 via 169.254.1.1 dev ens4 proto dhcp src 169.254.1.2 metric 100

Which shows a similar situation to the GUA instance.

Curl Tests

Repeating the connectivity tests to v4-only and v6-only websites using curl.

<username>@ula-instance:~$ curl -vv --connect-timeout 10 v6.ipv6test.app
<username>@ula-instance:~$ curl -vv --connect-timeout 10 v4.ipv6test.app

The below is a sample output.

<username>@ula-instance:~$ curl -vv --connect-timeout 10 v6.ipv6test.app
*   Trying [2600:9000:20be:8400:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 4986ms, move on!
*   Trying [2600:9000:20be:9000:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 2493ms, move on!
*   Trying [2600:9000:20be:d600:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 1246ms, move on!
*   Trying [2600:9000:20be:b000:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 622ms, move on!
*   Trying [2600:9000:20be:7200:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 312ms, move on!
*   Trying [2600:9000:20be:8600:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 155ms, move on!
*   Trying [2600:9000:20be:7a00:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 77ms, move on!
*   Trying [2600:9000:20be:ce00:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 77ms, move on!
* Failed to connect to v6.ipv6test.app port 80 after 10000 ms: Timeout was reached
* Closing connection 0

<username>@ula-instance:~$ curl -vv --connect-timeout 10 v4.ipv6test.app
*   Trying 3.163.165.4:80...
* ipv4 connect timeout after 4985ms, move on!
*   Trying 3.163.165.50:80...
* ipv4 connect timeout after 2492ms, move on!
*   Trying 3.163.165.127:80...
* ipv4 connect timeout after 1246ms, move on!
*   Trying 3.163.165.37:80...
* ipv4 connect timeout after 1245ms, move on!
* Failed to connect to v4.ipv6test.app port 80 after 10000 ms: Timeout was reached
* Closing connection 0
curl: (28) Failed to connect to v4.ipv6test.app port 80 after 10000 ms: Timeout was reached

In the ULA instance case, there is no reachability to both internet endpoints since for the IPv6 endpoint we cannot use a ULA address to communicate out, and the instance has no reachability to IPv4 as an IPv6-only instance.

6. Enable NAT64 and DNS64

Configure the managed DNS64 and NAT64 services for your VPC.

DNS64

Enable the DNS64 Server policy for your VPC . This tells the VPC's DNS resolver to synthesize AAAA records for A-only responses. Run the following command(s) in cloudshell:

gcloud beta dns policies create allow-dns64 \
    --description="Enable DNS64 Policy" \
    --networks=ipv6-only-vpc \
    --enable-dns64-all-queries \
    --project $projectname

NAT64

Create a Cloud Router, which is required for Cloud NAT . Then, create a Cloud NAT gateway configured for NAT64, enabling it for all IPv6-only subnet IP ranges and auto-allocating external IPs. Run the following command(s) in cloudshell:

gcloud compute routers create nat64-router \
--network=ipv6-only-vpc \
--region=$regionname \
--project=$projectname


gcloud beta compute routers nats create nat64-natgw \
--router=nat64-router \
--region=$regionname \
--auto-allocate-nat-external-ips \
--nat64-all-v6-subnet-ip-ranges \
--project=$projectname
 

7. Test NAT64 and DNS64

Now, let's test your NAT64 and DNS64 configuration from the IPv6-only instances, starting with the GUA instance followed by the ULA instance.

Testing DNS64/NAT64 from a GUA instance

SSH into the "gua-instance" by jumping through the "ula-instance" instance first.

gcloud compute ssh ula-instance --project $projectname --zone $zonename
<username>@ula-instance:~$ gcloud compute ssh gua-instance

DNS Tests

Test DNS resolution of an IPv6-only website (e.g., v6.ipv6test.app but any IPv6-only website should yield a similar result).

<username>@gua-instance:~$ host -t AAAA v6.ipv6test.app
<username>@gua-instance:~$ host -t A v6.ipv6test.app

We expect only IPv6 AAAA answers to be returned.

Example output

<username>@gua-instance:~$ host -t AAAA v6.ipv6test.app
v6.ipv6test.app has IPv6 address 2600:9000:269f:1000:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:6600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:b600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:3e00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:9c00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:b200:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:a600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:1400:9:ec55:a1c0:93a1

<username>@gua-instance:~$ host -t A v6.ipv6test.app
v6.ipv6test.app has no A record

Test DNS resolution of an IPv4-only website (e.g., v4.ipv6test.app) . You expect both an A record (the original IPv4) and an AAAA record synthesized by DNS64 using the well-known prefix 64:ff9b::/96 .

<username>@gua-instance:~$ host -t AAAA v4.ipv6test.app
<username>@gua-instance:~$ host -t A v4.ipv6test.app

Example output

<username>@gua-instance:~$ host -t AAAA v4.ipv6test.app
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3318
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3344
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:333c
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3326

<username>@gua-instance:~$ host -t A v4.ipv6test.app
v4.ipv6test.app has address 54.192.51.68
v4.ipv6test.app has address 54.192.51.24
v4.ipv6test.app has address 54.192.51.60
v4.ipv6test.app has address 54.192.51.38

In the example above, the IPv4 address (54.192.51.38) in decimal would translate to (36 c0 33 26) in hex and hence we would expect an answer for the AAAA record to be (64:ff9b::36c0:3326) which matches one of the AAAA answers we received.

Curl Tests

Let's test actual connectivity to the same v4-only and v6-only endpoints using curl over IPv6

<username>@gua-instance:~$ curl -vv -6 v6.ipv6test.app

<username>@gua-instance:~$ curl -vv -6 v4.ipv6test.app

The below is a sample output.

<username>@gua-instance:~$ curl -vv -6 v6.ipv6test.app
*   Trying [2600:9000:269f:1000:9:ec55:a1c0:93a1]:80...
* Connected to v6.ipv6test.app (2600:9000:269f:1000:9:ec55:a1c0:93a1) port 80 (#0)
> GET / HTTP/1.1

##
## <Output truncated for brevity>
##

<username>@gua-instance:~$ curl -vv -6 v4.ipv6test.app
*   Trying [64:ff9b::36c0:333c]:80...
* Connected to v4.ipv6test.app (64:ff9b::36c0:333c) port 80 (#0)
> GET / HTTP/1.1

##
## <Output truncated for brevity>
##

Both curl commands succeed. Notice how connecting to an IPv4-only website over IPv6 was possible due to NAT64 and DNS64 working in tandem to enable connectivity successfully.

Check Source IPs

Let's use an IP reflection service to check the source IP observed by the destination .

<username>@gua-instance:~$ curl -6 v4.ipv6test.app

<username>@gua-instance:~$ curl -6 v6.ipv6test.app

Sample output

<username>@gua-instance:~$ curl -6 v4.ipv6test.app
34.47.60.91

<username>@gua-instance:~$ curl -6 v6.ipv6test.app
2600:1900:40e0:6f:0:1::

The reported IPv6 address should match the instance's IPv6 address . This address should match the output of "ip -6 address" command on the instance. As an example

<username>@gua-instance:~$ ip -6 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2600:1900:40e0:6f:0:1::/128 scope global dynamic noprefixroute
       valid_lft 79912sec preferred_lft 79912sec
    inet6 fe80::86:d9ff:fe34:27ed/64 scope link
       valid_lft forever preferred_lft forever

The reported IPv4 address however should match the Cloud NAT gateway's external IP address since it's performing the NAT64 function before egressing to the internet. This can be verified by running this gcloud command in cloudshell

gcloud compute routers get-nat-ip-info \
       nat64-router \
       --region=$regionname

Sample output

result:
- natIpInfoMappings:
  - mode: AUTO
    natIp: 34.47.60.91
    usage: IN_USE
  natName: nat64-natgw

Note that the "natIp" reported in the output matches the output received from the IP reflection website.

Testing DNS64/NAT64 from a ULA instance

First, SSH to the ULA instance "ula-instance"

gcloud compute ssh ula-instance --project $projectname --zone $zonename

<username>@ula-instance:~$

DNS Tests

Test DNS resolution of an IPv6-only website (e.g., v6.ipv6test.app but any IPv6-only website should yield a similar result).

<username>@ula-instance:~$ host -t AAAA v6.ipv6test.app
<username>@ula-instance:~$ host -t A v6.ipv6test.app

We expect only IPv6 AAAA answers to be returned.

Example output

<username>@ula-instance:~$ host -t AAAA v6.ipv6test.app
v6.ipv6test.app has IPv6 address 2600:9000:269f:1000:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:6600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:b600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:3e00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:9c00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:b200:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:a600:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:1400:9:ec55:a1c0:93a1

<username>@ula-instance:~$ host -t A v6.ipv6test.app
v6.ipv6test.app has no A record

Test DNS resolution of an IPv4-only website (e.g., v4.ipv6test.app) . You expect both an A record (the original IPv4) and an AAAA record synthesized by DNS64 using the well-known prefix 64:ff9b::/96 .

<username>@ula-instance:~$ host -t AAAA v4.ipv6test.app
<username>@ula-instance:~$ host -t A v4.ipv6test.app

Example output

<username>@gua-instance:~$ host -t AAAA v4.ipv6test.app
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3318
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3344
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:333c
v4.ipv6test.app has IPv6 address 64:ff9b::36c0:3326

<username>@gua-instance:~$ host -t A v4.ipv6test.app
v4.ipv6test.app has address 54.192.51.68
v4.ipv6test.app has address 54.192.51.24
v4.ipv6test.app has address 54.192.51.60
v4.ipv6test.app has address 54.192.51.38

In the example above, the IPv4 address (54.192.51.38) in decimal would translate to (36 c0 33 26) in hex and hence we would expect an answer for the AAAA record to be (64:ff9b::36c0:3326) which matches one of the AAAA answers we received.

Curl Tests

Let's test actual connectivity to the same v4-only and v6-only endpoints using curl.

As a starting point let's show that reachability over IPv4 is not possible since the instance is an IPv6-only instance.

<username>@ula-instance:~$ curl -vv -4 --connect-timeout 10 v6.ipv6test.app
<username>@ula-instance:~$ curl -vv -4 --connect-timeout 10 v4.ipv6test.app

While both curls will fail. They will fail for different reasons. The below is a sample output.

<username>@ula-instance:~$ curl -vv -4 v6.ipv6test.app
* Could not resolve host: v6.ipv6test.app
* Closing connection 0
curl: (6) Could not resolve host: v6.ipv6test.app

<username>@ula-instance:~$ curl -vv -4 --connect-timeout 10 v4.ipv6test.app
*   Trying 54.192.51.68:80...
* ipv4 connect timeout after 4993ms, move on!
*   Trying 54.192.51.38:80...
* ipv4 connect timeout after 2496ms, move on!
*   Trying 54.192.51.24:80...
* ipv4 connect timeout after 1248ms, move on!
*   Trying 54.192.51.60:80...
* Connection timeout after 10000 ms
* Closing connection 0
curl: (28) Connection timeout after 10000 ms

The IPv4 curl to an IPv6-only endpoint fails because DNS resolution for the A record fails (as demonstrated during the DNS tests). The IPv4 curl to an IPv4-only endpoint fails because an IPv6-only instance doesn't have any reachability to any IPv4 address and we get a timeout because of that.

Now let's test reachability over IPv6.

<username>@ula-instance:~$ curl -vv -6 v6.ipv6test.app

<username>@ula-instance:~$ curl -vv -6 v4.ipv6test.app

The below is a sample output.

<username>@ula-instance:~$ curl -vv -6 v6.ipv6test.app
*   Trying [2600:9000:20be:c000:9:ec55:a1c0:93a1]:80...
* connect to 2600:9000:20be:c000:9:ec55:a1c0:93a1 port 80 failed: Connection timed out
*   Trying [2600:9000:20be:f000:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 84507ms, move on!
*   Trying [2600:9000:20be:ae00:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 42253ms, move on!
*   Trying [2600:9000:20be:2000:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 21126ms, move on!
*   Trying [2600:9000:20be:b600:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 10563ms, move on!
*   Trying [2600:9000:20be:7600:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 5282ms, move on!
*   Trying [2600:9000:20be:b000:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 2640ms, move on!
*   Trying [2600:9000:20be:3400:9:ec55:a1c0:93a1]:80...
* ipv6 connect timeout after 2642ms, move on!
* Failed to connect to v6.ipv6test.app port 80 after 300361 ms: Timeout was reached
* Closing connection 0

<username>@ula-instance:~$ curl -vv -6 v4.ipv6test.app
*   Trying [64:ff9b::36c0:333c]:80...
* Connected to v4.ipv6test.app (64:ff9b::36c0:333c) port 80 (#0)
> GET / HTTP/1.1

##
## <Output truncated for brevity>
##

While the curl to the IPv6-only website fails because ULA subnets don't have direct reachability to the internet. The curl to the IPv4-only website succeeds because DNS64 and NAT64 operate the same way for GUA and ULA instances; the only requirement is for the instance to be IPv6-only.

Testing DNS64/NAT64 from a Dual-Stack ULA instance

First, SSH to the Dual-Stack ULA instance "dualstack-ula-instance". We need to use the "–tunnel-through-iap" flag to force gcloud to use the IPv4 address for IAP.

gcloud compute ssh dualstack-ula-instance --project $projectname --zone $zonename --tunnel-through-iap 

<username>@dualstack-ula-instance:~$

Let's test DNS64 now using the "host" utility.

<username>@dualstack-ula-instance:~$ host v4.ipv6test.app
v4.ipv6test.app has address 54.192.51.38
v4.ipv6test.app has address 54.192.51.24
v4.ipv6test.app has address 54.192.51.68
v4.ipv6test.app has address 54.192.51.60

<username>@dualstack-ula-instance:~$ host v6.ipv6test.app
v6.ipv6test.app has IPv6 address 2600:9000:269f:fc00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:1c00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:a200:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:8a00:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:c800:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:c200:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:5800:9:ec55:a1c0:93a1
v6.ipv6test.app has IPv6 address 2600:9000:269f:dc00:9:ec55:a1c0:93a1

Notice how the IPv4-only website is returning only IPv4 addresses now and not the synthetic DNS64 answers anymore. That is because DNS64 is only applied to IPv6-only instances and is not evaluated for dual-stack instances.

To bypass the need for DNS64 let's add an entry to the /etc/hosts file to test if NAT64 works. Run the following command inside the dual-stack instance:

<username>@dualstack-ula-instance:~$ echo '64:ff9b::36c0:3326 v4.ipv6test.app' | sudo tee -a /etc/hosts

Then let's use curl to test reaching the ipv4 website over IPv6

<username>@dualstack-ula-instance:~$ curl -vv -6 --connect-timeout 10 v4.ipv6test.app

Here's a sample output from the above command

<username>@dualstack-ula-instance:~$ curl -vv -6 --connect-timeout 10 v4.ipv6test.app

*   Trying [64:ff9b::36c0:3326]:80...
* ipv6 connect timeout after 10000ms, move on!
* Failed to connect to v4.ipv6test.app port 80 after 10001 ms: Timeout was reached
* Closing connection 0
curl: (28) Failed to connect to v4.ipv6test.app port 80 after 10001 ms: Timeout was reached

The curl should timeout because just like DNS64, NAT64 also requires that the instance be IPv6-only to apply.

To confirm that NAT64 isn't actually applying to the dual-stack instance, let's use the "get-nat-mapping" command to list all port mappings that the NAT gateway is applying. Run the following command(s) in cloudshell:

gcloud compute routers get-nat-mapping-info \
      nat64-router --region $regionname \
      --project $projectname

You should expect an output similar to the below snippet:

---
instanceName: gua-instance
interfaceNatMappings:
- natIpPortRanges:
  - 34.47.60.91:1024-1055
  numTotalDrainNatPorts: 0
  numTotalNatPorts: 32
  sourceAliasIpRange: ''
  sourceVirtualIp: '2600:1900:40e0:6f:0:1::'
- natIpPortRanges:
  - 34.47.60.91:32768-32799
  numTotalDrainNatPorts: 0
  numTotalNatPorts: 32
  sourceAliasIpRange: ''
  sourceVirtualIp: '2600:1900:40e0:6f:0:1::'
---
instanceName: ula-instance
interfaceNatMappings:
- natIpPortRanges:
  - 34.47.60.91:1056-1087
  numTotalDrainNatPorts: 0
  numTotalNatPorts: 32
  sourceAliasIpRange: ''
  sourceVirtualIp: fd20:9c2:93fc:2800:0:0:0:0
- natIpPortRanges:
  - 34.47.60.91:32800-32831
  numTotalDrainNatPorts: 0
  numTotalNatPorts: 32
  sourceAliasIpRange: ''
  sourceVirtualIp: fd20:9c2:93fc:2800:0:0:0:0

The NAT output shows that the NAT64 gateway only allocated ports for the IPv6-only GUA and ULA instances but not the dual-stack instance.

8. Clean up

Clean up Cloud Router

Inside Cloud Shell, perform the following:

gcloud compute routers delete nat64-router \
      --region $regionname \
      --project $projectname --quiet

Unbind and Clean up DNS Policy

Inside Cloud Shell, perform the following:

gcloud beta dns policies update allow-dns64 \
    --networks="" \
    --project $projectname
gcloud beta dns policies delete allow-dns64 \
    --project $projectname --quiet

Clean up Instances

Inside Cloud Shell, perform the following: (note, gcloud beta is used to enable us to use the no-graceful-shutdown flag for a faster instance delete operation)

gcloud beta compute instances delete gua-instance \
         --zone $zonename \
         --no-graceful-shutdown \
         --project=$projectname --quiet

gcloud beta compute instances delete ula-instance \
         --zone $zonename \
         --no-graceful-shutdown \
         --project=$projectname --quiet

gcloud beta compute instances delete dualstack-ula-instance \
         --zone $zonename \
         --no-graceful-shutdown \
         --project=$projectname --quiet

Clean up subnets

Inside Cloud Shell, perform the following:

gcloud compute networks subnets delete gua-v6only-subnet \
    --project=$projectname --quiet \
    --region=$regionname
 
gcloud compute networks subnets delete ula-v6only-subnet \
    --project=$projectname --quiet \
    --region=$regionname

gcloud compute networks subnets delete ula-dualstack-subnet \
    --project=$projectname --quiet \
    --region=$regionname

Clean up firewall rules

Inside Cloud Shell, perform the following:

gcloud compute firewall-rules delete allow-v6-iap \
       --project=$projectname \
       --quiet

gcloud compute firewall-rules delete allow-v6-ssh-ula \
       --project=$projectname \
       --quiet

gcloud compute firewall-rules delete allow-v4-iap \
--project=$projectname \
--quiet

Clean up VPC

Inside Cloud Shell, perform the following:

gcloud compute networks delete ipv6-only-vpc \
       --project=$projectname \
       --quiet

Clean up IAM permissions and Service Account

Inside Cloud Shell, perform the following:

gcloud projects remove-iam-policy-binding  $projectname \
--member=serviceAccount:ipv6-codelab@$projectname.iam.gserviceaccount.com \
--role=roles/compute.instanceAdmin.v1

gcloud iam service-accounts delete \
     ipv6-codelab@$projectname.iam.gserviceaccount.com \
     --quiet \
     --project $projectname

9. Congratulations

You have successfully used NAT64 and DNS64 to allow IPv6-only instances to reach IPv4-only destinations on the internet.

What's next?

Check out some of these codelabs...

Further reading & Videos

Reference docs