#!/usr/bin/env python3 import boto3 import atexit import paramiko import io import time import threading import re import json import base64 import sys from datetime import date # Note: this script will run for 15 minutes and incur costs of about $ 0.03 REGION = "eu-central-1" AMI = "ami-0a02ee601d742e89f" USERNAME = "ec2-user" INSTANCE_TYPE = "m5.large" SPOT = True MAX_PRICE = "0.08" # USD per hour per VM VERSION = "1.4.0" USERDATA = """#cloud-config packages: - iperf3 runcmd: - wget https://github.com/dswd/vpncloud/releases/download/v{version}/vpncloud_{version}.x86_64.rpm -O /tmp/vpncloud.rpm - yum install -y /tmp/vpncloud.rpm """.format(version=VERSION) MAX_WAIT = 300 def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def run_cmd(connection, cmd): _stdin, stdout, stderr = connection.exec_command(cmd) out = stdout.read().decode('utf-8') err = stderr.read().decode('utf-8') code = stdout.channel.recv_exit_status() if code: raise Exception("Command failed", code, out, err) else: return out, err class EC2Environment: def __init__(self): self.vpc = None self.igw = None self.rtb = None self.subnet = None self.sg = None self.key_pair = None self.rsa_key = None self.placement_group = None self.sender = None self.receiver = None self.sender_request = None self.receiver_request = None self.sender_ssh = None self.receiver_ssh = None try: self.setup() self.wait_until_ready() eprint("Setup done") except: eprint("Error, shutting down") self.terminate() raise def setup(self): ec2 = boto3.resource('ec2', region_name=REGION) ec2client = boto3.client('ec2', region_name=REGION) self.vpc = ec2.create_vpc(CidrBlock='172.16.0.0/16') eprint("Created VPC {}".format(self.vpc.id)) self.vpc.create_tags(Tags=[{"Key": "Name", "Value": "vpncloud-perf-test"}]) self.vpc.wait_until_available() ec2client.modify_vpc_attribute(VpcId=self.vpc.id, EnableDnsSupport={'Value': True}) ec2client.modify_vpc_attribute(VpcId=self.vpc.id, EnableDnsHostnames={'Value': True}) self.igw = ec2.create_internet_gateway() eprint("Created Internet Gateway {}".format(self.igw.id)) self.igw.attach_to_vpc(VpcId=self.vpc.id) self.rtb = self.vpc.create_route_table() eprint("Created Routing table {}".format(self.rtb.id)) self.rtb.create_route(DestinationCidrBlock='0.0.0.0/0', GatewayId=self.igw.id) self.subnet = ec2.create_subnet(CidrBlock='172.16.1.0/24', VpcId=self.vpc.id) eprint("Created Subnet {}".format(self.subnet.id)) self.rtb.associate_with_subnet(SubnetId=self.subnet.id) self.sg = ec2.create_security_group(GroupName='SSH-ONLY', Description='only allow SSH traffic', VpcId=self.vpc.id) eprint("Created security group {}".format(self.sg.id)) self.sg.authorize_ingress(CidrIp='0.0.0.0/0', IpProtocol='tcp', FromPort=22, ToPort=22) self.sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='icmp', FromPort=-1, ToPort=-1) self.sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='tcp', FromPort=0, ToPort=65535) self.sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='udp', FromPort=0, ToPort=65535) self.key_pair = ec2.create_key_pair(KeyName='vpncloud-perf-test-keypair') eprint("Created key pair {}".format(self.key_pair.name)) self.rsa_key = paramiko.RSAKey.from_private_key(io.StringIO(self.key_pair.key_material)) self.placement_group = ec2.create_placement_group(GroupName="vpncloud-test-placement", Strategy="cluster") eprint("Created placement group {}".format(self.placement_group.name)) if SPOT: response = ec2client.request_spot_instances( SpotPrice = MAX_PRICE, Type = "one-time", InstanceCount = 2, LaunchSpecification = { "ImageId": AMI, "InstanceType": INSTANCE_TYPE, "KeyName": self.key_pair.name, "UserData": base64.b64encode(USERDATA.encode("ascii")).decode('ascii'), "BlockDeviceMappings": [ { "DeviceName": "/dev/xvda", "Ebs": { "DeleteOnTermination": True, "VolumeType": "gp2", "VolumeSize": 8, } } ], "NetworkInterfaces": [ { 'SubnetId': self.subnet.id, 'DeviceIndex': 0, 'AssociatePublicIpAddress': True, 'Groups': [self.sg.group_id] } ], "Placement": { 'GroupName': self.placement_group.name } } ) sender, receiver = response['SpotInstanceRequests'] self.sender_request = sender['SpotInstanceRequestId'] self.receiver_request = receiver['SpotInstanceRequestId'] eprint("Created spot instance requests {} and {}".format(self.sender_request, self.receiver_request)) eprint("Waiting for spot instance requests") waited = 0 while waited < MAX_WAIT: time.sleep(1.0) response = ec2client.describe_spot_instance_requests(SpotInstanceRequestIds=[self.sender_request]) sender = response['SpotInstanceRequests'][0] response = ec2client.describe_spot_instance_requests(SpotInstanceRequestIds=[self.receiver_request]) receiver = response['SpotInstanceRequests'][0] if 'InstanceId' in sender: self.sender = ec2.Instance(sender['InstanceId']) if 'InstanceId' in receiver: self.receiver = ec2.Instance(receiver['InstanceId']) if self.sender and self.receiver: break if waited >= MAX_WAIT: raise Exception("Waited too long") else: self.sender, self.receiver = ec2.create_instances( ImageId=AMI, InstanceType=INSTANCE_TYPE, MaxCount=2, MinCount=2, NetworkInterfaces=[ { 'SubnetId': self.subnet.id, 'DeviceIndex': 0, 'AssociatePublicIpAddress': True, 'Groups': [self.sg.group_id] } ], Placement={ 'GroupName': self.placement_group.name }, UserData=USERDATA, KeyName='vpncloud-perf-test-keypair' ) eprint("Created EC2 instances {} and {}".format(self.sender.id, self.receiver.id)) eprint("Waiting for instances to start...") self.sender.wait_until_running() self.receiver.wait_until_running() self.sender.reload() self.receiver.reload() def wait_until_ready(self): waited = 0 eprint("Waiting for SSH to be ready...") while waited < MAX_WAIT: try: if not self.sender_ssh: self.sender_ssh = self._connect(self.sender) if not self.receiver_ssh: self.receiver_ssh = self._connect(self.receiver) break except: pass time.sleep(1.0) waited += 1 eprint("Waiting for instances to finish setup...") while waited < MAX_WAIT: try: run_cmd(self.sender_ssh, 'test -f /var/lib/cloud/instance/boot-finished') run_cmd(self.receiver_ssh, 'test -f /var/lib/cloud/instance/boot-finished') break except: pass time.sleep(1.0) waited += 1 if waited >= MAX_WAIT: raise Exception("Waited too long") def terminate(self): eprint("Deleting resources...") if self.sender_ssh: self.sender_ssh.close() if self.receiver_ssh: self.receiver_ssh.close() if self.sender: eprint(self.sender.id) self.sender.terminate() if self.receiver: eprint(self.receiver.id) self.receiver.terminate() if self.sender: self.sender.wait_until_terminated() if self.receiver: self.receiver.wait_until_terminated() if self.sender_request or self.receiver_request: ec2client = boto3.client('ec2', region_name=REGION) if self.sender_request: eprint(self.sender_request) ec2client.cancel_spot_instance_requests(SpotInstanceRequestIds=[self.sender_request]) if self.receiver_request: eprint(self.receiver_request) ec2client.cancel_spot_instance_requests(SpotInstanceRequestIds=[self.receiver_request]) if self.placement_group: self.placement_group.delete() if self.key_pair: eprint(self.key_pair.name) self.key_pair.delete() if self.sg: eprint(self.sg.id) self.sg.delete() if self.subnet: eprint(self.subnet.id) self.subnet.delete() if self.rtb: eprint(self.rtb.id) self.rtb.delete() if self.igw: eprint(self.igw.id) self.igw.detach_from_vpc(VpcId=self.vpc.id) self.igw.delete() if self.vpc: eprint(self.vpc.id) self.vpc.delete() def _connect(self, instance): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname=instance.public_dns_name, username=USERNAME, pkey=self.rsa_key, timeout=1.0, banner_timeout=1.0) return client class PerfTest: def __init__(self, sender_ssh, sender_ip, receiver_ssh, receiver_ip): self.sender_ssh = sender_ssh self.sender_ip = sender_ip self.receiver_ssh = receiver_ssh self.receiver_ip = receiver_ip self.sender_ip_vpncloud = "10.0.0.1" self.receiver_ip_vpncloud = "10.0.0.2" @classmethod def from_ec2_env(cls, env): return cls(env.sender_ssh, env.sender.private_ip_address, env.receiver_ssh, env.receiver.private_ip_address) def run_sender(self, cmd): return run_cmd(self.sender_ssh, cmd) def run_receiver(self, cmd): return run_cmd(self.receiver_ssh, cmd) def run_ping(self, dst, size): eprint("Running ping {} with size {} ...".format(dst, size)) (out, _) = self.run_sender('sudo ping {dst} -c 30000 -i 0.001 -s {size} -U -q'.format(dst=dst, size=size)) match = re.search(r'([\d]*\.[\d]*)/([\d]*\.[\d]*)/([\d]*\.[\d]*)/([\d]*\.[\d]*)', out) ping_min = float(match.group(1)) ping_avg = float(match.group(2)) ping_max = float(match.group(3)) match = re.search(r'(\d*)% packet loss', out) pkt_loss = float(match.group(1)) return { "rtt_min": ping_min, "rtt_max": ping_max, "rtt_avg": ping_avg, "pkt_loss": pkt_loss } def run_iperf(self, dst): eprint("Running iperf on {} ...".format(dst)) self.run_receiver('iperf3 -s -D') time.sleep(0.1) (out, _) = self.run_sender('iperf3 -c {dst} -t 30 --json'.format(dst=dst)) self.run_receiver('killall iperf3') data = json.loads(out) return { "throughput": data['end']['streams'][0]['receiver']['bits_per_second'], "cpu_sender": data['end']['cpu_utilization_percent']['host_total'], "cpu_receiver": data['end']['cpu_utilization_percent']['remote_total'] } def run_suite(self, dst): return { "iperf": self.run_iperf(dst), "ping_100": self.run_ping(dst, 100), "ping_500": self.run_ping(dst, 500), "ping_1000": self.run_ping(dst, 1000), } def start_vpncloud(self, mtu=1400, crypto=None): eprint("Setting up vpncloud on receiver") crypto_str = " --shared-key test --crypto {}".format(crypto) if crypto else "" args = "-t tap --daemon -l 3210 --no-port-forwarding" + crypto_str self.run_receiver("sudo vpncloud {args} --ifup 'ifconfig $IFNAME {ip}/24 mtu {mtu} up'".format(args=args, mtu=mtu, ip=self.receiver_ip_vpncloud)) eprint("Setting up vpncloud on sender") self.run_sender("sudo vpncloud {args} -c {peer}:3210 --ifup 'ifconfig $IFNAME {ip}/24 mtu {mtu} up'".format(args=args, mtu=mtu, ip=self.sender_ip_vpncloud, peer=self.receiver_ip)) time.sleep(1.0) def stop_vpncloud(self): self.run_sender("sudo killall vpncloud") self.run_receiver("sudo killall vpncloud") time.sleep(3.0) def run(self): results = { "meta": { "region": REGION, "instance_type": INSTANCE_TYPE, "ami": AMI, "version": VERSION }, "native": self.run_suite(self.receiver_ip) } for mtu in [1400, 7000]: for crypto in [None, "aes256", "chacha20"]: eprint("Running with mtu {} and crypto {}".format(mtu, crypto or "plain")) self.start_vpncloud(mtu=mtu, crypto=crypto) res = self.run_suite(self.receiver_ip_vpncloud) self.stop_vpncloud() results["{}-{}".format(crypto or "plain", mtu)] = res results['results'] = { "throughput_mbits": dict([ (k, results[k]["iperf"]["throughput"] / 1000000.0) for k in ["native", "plain-1400", "aes256-1400", "chacha20-1400", "plain-7000", "aes256-7000", "chacha20-7000"] ]), "latency_ms": dict([ (k, dict([ (str(s), (results[k]["ping_%s" % s]["rtt_avg"] - results["native"]["ping_%s" % s]["rtt_avg"])*1000.0/2.0) for s in [100, 500, 1000] ])) for k in ["plain-1400", "aes256-1400", "chacha20-1400", "plain-7000", "aes256-7000", "chacha20-7000"] ]) } return results env = EC2Environment() atexit.register(lambda: env.terminate()) perf = PerfTest.from_ec2_env(env) start = time.time() results = perf.run() duration = time.time() - start results["meta"]["duration"] = duration name = "{date}_{version}_perf.json".format(date=date.today().strftime('%Y-%m-%d'), version=VERSION) eprint('Storing results in {}'.format(name)) with open(name, 'w') as fp: json.dump(results, fp, indent=2) eprint("done.")