We will be setting up an OpenVPN server with dual-stack IPv6 support using Rocky Linux 9 on AWS.
Problem: Overcoming Lack of Native IPv6 Support from ISP
My current ISP only provides IPv4 connectivity, without access to IPv6 services. The goal here is to be able to access IPv6 websites using VPN, without relying on the ISP to provide IPv6.
Solution
Set up a Rocky Linux 9 OpenVPN server to accept connections over IPv4 and then provide the client with two routes:
- IPv4 traffic handled as normal through the VPN.
- IPv6 traffic routed via an AWS egress-only IPv6 gateway, giving the client a public IPv6 exit IP.
Pre-requisites
AWS account with admin rights.
Packages awscli
and jq
on a Linux client.
AWS Setup
1 Credentials
Set up your AWS CLI credentials ~/.aws/credentials
on the client machine and then export environment variables to match your setup, e.g.:
export AWS_REGION="eu-west-2" export AWS_PROFILE="01234567890_AdministratorAccess"
2 Create VPC with IPv4 CIDR
Change CIDR if required (e.g. in case of an overlap).
export VPC_CIDR="10.90.90.0/24"
aws ec2 create-vpc --cidr-block "${VPC_CIDR}" \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=dual-stack-ipv6}]' \ --query 'Vpc.VpcId' --output text
3 Assign IPv6 CIDR block to VPC
aws ec2 associate-vpc-cidr-block \ --vpc-id $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].VpcId' --output text) \ --amazon-provided-ipv6-cidr-block
4 Create subnet
aws ec2 create-subnet \ --vpc-id $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].VpcId' --output text) \ --cidr-block "${VPC_CIDR}" \ --availability-zone eu-west-2a \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=dual-stack-ipv6-subnet}]' \ --query 'Subnet.SubnetId' --output text
5 Associate IPv6 /64 to subnet
aws ec2 associate-subnet-cidr-block \ --subnet-id $(aws ec2 describe-subnets \ --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \ --query 'Subnets[0].SubnetId' --output text) \ --ipv6-cidr-block $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock' \ --output text | sed 's|/56|/64|')
6 Create Internet Gateway (IGW)
aws ec2 create-internet-gateway \ --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=dual-stack-ipv6-igw}]' \ --query 'InternetGateway.InternetGatewayId' --output text
7 Attach IGW to VPC
aws ec2 attach-internet-gateway \ --internet-gateway-id $(aws ec2 describe-internet-gateways \ --filters Name=tag:Name,Values=dual-stack-ipv6-igw \ --query 'InternetGateways[0].InternetGatewayId' --output text) \ --vpc-id $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].VpcId' --output text)
8 Create route table
aws ec2 create-route-table \ --vpc-id $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].VpcId' --output text) \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=dual-stack-ipv6-rt}]' \ --query 'RouteTable.RouteTableId' --output text
9 Associate route table with subnet
aws ec2 associate-route-table \ --route-table-id $(aws ec2 describe-route-tables \ --filters Name=tag:Name,Values=dual-stack-ipv6-rt \ --query 'RouteTables[0].RouteTableId' --output text) \ --subnet-id $(aws ec2 describe-subnets \ --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \ --query 'Subnets[0].SubnetId' --output text)
10 Add default IPv4 route to IGW
aws ec2 create-route \ --route-table-id $(aws ec2 describe-route-tables \ --filters Name=tag:Name,Values=dual-stack-ipv6-rt \ --query 'RouteTables[0].RouteTableId' --output text) \ --destination-cidr-block 0.0.0.0/0 \ --gateway-id $(aws ec2 describe-internet-gateways \ --filters Name=tag:Name,Values=dual-stack-ipv6-igw \ --query 'InternetGateways[0].InternetGatewayId' --output text)
11 Create an Egress-Only Internet Gateway (EIGW)
aws ec2 create-egress-only-internet-gateway \ --vpc-id $(aws ec2 describe-vpcs \ --filters Name=tag:Name,Values=dual-stack-ipv6 \ --query 'Vpcs[0].VpcId' --output text) \ --tag-specifications 'ResourceType=egress-only-internet-gateway,Tags=[{Key=Name,Value=dual-stack-ipv6-eigw}]' \ --query 'EgressOnlyInternetGateway.EgressOnlyInternetGatewayId' --output text
12 Add default IPv6 route to EIGW (egress-only for IPv6)
aws ec2 create-route \ --route-table-id $(aws ec2 describe-route-tables \ --filters Name=tag:Name,Values=dual-stack-ipv6-rt \ --query 'RouteTables[0].RouteTableId' --output text) \ --destination-ipv6-cidr-block ::/0 \ --egress-only-internet-gateway-id $(aws ec2 describe-egress-only-internet-gateways \ --filters Name=tag:Name,Values=dual-stack-ipv6-eigw \ --query 'EgressOnlyInternetGateways[0].EgressOnlyInternetGatewayId' --output text)
13 Create security group
aws ec2 create-security-group \ --group-name dual-stack-ipv6-sg \ --description "dual-stack-ipv6-sg" \ --vpc-id $(aws ec2 describe-vpcs --filters Name=tag:Name,Values=dual-stack-ipv6 --query 'Vpcs[0].VpcId' --output text) \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=dual-stack-ipv6-sg}]' \ --query 'GroupId' --output text
14 Ingress: SSH (IPv4 from ISP IP)
MY_IP=$(curl -s https://ip.lisenet.com)/32 aws ec2 authorize-security-group-ingress \ --group-id $(aws ec2 describe-security-groups --filters Name=group-name,Values=dual-stack-ipv6-sg --query 'SecurityGroups[0].GroupId' --output text) \ --protocol tcp --port 22 --cidr "${MY_IP}"
14 Ingress: OpenVPN (IPv4 from anywhere)
aws ec2 authorize-security-group-ingress \ --group-id $(aws ec2 describe-security-groups --filters Name=group-name,Values=dual-stack-ipv6-sg --query 'SecurityGroups[0].GroupId' --output text) \ --protocol udp --port 1194 --cidr "0.0.0.0/0"
15 Enable auto-assign public IPv4 + IPv6 on the subnet
aws ec2 modify-subnet-attribute \ --subnet-id $(aws ec2 describe-subnets \ --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \ --query 'Subnets[0].SubnetId' --output text) \ --map-public-ip-on-launch
aws ec2 modify-subnet-attribute \ --subnet-id $(aws ec2 describe-subnets \ --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \ --query 'Subnets[0].SubnetId' --output text) \ --assign-ipv6-address-on-creation
16 Create a key pair (for SSH login)
aws ec2 create-key-pair \ --key-name dual-stack-ipv6-key \ --query 'KeyMaterial' --output text > ~/dual-stack-ipv6-key.pem
chmod 0600 ~/dual-stack-ipv6-key.pem
17 Launch Rocky Linux EC2 instance
Please change AMI ID depending on your chosen region.
aws ec2 run-instances \ --image-id ami-0cfb9357729731907 \ --instance-type t3a.small \ --key-name dual-stack-ipv6-key \ --security-group-ids $(aws ec2 describe-security-groups \ --filters Name=group-name,Values=dual-stack-ipv6-sg \ --query 'SecurityGroups[0].GroupId' --output text) \ --subnet-id $(aws ec2 describe-subnets \ --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \ --query 'Subnets[0].SubnetId' --output text) \ --ipv6-address-count 1 \ --enable-primary-ipv6 \ --associate-public-ip-address \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=dual-stack-ipv6-ec2}]' \ --query 'Instances[0].InstanceId' --output text
18 Allocate Elastic IP (EIP) for EC2 instance
aws ec2 allocate-address \ --domain vpc \ --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=dual-stack-ipv6-eip}]' \ --query 'AllocationId' --output text
19 Associate EIP with EC2 instance
aws ec2 associate-address \ --instance-id $(aws ec2 describe-instances \ --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \ --query 'Reservations[0].Instances[0].InstanceId' --output text) \ --allocation-id $(aws ec2 describe-addresses \ --filters Name=tag:Name,Values=dual-stack-ipv6-eip \ --query 'Addresses[0].AllocationId' --output text)
20 Retrieve public IPv4 address of EC2 instance
aws ec2 describe-instances \ --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \ --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'
21 SSH into the EC2 instance
ssh -i ~/dual-stack-ipv6-key.pem \ rocky@$(aws ec2 describe-instances \ --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \ --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'|jq -r)
OpenVPN Server Setup
Run these commands on the VPN server when connected over SSH.
1 Install OpenVPN packages
sudo yum install epel-release sudo yum install openvpn pkcs11-helper easy-rsa
2 Configure sysctl
Add the following to /etc/sysctl.d/99-sysctl.conf
:
net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1 net.ipv6.conf.eth0.accept_ra=2 net.ipv6.conf.eth0.autoconf=1 net.ipv6.conf.all.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0 net.ipv6.conf.lo.disable_ipv6=0
Read values from updated file:
sudo sysctl -p
3 Create cloud-init patch
Create a new file /etc/cloud/cloud.cfg.d/99_ipv6_dualstack.cfg
and add the following lines:
runcmd: - | CON=$(nmcli -t -f NAME,DEVICE con show --active | awk -F: '$2=="eth0"{print $1; exit}') [ -z "$CON" ] && CON=$(nmcli -t -f NAME con show | head -n1) nmcli con modify "$CON" ipv4.method auto nmcli con modify "$CON" ipv6.method auto nmcli con up "$CON" || nmcli dev reapply eth0
4 Configure firewall (iptables)
Install iptables
:
sudo yum install iptables-services iptables sudo systemctl enable --now iptables ip6tables
Delete all existing rules:
sudo iptables -F sudo ip6tables -F
Create new rules for IPv4:
sudo iptables -A INPUT -i lo -j ACCEPT sudo iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT sudo iptables -A INPUT -p icmp -j ACCEPT sudo iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT sudo iptables -A INPUT -p udp -m state --state NEW -m udp --dport 1194 -j ACCEPT sudo iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited sudo iptables -t nat -A POSTROUTING -s 10.8.8.0/24 -j MASQUERADE
Create new rules for IPv6:
sudo ip6tables -A INPUT -i lo -j ACCEPT sudo ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT sudo ip6tables -A INPUT -p ipv6-icmp -j ACCEPT sudo ip6tables -A INPUT -d fe80::/64 -p udp -m udp --dport 546 -m state --state NEW -j ACCEPT sudo ip6tables -A INPUT -j REJECT --reject-with icmp6-adm-prohibited sudo ip6tables -A FORWARD -s fd00:10:8:8::/64 -i tun0 -o eth0 -j ACCEPT sudo ip6tables -A FORWARD -d fd00:10:8:8::/64 -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT sudo ip6tables -t nat -A POSTROUTING -s fd00:10:8:8::/64 -o eth0 -j MASQUERADE
Save the rules:
sudo service iptables save sudo service ip6tables save
5 Configure OpenVPN
cd /etc/openvpn/ sudo /usr/share/easy-rsa/3/easyrsa --batch init-pki sudo /usr/share/easy-rsa/3/easyrsa --batch build-ca nopass sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 build-server-full server nopass sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 build-client-full client nopass sudo /usr/share/easy-rsa/3/easyrsa --batch gen-dh sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 gen-crl sudo openvpn --genkey secret ./pki/ta.key
6 Create OpenVPN server config file
Create a new file /etc/openvpn/server/server.conf
and add the following lines:
proto udp port 1194 dev tun server 10.8.8.0 255.255.255.0 # A unique local address (ULA) is in the block fc00::/7. server-ipv6 fd00:10:8:8::/64 topology subnet persist-key persist-tun keepalive 10 60 max-clients 253 cipher AES-256-GCM comp-lzo no reneg-sec 0 ca /etc/openvpn/pki/ca.crt cert /etc/openvpn/pki/issued/server.crt key /etc/openvpn/pki/private/server.key dh /etc/openvpn/pki/dh.pem tls-auth /etc/openvpn/pki/ta.key 0 auth SHA512 user nobody group nobody verb 3 explicit-exit-notify 1 status /var/log/openvpn-status.log log-append /var/log/openvpn.log # Clients to use VPN as default IPv4 route. push "redirect-gateway def1 bypass-dhcp" # Give the client a default IPv6 route via the VPN, # like redirect-gateway def1 does for IPv4. push "route-ipv6 2000::/3" # IPv6 DNS for clients. push "dhcp-option DNS6 2001:4860:4860::8888" push "dhcp-option DNS6 2001:4860:4860::8844"
7 Enable and start OpenVPN server service
sudo systemctl enable --now openvpn-server@server
Check tun0
interface:
ip ad show dev tun0 3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500 link/none inet 10.8.8.1/24 scope global tun0 valid_lft forever preferred_lft forever inet6 fd00:10:8:8::1/64 scope global valid_lft forever preferred_lft forever inet6 fe80::1135:b02f:161b:c6d/64 scope link stable-privacy valid_lft forever preferred_lft forever
Check ping:
ping -4 -c3 google.com PING (142.251.29.139) 56(84) bytes of data. 64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=1 ttl=115 time=1.11 ms 64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=2 ttl=115 time=1.14 ms 64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=3 ttl=115 time=1.14 ms --- ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 1.114/1.131/1.140/0.012 ms
ping -6 -c3 google.com PING google.com(wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b)) 56 data bytes 64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=1 ttl=114 time=1.49 ms 64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=2 ttl=114 time=1.53 ms 64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=3 ttl=114 time=1.52 ms --- google.com ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2004ms rtt min/avg/max/mdev = 1.494/1.516/1.530/0.015 ms
8 Create OpenVPN client config file
On the VPN server: copy client certificates to the rocky
user home directory.
sudo cp /etc/openvpn/pki/ca.crt /home/rocky/ sudo cp /etc/openvpn/pki/ta.key /home/rocky/ sudo cp /etc/openvpn/pki/private/client.key /home/rocky/ sudo cp /etc/openvpn/pki/issued/client.crt /home/rocky/ sudo chown rocky: /home/rocky/{*crt,*key}
On the VPN client: install openvpn
package on the client machine (choose your package manager).
[client]$ sudo apt install openvpn [client]$ sudo yum install openvpn
Copy client certificates from the VPN server to the VPN client machine:
[client]$ scp -i ~/dual-stack-ipv6-key.pem \ rocky@$(aws ec2 describe-instances \ --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \ --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'|jq -r):~/{*crt,*key} ~/
Move copied certificates to /etc/openvpn/client/
directory:
[client]$ sudo mv ~/{*crt,*key} /etc/openvpn/client/
Create an OpenVPN client config file client-dual-stack-ipv6.ovpn
and add the following lines:
client remote REPLACE_WITH_VPN_SERVER_EIP proto udp port 1194 dev tun nobind comp-lzo no reneg-sec 0 remote-cert-tls server key-direction 1 auth SHA512 cipher AES-256-GCM verb 3 ca /etc/openvpn/client/ca.crt cert /etc/openvpn/client/client.crt key /etc/openvpn/client/client.key tls-auth /etc/openvpn/client/ta.key 1
9 Connect OpenVPN client to the server
[client]$ sudo openvpn --config client-dual-stack-ipv6.ovpn
Check tun0
interface:
[client]$ ip ad show dev tun0 132: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500 link/none inet 10.8.8.2/24 scope global tun0 valid_lft forever preferred_lft forever inet6 fd00:10:8:8::1000/64 scope global valid_lft forever preferred_lft forever inet6 fe80::b3c2:ab0d:2878:36f8/64 scope link stable-privacy valid_lft forever preferred_lft forever
Check ping:
[client]$ ping -4 -c3 google.com PING google.com (142.250.187.206) 56(84) bytes of data. 64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=1 ttl=114 time=14.6 ms 64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=2 ttl=114 time=14.6 ms 64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=3 ttl=114 time=15.0 ms --- google.com ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2001ms rtt min/avg/max/mdev = 14.569/14.737/15.020/0.201 ms
[client]$ ping -6 -c3 google.com PING google.com (2a00:1450:4009:c04::64) 56 data bytes 64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=1 ttl=111 time=15.7 ms 64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=2 ttl=111 time=14.7 ms 64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=3 ttl=111 time=14.4 ms --- google.com ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 14.376/14.933/15.682/0.550 ms
You should now be able to access IPv6 websites on the internet.