Skip to content

Commit

Permalink
Remove port number requirement
Browse files Browse the repository at this point in the history
Remove env vars displayed in docs
Update README.md
  • Loading branch information
dormant-user committed Nov 16, 2022
1 parent c413b0e commit 7ceaf4c
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 59 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Change Log
==========

0.6.1 (11/16/2022)
------------------
- Remove port number requirement
- Remove env vars displayed in docs
- Update README.md

0.6.0 (11/15/2022)
------------------
- Provide option for instance types and validate
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@
[![sourcerank](https://img.shields.io/librariesio/sourcerank/pypi/vpn-server)](https://libraries.io/pypi/vpn-server)

# VPN Server
Create an on demand VPN Server running with `OpenVPN` using `AWS EC2` and `Python`.
Create your own VPN server on demand (fully automated) running with `OpenVPN` using `AWS EC2` implemented using `python`.

### How it works
- Create an AWS EC2 instance using a pre-built OpenVPN AMI.
- Create a security group with the necessary ports allowed.
- Configure the vpn server.
- Download the [OpenVPN client](https://openvpn.net/vpn-client/) and connect using public IP of the ec2 instance and login.
> To take it a step further, if you have a registered domain in AWS,
> vpn-server can be accessed with an alias record in route53 pointing to the public IP of the ec2 instance
- All the above steps are performed automatically when creating a new VPN server.
- This module can also be used to clean up all the AWS resources spun up for creating a vpn server.

### ENV Variables
Environment variables are loaded from `.env` file if present.
Expand All @@ -28,9 +38,8 @@ Environment variables are loaded from `.env` file if present.

Use [cloudping.info](https://www.cloudping.info/) to pick the fastest (from current location) available region.

- **VPN_USERNAME** - Username to access `OpenVPN Connect` client. Defaults to `openvpn`
- **VPN_USERNAME** - Username to access `OpenVPN Connect` client. Defaults to login profile or `openvpn`
- **VPN_PASSWORD** - Password to access `OpenVPN Connect` client. Defaults to `awsVPN2021`
- **VPN_PORT** - Port number where the traffic has to be forwarded. Defaults to `943`
- **IMAGE_ID** - AMI ID to be used. Defaults to a pre-built AMI for the US regions.
- **INSTANCE_TYPE** - Instance type to use for the VPN server. Defaults to `t2.nano`, use `t2.micro` when on free-tier.
- **DOMAIN** - Domain name for the hosted zone.
Expand All @@ -50,16 +59,11 @@ Optionally `env vars` for AWS config (`AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_R

### Usage
```python
from vpn import controller
from vpn.controller import VPNServer

# Store generated files in any location (Default: current working directory)
# If a custom file path is set for creation, the same path has to be used for deletion
controller.INFO_FILE = '/path/to/my_secret_repo/info.json'
controller.PEM_FILE = '/path/to/my_secret_repo/key.pem'
vpn_server = VPNServer()

vpn_server = controller.VPNServer() # Instantiates of the object

vpn_server.create_vpn_server() # Create a VPN Server
vpn_server.create_vpn_server() # Create a VPN Server, login information will be saved to a JSON file

vpn_server.reconfigure_vpn() # Re-configure an existing VPN Server

Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version_info = (0, 6, 0)
version_info = (0, 6, 1)
6 changes: 2 additions & 4 deletions vpn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ class SSHConfig:
"""

def __init__(self, port, vpn_username, vpn_password):
def __init__(self, vpn_username: AnyStr, vpn_password: AnyStr):
"""Instantiates object and stores port, username and password as members.
Args:
port: Port number to spin up VPN server on.
vpn_username: Username for authentication.
vpn_password: Password for authentication.
"""
self.port = port
self.vpn_username = vpn_username
self.vpn_password = vpn_password

Expand All @@ -26,7 +24,7 @@ def get_config(self) -> Dict[AnyStr, Union[Tuple, List]]:
"1|Please enter 'yes' to indicate your agreement \\[no\\]: ": ("yes", 10),
"2|> Press ENTER for default \\[yes\\]: ": ("yes", 2),
"3|> Press Enter for default \\[1\\]: ": ("1", 2),
"4|> Press ENTER for default \\[943\\]: ": [str(self.port), 2],
"4|> Press ENTER for default \\[943\\]: ": ("943", 2),
"5|> Press ENTER for default \\[443\\]: ": ("443", 2),
"6|> Press ENTER for default \\[no\\]: ": ("yes", 2),
"7|> Press ENTER for default \\[no\\]: ": ("yes", 2),
Expand Down
62 changes: 28 additions & 34 deletions vpn/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,17 @@ class VPNServer:
"""

def __init__(self, aws_access_key: str = settings.aws_access_key,
aws_secret_key: str = settings.aws_secret_key, image_id: str = settings.image_id,
aws_region_name: str = settings.aws_region_name, vpn_port: int = settings.vpn_port,
domain: str = settings.domain, record_name: str = settings.record_name,
vpn_username: str = settings.vpn_username, vpn_password: str = settings.vpn_password,
gmail_user: str = settings.gmail_user, gmail_pass: str = settings.gmail_pass,
phone: str = settings.phone, recipient: str = settings.recipient,
instance_type: str = settings.instance_type,
log: str = 'CONSOLE'):
def __init__(self, aws_access_key: str = None, aws_secret_key: str = None,
image_id: str = None, aws_region_name: str = None, domain: str = None, record_name: str = None,
vpn_username: str = None, vpn_password: str = None, gmail_user: str = None, gmail_pass: str = None,
phone: str = None, recipient: str = None, instance_type: str = None, log: str = 'CONSOLE'):
"""Assigns a name to the PEM file, initiates the logger, client and resource for EC2 using ``boto3`` module.
Args:
aws_access_key: Access token for AWS account.
aws_secret_key: Secret ID for AWS account.
aws_region_name: Region where the instance should live. Defaults to AWS profile default.
image_id: AMI ID using which the instance should be created.
vpn_port: Port number using which VPN traffic should be forwarded.
domain: Domain name for the hosted zone.
record_name: Record using which the VPN server has to be accessed.
vpn_username: Username to access VPN client.
Expand Down Expand Up @@ -84,26 +78,26 @@ def __init__(self, aws_access_key: str = settings.aws_access_key,
raise NotADirectoryError(f"{os.path.dirname(INFO_FILE)!r} does not exist!")

# AWS region setup
self.region = aws_region_name
self.region = aws_region_name or settings.aws_region_name

self.SESSION = boto3.session.Session(aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key,
self.SESSION = boto3.session.Session(aws_access_key_id=aws_access_key or settings.aws_access_key,
aws_secret_access_key=aws_secret_key or settings.aws_secret_key,
region_name=self.region)

# AWS user inputs
self.image_id = image_id
self.port = vpn_port
self.domain = domain
self.record_name = record_name
self.instance_type = instance_type
self.image_id = image_id or settings.image_id
self.domain = domain or settings.domain
self.record_name = record_name or settings.record_name
self.instance_type = instance_type or settings.instance_type

# Load boto3 clients
self.ec2_client = self.SESSION.client(service_name='ec2')
self.ec2_resource = self.SESSION.resource(service_name='ec2')
self.route53_client = self.SESSION.client(service_name='route53')

# Login credentials setup
self.vpn_username = vpn_username
self.vpn_password = vpn_password
self.vpn_username = vpn_username or settings.vpn_username
self.vpn_password = vpn_password or settings.vpn_password

# Logger setup
if log.upper() == 'CONSOLE':
Expand All @@ -118,13 +112,12 @@ def __init__(self, aws_access_key: str = settings.aws_access_key,
self.log_file = log_file

# Notification information
self.gmail_user = gmail_user
self.gmail_pass = gmail_pass
self.recipient = recipient
self.phone = phone
self.gmail_user = gmail_user or settings.gmail_user
self.gmail_pass = gmail_pass or settings.gmail_pass
self.recipient = recipient or settings.recipient
self.phone = phone or settings.phone

self.configuration = SSHConfig(vpn_username=self.vpn_username, vpn_password=self.vpn_password,
port=self.port)
self.configuration = SSHConfig(vpn_username=self.vpn_username, vpn_password=self.vpn_password)

def __del__(self):
"""Destructor to print the run time at the end."""
Expand Down Expand Up @@ -259,8 +252,8 @@ def _authorize_security_group(self, security_group_id: str) -> bool:
'ToPort': 443,
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]},
{'IpProtocol': 'tcp',
'FromPort': self.port,
'ToPort': self.port,
'FromPort': 943,
'ToPort': 943,
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]},
{'IpProtocol': 'tcp',
'FromPort': 945,
Expand Down Expand Up @@ -496,18 +489,19 @@ def _tester(self, data: Dict) -> bool:
- ``True`` if the existing connection is reachable and ``ssh`` to the origin succeeds.
- ``False`` if the connection fails or unable to ``ssh`` to the origin.
"""
self.logger.info(f"Testing GET connection to https://{data.get('public_ip')}:{self.port}")
self.logger.info(f"Testing GET connection to https://{data.get('public_ip')}:943")
try:
url_check = requests.get(url=f"https://{data.get('public_ip')}:{self.port}", verify=False)
except requests.ConnectionError:
self.logger.error('Unable to connect the VPN server. Please check the logs for more information.')
url_check = requests.get(url=f"https://{data.get('public_ip')}:943", verify=False, timeout=5)
except requests.RequestException as error:
self.logger.error(error)
self.logger.error('Unable to connect the VPN server.')
return False

test_ssh = Server(username='openvpnas', hostname=data.get('public_dns'), pem_file=self.PEM_FILE)
self.logger.info(f"Testing SSH connection to {data.get('public_dns')}")
if url_check.ok and test_ssh.run_interactive_ssh(logger=self.logger, display=False,
timeout=5, log_file=self.log_file):
self.logger.info(f"Connection to https://{data.get('public_ip')}:{self.port} and "
self.logger.info(f"Connection to https://{data.get('public_ip')}:943 and "
f"SSH to {data.get('public_dns')} was successful.")
return True
else:
Expand Down Expand Up @@ -683,13 +677,13 @@ def create_vpn_server(self) -> None:
self.logger.info('VPN server has been configured successfully. '
f'Details have been stored in {self.INFO_IDENTIFIER}.')
url = f"https://{instance_info.get('public_ip')}"
instance_info.update({'SERVER': f"{url}:{self.port}",
instance_info.update({'SERVER': f"{url}:943",
'USERNAME': self.vpn_username,
'PASSWORD': self.vpn_password})
with open(self.INFO_FILE, 'w') as file:
json.dump(instance_info, file, indent=2)

self._notify(message=f"SERVER: {public_ip}:{self.port}\n\n"
self._notify(message=f"SERVER: {public_ip}:943\n\n"
f"Username: {self.vpn_username}\n"
f"Password: {self.vpn_password}")

Expand Down
9 changes: 0 additions & 9 deletions vpn/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def __init__(self):
self.aws_secret_key: str = os.environ.get('AWS_SECRET_KEY', os.environ.get('aws_secret_key'))
self.aws_region_name: str = os.environ.get('AWS_REGION_NAME', os.environ.get('aws_region_name'))
self.image_id: str = os.environ.get('IMAGE_ID', os.environ.get('image_id'))
self.vpn_port: int = os.environ.get('VPN_PORT', os.environ.get('vpn_port', 943))
self.domain: str = os.environ.get('DOMAIN', os.environ.get('domain'))
self.record_name: str = os.environ.get('RECORD_NAME', os.environ.get('record_name'))
self.vpn_username: str = os.environ.get('VPN_USERNAME', os.environ.get('vpn_username',
Expand All @@ -40,14 +39,6 @@ def __init__(self):
self.recipient: str = os.environ.get('RECIPIENT', os.environ.get('recipient'))
self.instance_type: str = os.environ.get('INSTANCE_TYPE', os.environ.get('instance_type'))

if not isinstance(self.vpn_port, int):
if str(self.vpn_port).isdigit():
self.vpn_port = int(self.vpn_port)
else:
raise ValueError(
"Port number should be an integer."
)

test_client = boto3.client('ec2')
if self.aws_region_name and self.aws_region_name.lower() in [region['RegionName'] for region in
test_client.describe_regions()['Regions']]:
Expand Down

0 comments on commit 7ceaf4c

Please sign in to comment.