Repository: jar-o/rotvpn
Branch: master
Commit: 30745147ca9c
Files: 11
Total size: 32.6 KB
Directory structure:
gitextract_aji8b5ex/
├── .gitignore
├── LICENSE
├── README.md
├── aux/
│ ├── rotvpn.psd
│ ├── setup-debian-12.sh
│ └── setup-ubuntu.sh
├── providers/
│ ├── aws.py
│ ├── common.py
│ └── digitalocean.py
├── requirements.txt
└── rotvpn.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.key
peer-tunnel-configs*
*.pem
.venv3
.env
.DS_Store
__pycache__
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 James R.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

Run a personal VPN in the cloud. And _rotate_ it regularly.
VPN servers are a great way to hide your activity from the prying eyes of ISPs
and obfuscate your location. But it's better to run your own VPN than to pay
for a service, since the VPN service may be tracking you too. However, setting
up a VPN is difficult. Add to that, it's probably wise to change the IP address
of your VPN after a while. Usually, this means tearing the existing server down
and standing up a new one in its place.
**rotvpn** is a pure Python utility that sets up a
[WireGuard](https://github.com/WireGuard/WireGuard) full tunnel VPN with DNS (via
[unbound](https://github.com/NLnetLabs/unbound)). It is designed to be
ephemeral, so you can rotate to a new server with a single command.
_Currently, only [DigitalOcean](https://digitalocean.com) and [AWS](https://aws.amazon.com/) are supported. Other providers may be added in the future... Got a favorite? Feel free to send a patch!_
# Installation
**rotvpn** installs and runs from your workstation.
You must have `python3` in your path. You will need to create a virtual
environment and install the dependencies:
```
python3 -m venv .env # create the virtualenv
source .env/bin/activate # start it up
python3 -m pip install --upgrade pip # upgrade installer
python3 -m pip install -r requirements.txt # install requirements
```
# Running
After you've installed the Python3 dependencies, you're ready to run your own VPN. Almost. Depending on which provider you use (Digitalocean is the default), you will need to export some variables into the environment.
### Digitalocean
You'll need to get a [DigitalOcean API token](https://cloud.digitalocean.com/account/api/tokens). Export it into your environment:
```
export ROT_DO_TOKEN=123abc...
```
And you're ready to go.
### AWS (Amazon Web Services)
You need an account ID, secret, and region, and you will need to export them into your environment like so:
```
export ROT_AWS_ID=AKI...
export ROT_AWS_SECRET=WM0...
export ROT_AWS_REGION=us-west-2
```
After that, you can run `rotvpn.py`.
### Actually run it
If the two steps above are complete, you're ready to go. Make sure you're running in your virtualenv, and in the root of this repo. Then do something like
```
python3 rotvpn.py --name my-cool-vpn
```
After the script runs, you should have a file named `peer-tunnel-configs-<PROVIDER>-<NAME>.zip`. `<PROVIDER>` will be one of the providers above, and `<NAME>` will be whatever you gave rotvpn in the `--name` parameter. Unzip and you will have 10 peer configurations.
Here is the usage for the script:
```
> python3 rotvpn.py --help
usage: rotvpn.py [-h] [--provider PROVIDER] [--name NAME] [--do DO]
[--config CONFIG]
optional arguments:
-h, --help show this help message and exit
--provider PROVIDER Specify the provider, i.e. digitalocean
--name NAME A name for your deploy, like 'mycoolvpn'. Lets you have
multiple deploys for a provider.
--do DO Provision or remove your VPN: --do provision | --do
remove
--config CONFIG Optional JSON config for your provider
```
The `--name` parameter is the only one that is required. **rotvpn** defaults to
DigitalOcean for `--provider` and the default `--do` action is `provision`. Any
time you run the prior command, any existing server matching `--name` will be
deleted, and a new server deployed with a new set of client configs
(`peer-tunnel-configs.zip`)
Providers may have additional configuration fields they accept. If they do, you
can use the `--config` parameter to pass in that information. For instance, the
DigitalOcean provider defaults to the `sfo2` region, the `s-1vcpu-1gb`
(smallest) sized droplet and uses Ubuntu 18.04 LTS. If you want to change that,
you can do something like:
```
python3 rotvpn.py --name myvpn --config '{"region":"ams3", "size":"s-1vcpu-2gb-amd", "image":"debian-12-x64"}'
```
AWS currently supports changing the size of your instance. It defaults to `ts.micro`. You can modify this via `--config`, e.g.
```
python3 rotvpn.py --provider aws --name my-cool-vpn --config '{"size":"t2.medium"}'
```
If you're done with the VPN for a while, you can simply remove it, and save
some money until you need it again:
```
python3 rotvpn.py --name my-cool-vpn --do remove
```
# Client configuration
## MacOS
Install the [WireGuard](https://apps.apple.com/us/app/wireguard/id1451685025?mt=12) client from the App Store.
Open it and click 'Import tunnel(s) from file'.
Select one of the files unzipped from `peer-tunnel-configs.zip`.
It will load in your WireGuard client interface:

Click 'Activate'. Test your connection and IP address.
# References
Much was learned from these posts:
1. https://craighuther.com/2019/05/14/wireguard-setup-and-installation/
1. https://www.ckn.io/blog/2017/11/14/wireguard-vpn-typical-setup/
1. https://www.stavros.io/posts/how-to-configure-wireguard/
1. https://emanuelduss.ch/2018/09/wireguard-vpn-road-warrior-setup/
================================================
FILE: aux/setup-debian-12.sh
================================================
#### Debian 12
#### Installation
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install wireguard zip bind9-dnsutils unbound unbound-host -yq
#### Wireguard
umask 077 && wg genkey | tee privatekey | wg pubkey > publickey
publicaddr=$(dig +short myip.opendns.com @resolver1.opendns.com)
ipv6_prefix='fd86:ea04:1111'
fn='/etc/wireguard/wg0.conf'
echo '[Interface]' > $fn
echo "PrivateKey = $(cat privatekey)" >> $fn
echo "Address = 10.200.200.1/24, ${ipv6_prefix}::1/64" >> $fn
echo 'ListenPort = 51820' >> $fn
echo 'PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
SaveConfig = true' >> $fn
rm -f peer-tunnel-configs.zip
for i in `seq 2 11`; do
j=$(expr $i - 1)
prefix="peer${j}"
umask 077 && wg genpsk > client-preshared
umask 077 && wg genkey | tee client-privatekey | wg pubkey > client-pubkey
# add to wireguard server config file
echo '' >> $fn
echo '[Peer]' >> $fn
echo "PublicKey = $(cat client-pubkey)" >> $fn
echo "PresharedKey = $(cat client-preshared)" >> $fn
echo "AllowedIPs = 10.200.200.${i}/32, ${ipv6_prefix}::${i}/128" >> $fn
# create the client tunnel file
clifn="$prefix-tunnel.conf"
echo "[Interface]" >> $clifn
echo "Address = 10.200.200.${i}/24, ${ipv6_prefix}::${i}/64" >> $clifn
echo "PrivateKey = $(cat client-privatekey)" >> $clifn
echo "DNS = 10.200.200.1" >> $clifn
echo "" >> $clifn
echo "[Peer]" >> $clifn
echo "PublicKey = $(cat publickey)" >> $clifn
echo "PresharedKey = $(cat client-preshared)" >> $clifn
echo "AllowedIPs = 0.0.0.0/0,::/0" >> $clifn
echo "Endpoint = $publicaddr:51820" >> $clifn
rm client-preshared
rm client-privatekey
rm client-pubkey
done
zip peer-tunnel-configs.zip peer*.conf
rm peer*.conf
#### DNS Server
curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
chown unbound:unbound /var/lib/unbound/root.hints
cat > /etc/unbound/unbound.conf <<- EOM
server:
directory: "/etc/unbound"
username: unbound
chroot: "/etc/unbound"
pidfile: "/etc/unbound/unbound.pid"
verbosity: 1
num-threads: 4
max-udp-size: 3072
interface: 10.200.200.1
interface: 127.0.0.1
interface: ::1
#Authorized IPs to access the DNS Server
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.1 allow
access-control: ::1 allow
access-control: 10.200.200.0/24 allow
#not allowed to be returned for public internet names
private-address: 10.200.200.0/24
# Hide DNS Server info
hide-identity: yes
hide-version: yes
#Limit DNS Fraud and use DNSSEC
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
#Add an unwanted reply threshold to clean the cache and avoid when possible a DNS Poisoning
unwanted-reply-threshold: 10000000
#Have the validator print validation failures to the log.
val-log-level: 1
#Minimum lifetime of cache entries in seconds
cache-min-ttl: 1800
#Maximum lifetime of cached entries
cache-max-ttl: 14400
prefetch: yes
prefetch-key: yes
EOM
systemctl disable systemd-resolved
systemctl stop systemd-resolved
systemctl enable unbound
systemctl restart unbound
#### Firewall, etc
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 51820 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -s 10.200.200.0/24 -p tcp -m tcp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -s 10.200.200.0/24 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
iptables -A OUTPUT -p udp -m udp --sport 51820 -j ACCEPT
#### Start wireguard
wg-quick up wg0
systemctl enable wg-quick@wg0
wg show
================================================
FILE: aux/setup-ubuntu.sh
================================================
#### Ubuntu 18.04
#### Installation
echo "nameserver 8.8.8.8" | tee /etc/resolv.conf > /dev/null
add-apt-repository -y universe
apt install wireguard -y
apt install zip -y
apt install unbound unbound-host -y
#### Wireguard
umask 077 && wg genkey | tee privatekey | wg pubkey > publickey
publicaddr=$(dig +short myip.opendns.com @resolver1.opendns.com)
ipv6_prefix='fd86:ea04:1111'
fn='/etc/wireguard/wg0.conf'
echo '[Interface]' > $fn
echo "PrivateKey = $(cat privatekey)" >> $fn
echo "Address = 10.200.200.1/24, ${ipv6_prefix}::1/64" >> $fn
echo 'ListenPort = 51820' >> $fn
echo 'PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
SaveConfig = true' >> $fn
rm -f peer-tunnel-configs.zip
for i in `seq 2 11`; do
j=$(expr $i - 1)
prefix="peer${j}"
umask 077 && wg genpsk > client-preshared
umask 077 && wg genkey | tee client-privatekey | wg pubkey > client-pubkey
# add to wireguard server config file
echo '' >> $fn
echo '[Peer]' >> $fn
echo "PublicKey = $(cat client-pubkey)" >> $fn
echo "PresharedKey = $(cat client-preshared)" >> $fn
echo "AllowedIPs = 10.200.200.${i}/32, ${ipv6_prefix}::${i}/128" >> $fn
# create the client tunnel file
clifn="$prefix-tunnel.conf"
echo "[Interface]" >> $clifn
echo "Address = 10.200.200.${i}/24, ${ipv6_prefix}::${i}/64" >> $clifn
echo "PrivateKey = $(cat client-privatekey)" >> $clifn
echo "DNS = 10.200.200.1" >> $clifn
echo "" >> $clifn
echo "[Peer]" >> $clifn
echo "PublicKey = $(cat publickey)" >> $clifn
echo "PresharedKey = $(cat client-preshared)" >> $clifn
echo "AllowedIPs = 0.0.0.0/0,::/0" >> $clifn
echo "Endpoint = $publicaddr:51820" >> $clifn
rm client-preshared
rm client-privatekey
rm client-pubkey
done
zip peer-tunnel-configs.zip peer*.conf
rm peer*.conf
#### DNS Server
curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
chown unbound:unbound /var/lib/unbound/root.hints
cat > /etc/unbound/unbound.conf <<- EOM
# Unbound configuration file for Debian.
#
# See the unbound.conf(5) man page.
#
# See /usr/share/doc/unbound/examples/unbound.conf for a commented
# reference config file.
#
# The following line includes additional configuration files from the
# /etc/unbound/unbound.conf.d directory.
include: "/etc/unbound/unbound.conf.d/*.conf"
server:
num-threads: 4
#Enable logs
verbosity: 1
#list of Root DNS Server
root-hints: "/var/lib/unbound/root.hints"
#Respond to DNS requests on wireguard interface
interface: 10.200.200.1
max-udp-size: 3072
#Authorized IPs to access the DNS Server
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.1 allow
access-control: 10.200.200.0/24 allow
#not allowed to be returned for public internet names
private-address: 10.200.200.0/24
# Hide DNS Server info
hide-identity: yes
hide-version: yes
#Limit DNS Fraud and use DNSSEC
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
#Add an unwanted reply threshold to clean the cache and avoid when possible a DNS Poisoning
unwanted-reply-threshold: 10000000
#Have the validator print validation failures to the log.
val-log-level: 1
#Minimum lifetime of cache entries in seconds
cache-min-ttl: 1800
#Maximum lifetime of cached entries
cache-max-ttl: 14400
prefetch: yes
prefetch-key: yes
EOM
systemctl disable systemd-resolved
systemctl stop systemd-resolved
systemctl enable unbound
systemctl restart unbound
#### Firewall, etc
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 51820 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -s 10.200.200.0/24 -p tcp -m tcp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -s 10.200.200.0/24 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
iptables -A OUTPUT -p udp -m udp --sport 51820 -j ACCEPT
ufw allow 22/tcp
ufw allow 51820/udp
echo "y" | ufw enable
#### Teh end
wg-quick up wg0
systemctl enable wg-quick@wg0
wg show
================================================
FILE: providers/aws.py
================================================
import os, time, base64, json
import boto3
from providers.common import get_my_ip, wireguard_port, install_wireguard
class AWSProvider:
def __init__(self, deploy_name, config=None):
self.deploy_name = 'rotvpn-{}'.format(deploy_name)
self.aws_id = os.getenv('ROT_AWS_ID')
if self.aws_id == None:
raise Exception("Must set ROT_AWS_* env vars! See https://github.com/jar-o/rotvpn")
self.aws_secret = os.getenv('ROT_AWS_SECRET')
self.aws_region = os.getenv('ROT_AWS_REGION')
self.client = boto3.client('ec2',
aws_access_key_id=self.aws_id,
aws_secret_access_key=self.aws_secret,
region_name=self.aws_region)
self.resource = boto3.session.Session(
aws_access_key_id=self.aws_id,
aws_secret_access_key=self.aws_secret,
aws_session_token=None,
).resource('ec2')
self.key_name = 'rotvpn-aws-ecs-keypair-' + deploy_name
self.key_fn = self.key_name + '.pem'
if config != None:
self.config = json.loads(config)
def gen_ssh_keys(self):
if os.path.exists(self.key_fn):
print('SSH key already seems to exist ({}). Skipping generation.'.format(self.key_fn))
return
keypair = None
try:
keypair = self.client.create_key_pair(KeyName=self.key_name)
except self.client.exceptions.ClientError as e:
if 'invalidkeypair.duplicate' not in str(e).lower():
raise(e)
print('Key {} already exists. Going to renew.'.format(self.key_name))
if keypair == None:
self.client.delete_key_pair(KeyName=self.key_name)
keypair = self.client.create_key_pair(KeyName=self.key_name)
with open(self.key_fn, 'w') as outfile:
outfile.write(str(keypair['KeyMaterial']))
os.chmod(self.key_fn, 0o600)
def provision(self):
self.gen_ssh_keys()
self.remove() # rotation
# create the instance
size = 't2.micro'
if hasattr(self, 'config') and self.config != None:
if 'size' in self.config:
size = self.config['size']
# Details about Ubuntu machine images here: https://cloud-images.ubuntu.com/locator/
self.instance = create_ec2_instance(self.client, 'ami-0a7d051a1c4b54f65', size, self.key_name)
self.instance_obj = self.resource.Instance(id=self.instance['InstanceId'])
self.instance_obj.create_tags(Tags=[
{
'Key': 'Name',
'Value': self.deploy_name,
}
])
print('Waiting for instance...')
self.instance_obj.wait_until_running()
self.instance_obj.load()
try:
self.set_inbound_rules()
except self.client.exceptions.ClientError as e:
if 'invalidpermission.duplicate' not in str(e).lower():
raise(e)
print('Inbound firewall rules already exist. Nothing to do.')
print('Instance is live: {}'.format(self.instance_obj.public_ip_address))
peer_config_download_dest = 'peer-tunnel-configs-aws-{}.zip'.format(self.deploy_name)
install_wireguard(
self.instance_obj.public_ip_address,
self.key_fn,
peer_config_download_dest,
'ubuntu',
'/home/ubuntu')
def set_inbound_rules(self):
response = self.client.describe_instances()
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
if instance['InstanceId'] == self.instance['InstanceId']:
for sg in instance['SecurityGroups']:
if sg['GroupName'] == 'default':
print('Setting SSH inbound rule on {}'.format(self.instance['InstanceId']))
myip = get_my_ip()
self.client.authorize_security_group_ingress(
GroupId=sg['GroupId'],
IpPermissions=[{
'FromPort': 22,
'ToPort': 22,
'IpProtocol': 'tcp',
'IpRanges': [
{'CidrIp': '{}/32'.format(myip)}
]
}],
)
print('Setting Wireguard inbound rule on {} for port {}'.format(
self.instance['InstanceId'], wireguard_port))
self.client.authorize_security_group_ingress(
GroupId=sg['GroupId'],
IpPermissions=[{
'FromPort': wireguard_port,
'ToPort': wireguard_port,
'IpProtocol': 'udp',
'IpRanges': [
{'CidrIp': '0.0.0.0/0'}
]
}],
)
def remove(self):
reservations = self.client.describe_instances(
Filters=[{'Name': 'instance-state-name', 'Values': ['running']}])["Reservations"]
for res in reservations:
for inst in res['Instances']:
for tag in inst['Tags']:
instid = inst['InstanceId']
if tag['Key'] == 'Name' and tag['Value'] == self.deploy_name:
print('Instance {} found with ID {}. Terminating ...'.format(self.deploy_name, instid))
self.resource.instances.filter(InstanceIds = [instid]).terminate()
print('Done.')
# Stole from AWS docs
def create_ec2_instance(ec2_client, image_id, instance_type, keypair_name):
"""Provision and launch an EC2 instance
The method returns without waiting for the instance to reach
a running state.
:param image_id: ID of AMI to launch, such as 'ami-XXXX'
:param instance_type: string, such as 't2.micro'
:param keypair_name: string, name of the key pair
:return Dictionary containing information about the instance. If error,
returns None.
"""
# Provision and launch the EC2 instance
# ec2_client = boto3.client('ec2')
try:
response = ec2_client.run_instances(ImageId=image_id,
InstanceType=instance_type,
KeyName=keypair_name,
MinCount=1,
MaxCount=1)
except ec2_client.exceptions.ClientError as e:
print(e)
return None
return response['Instances'][0]
================================================
FILE: providers/common.py
================================================
import paramiko
import qrcode
import time, os
import zipfile
from os import listdir
from os.path import isfile
from requests import get
from scp import SCPClient, SCPException
wireguard_port = 51820
# TODO it's sort of bothersome that this requires a matching edit in
# setup-ubuntu.sh. Should templatize and drive from Python side.
peer_config_download = 'peer-tunnel-configs.zip'
def get_my_ip():
return get('https://api.ipify.org').text
def unzip_file(zip_path, dest_path):
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(dest_path)
def gen_qr_code(input_path, output_path):
with open(input_path, 'rb') as f:
input_data = f.read()
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(input_data)
qr.make(fit=True)
img = qr.make_image(fill='black', back_color='white')
img.save(output_path)
def extract_configs_and_generate_qr_codes(zip_path):
out_path = zip_path + '.extracted'
unzip_file(zip_path, out_path)
conf_files = [os.path.join(out_path, f) for f in listdir(out_path) if isfile(os.path.join(out_path, f))]
for f in conf_files:
of = f + '.png'
gen_qr_code(f, of)
print(f"QR code generated: {f} -> {of}")
def install_wireguard(ip_address, privkey_filename, peer_config_download_dest, setup_script=None, username='root', home='/root'):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
for i in range(5):
print("Attempting to connect {}/{} ...".format(i, 5))
try:
client.connect(ip_address, username=username, key_filename=os.path.abspath(privkey_filename))
except paramiko.ssh_exception.NoValidConnectionsError:
print('No valid connection. (Server probably not ready.)')
time.sleep(5)
continue
except TimeoutError:
print("Timeout. Hm... let's try again")
time.sleep(5)
continue
scp = SCPClient(client.get_transport())
if setup_script == None:
setup_script = './aux/setup-ubuntu.sh'
print('Installing server (running script {} remotely), takes a little time ...'.format(setup_script))
#TODO ensure pathing
scp.put(setup_script, '{}/setup.sh'.format(home))
if username == 'root':
stdin, stdout, stderr = client.exec_command('{}/setup.sh'.format(home))
else: # Assume sudo
cmd1 = '{}/setup.sh'.format(home)
cmd2 = 'chmod a+rw {}'.format(peer_config_download)
cmd3 = 'mv {} {}'.format(peer_config_download, home)
cmd = "sudo su - root -c 'sleep 15 && {} && {} && {}'".format(cmd1, cmd2, cmd3)
print('Running {}'.format(cmd))
stdin, stdout, stderr = client.exec_command(cmd)
exit_status = stdout.channel.recv_exit_status() # Blocking call
if exit_status != 0:
print('Error occured. Cannot continue Exit status {}'.format(exit_status))
print('{}'.format(stdout))
print('{}'.format(stderr))
print('You may be able to SSH into the server and troubleshoot:')
print('ssh -i {} {}@{}'.format(os.path.abspath(privkey_filename), username, ip_address))
return
# now, retrieve the generated peer configs
try:
os.remove(peer_config_download)
os.remove(peer_config_download_dest)
except FileNotFoundError:
pass
for j in range(10):
try:
scp.get('{}/{}'.format(home, peer_config_download))
except SCPException:
print("{} not avilable. Trying again {}/{}".format(peer_config_download, j, 10))
time.sleep(5)
continue
break
if os.path.exists(peer_config_download):
os.rename(peer_config_download, peer_config_download_dest)
print('Peer configs available: {}'.format(peer_config_download_dest))
extract_configs_and_generate_qr_codes(peer_config_download_dest)
print('SUCCESS!')
else:
print('Something went wrong? No {} found.'.format(peer_config_download))
break
================================================
FILE: providers/digitalocean.py
================================================
# NOTE Providers MUST implement provision() and remove(). That is all.
import os, time, base64, json
from providers.common import install_wireguard
from Crypto.PublicKey import RSA
import digitalocean
class DigitalOceanProvider:
endpoint = 'https://api.digitalocean.com/v2/'
prefix = 'digitalocean'
privkey_fn = "rotvpn-{}-private.key".format(prefix)
pubkey_fn = "rotvpn-{}-public.key".format(prefix)
vpn_name_prefix = 'rotvpn'
def __init__(self, deploy_name, config=None):
v = os.getenv('ROT_DO_TOKEN')
if v == None:
raise Exception("Must set ROT_DO_TOKEN env var to DigitalOcean API token! See https://cloud.digitalocean.com/account/api/tokens")
self.deploy_name = deploy_name
self.manager = digitalocean.Manager(token=v)
self.name = '-'.join([self.vpn_name_prefix, self.deploy_name])
self.keyname = '-'.join([self.vpn_name_prefix, self.deploy_name, 'ssh-key'])
if config != None:
self.config = json.loads(config)
def gen_ssh_keys(self):
if os.path.exists(self.privkey_fn) and os.path.exists(self.pubkey_fn):
print('SSH keys already seem to exist. Skipping generation.')
with open(self.privkey_fn, 'r') as content_file:
self.privkey = content_file.read()
with open(self.pubkey_fn, 'r') as content_file:
self.pubkey = content_file.read()
return
key = RSA.generate(2048)
with open(self.privkey_fn, 'wb') as content_file:
os.chmod(self.privkey_fn, 0o600)
k = key.exportKey('PEM')
content_file.write(k)
self.privkey = k.decode('utf-8')
with open(self.pubkey_fn, 'wb') as content_file:
k = key.publickey().exportKey('OpenSSH')
content_file.write(k)
self.pubkey = k.decode('utf-8')
self.__add_ssh_key_to_digitalocean()
def __add_ssh_key_to_digitalocean(self):
print('Adding public key {} to DigitalOcean'.format(self.keyname))
# check for existing key, and delete
for key in self.manager.get_all_sshkeys():
if key.name == self.keyname:
key.destroy()
key = digitalocean.SSHKey(
token=os.getenv('ROT_DO_TOKEN'),
name=self.keyname,
public_key=self.pubkey)
key.create()
def provision(self):
self.gen_ssh_keys()
self.remove() # if droplet exists, we delete, and make a new one... "rotation"
keys = self.manager.get_all_sshkeys()
# create droplet
size = 's-1vcpu-1gb'
region = 'sfo2'
image = 'ubuntu-18-04-x64'
if hasattr(self, 'config') and self.config != None:
if 'size' in self.config:
size = self.config['size']
if 'region' in self.config:
region = self.config['region']
if 'image' in self.config:
image = self.config['image']
droplet = digitalocean.Droplet(
token = os.getenv('ROT_DO_TOKEN'),
name = self.name,
region = region,
image = image,
size = size,
ssh_keys = keys,
backups = False)
print('Creating droplet ...')
droplet.create()
print('Droplet: {}, id={}'.format(droplet.name, droplet.id))
self.droplet_id = droplet.id
actions = droplet.get_actions()
for action in actions:
action.load()
print(action.status)
time.sleep(5)
if action.status == 'completed':
break
while droplet.ip_address == None:
print('Waiting for server ... IP: {}'.format(droplet.ip_address))
droplet = self.manager.get_droplet(self.droplet_id)
time.sleep(5)
print('IP Address: {}'.format(droplet.ip_address))
self.ip_address = droplet.ip_address
# NOTE there is no API specific way of telling if the SSH daemon is
# ready. We just have to try in a loop
time.sleep(10)
setup_script = None
if image == "debian-12-x64":
setup_script = './aux/setup-debian-12.sh'
install_wireguard(
self.ip_address,
self.privkey_fn,
'peer-tunnel-configs-digitalocean-{}.zip'.format(self.deploy_name),
setup_script
)
def remove(self):
has_removed = False
for droplet in self.manager.get_all_droplets():
if droplet.name == self.name:
print('{} exists. Removing.'.format(self.name))
print(droplet)
droplet.destroy()
actions = droplet.get_actions()
for action in actions:
action.load()
print(action.status)
time.sleep(5)
if action.status == 'completed':
break
has_removed = True
if has_removed == False:
print('Deploy {} not found.'.format(self.name))
================================================
FILE: requirements.txt
================================================
awscli==0.7.0
bcrypt==3.1.7
boto3==1.10.37
botocore==1.13.50
certifi==2023.7.22
cffi==1.15.0
chardet==3.0.4
colorama==0.4.1
cryptography==42.0.4
docutils==0.15.2
idna==3.7
Jinja2==3.1.3
jmespath==0.9.4
jsonpickle==1.2
MarkupSafe==1.1.1
Naked==0.1.31
paramiko==2.10.1
Pillow==10.3.0
pyasn1==0.4.8
pycparser==2.19
pycryptodome==3.19.1
PyNaCl==1.3.0
python-dateutil==2.8.0
python-digitalocean==1.14.0
PyYAML==5.4
qrcode==7.3.1
requests==2.31.0
rsa==4.7
s3transfer==0.2.1
scp==0.13.2
shellescape==3.4.1
six==1.13.0
urllib3==1.26.18
================================================
FILE: rotvpn.py
================================================
import sys, argparse
from providers import digitalocean, aws
def get_provider(provider, deploy_name, config):
if provider.lower() == "digitalocean":
return digitalocean.DigitalOceanProvider(deploy_name, config)
elif provider.lower() == "aws":
return aws.AWSProvider(deploy_name, config)
raise Exception('Could not match provider {}'.format(provider))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--provider', default='digitalocean', help='Specify the provider, i.e. digitalocean')
parser.add_argument('--name', help="A name for your deploy, like 'mycoolvpn'. Lets you have multiple deploys for a provider.")
parser.add_argument('--do', default='provision', help='Provision or remove your VPN: --do provision | --do remove')
parser.add_argument('--config', help='Optional JSON config for your provider')
args = parser.parse_args()
if args.name == None:
print("Must provide a name for your rotvpn deploy. E.g. my-cool-vpn")
sys.exit(7)
provider = get_provider(args.provider, args.name, args.config)
if args.do == 'remove':
provider.remove()
else:
provider.provision()
gitextract_aji8b5ex/ ├── .gitignore ├── LICENSE ├── README.md ├── aux/ │ ├── rotvpn.psd │ ├── setup-debian-12.sh │ └── setup-ubuntu.sh ├── providers/ │ ├── aws.py │ ├── common.py │ └── digitalocean.py ├── requirements.txt └── rotvpn.py
SYMBOL INDEX (19 symbols across 4 files)
FILE: providers/aws.py
class AWSProvider (line 5) | class AWSProvider:
method __init__ (line 6) | def __init__(self, deploy_name, config=None):
method gen_ssh_keys (line 26) | def gen_ssh_keys(self):
method provision (line 43) | def provision(self):
method set_inbound_rules (line 78) | def set_inbound_rules(self):
method remove (line 111) | def remove(self):
function create_ec2_instance (line 125) | def create_ec2_instance(ec2_client, image_id, instance_type, keypair_name):
FILE: providers/common.py
function get_my_ip (line 17) | def get_my_ip():
function unzip_file (line 20) | def unzip_file(zip_path, dest_path):
function gen_qr_code (line 24) | def gen_qr_code(input_path, output_path):
function extract_configs_and_generate_qr_codes (line 33) | def extract_configs_and_generate_qr_codes(zip_path):
function install_wireguard (line 42) | def install_wireguard(ip_address, privkey_filename, peer_config_download...
FILE: providers/digitalocean.py
class DigitalOceanProvider (line 8) | class DigitalOceanProvider:
method __init__ (line 14) | def __init__(self, deploy_name, config=None):
method gen_ssh_keys (line 24) | def gen_ssh_keys(self):
method __add_ssh_key_to_digitalocean (line 43) | def __add_ssh_key_to_digitalocean(self):
method provision (line 54) | def provision(self):
method remove (line 106) | def remove(self):
FILE: rotvpn.py
function get_provider (line 5) | def get_provider(provider, deploy_name, config):
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (35K chars).
[
{
"path": ".gitignore",
"chars": 67,
"preview": "*.key\npeer-tunnel-configs*\n*.pem\n.venv3\n.env\n.DS_Store\n__pycache__\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2024 James R.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 5302,
"preview": "\n\nRun a personal VPN in the cloud. And _rotate_ it regularly.\n\nVPN servers are a "
},
{
"path": "aux/setup-debian-12.sh",
"chars": 4266,
"preview": "#### Debian 12\n\n#### Installation\nexport DEBIAN_FRONTEND=noninteractive\napt-get update\napt-get install wireguard zip bin"
},
{
"path": "aux/setup-ubuntu.sh",
"chars": 4686,
"preview": "#### Ubuntu 18.04\n\n\n#### Installation\n\n\necho \"nameserver 8.8.8.8\" | tee /etc/resolv.conf > /dev/null\n\nadd-apt-repository"
},
{
"path": "providers/aws.py",
"chars": 6935,
"preview": "import os, time, base64, json\nimport boto3\nfrom providers.common import get_my_ip, wireguard_port, install_wireguard\n\ncl"
},
{
"path": "providers/common.py",
"chars": 4238,
"preview": "import paramiko\nimport qrcode\nimport time, os\nimport zipfile\n\nfrom os import listdir\nfrom os.path import isfile\nfrom req"
},
{
"path": "providers/digitalocean.py",
"chars": 5106,
"preview": "# NOTE Providers MUST implement provision() and remove(). That is all.\nimport os, time, base64, json\nfrom providers.comm"
},
{
"path": "requirements.txt",
"chars": 528,
"preview": "awscli==0.7.0\nbcrypt==3.1.7\nboto3==1.10.37\nbotocore==1.13.50\ncertifi==2023.7.22\ncffi==1.15.0\nchardet==3.0.4\ncolorama==0."
},
{
"path": "rotvpn.py",
"chars": 1211,
"preview": "import sys, argparse\nfrom providers import digitalocean, aws\n\n\ndef get_provider(provider, deploy_name, config):\n if p"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the jar-o/rotvpn GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (32.6 KB), approximately 8.8k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.