Manage Your Own VPN - A Penny-Pincher's Guide

  1. Problem with VPN and Proxy Service Pricing Model
  2. On Demand Cloud Proxy
    1. Provider 1. Google Cloud
    2. Provider 2. Azure
    3. Other Cloud Service Provider
    4. Cloud Agnostic Terraform Script
    5. Notification on Proxy Ready
  3. Price comparision
  4. VPN with WireGuard
  5. User friendly on and off
  6. Reminder to switch off

Ah, the internet – a vast expanse of knowledge and cat videos. But as you navigate this digital sea, you might find yourself wanting a bit more privacy, or perhaps you’re just tired of being told what content you can and cannot view based on your location…

Problem with VPN and Proxy Service Pricing Model

Let’s have a quick look on the VPN service price as of Dec 2023

VPN Provider Price per Month (USD) Commitment
ProtonVPN $3.95 2-Year
Surfshark VPN $229 2-Year
ExpressVPN $6.67 1-Year
NordVPN $3.09 2-Year

Pay-as-you-go option does not exists and we are forcing to adopt subscription based model. Subscription based model is like hiring a bodyguard who insists on a year-long contract when you only need someone to watch your back during that shady walk home once a month. A penny pincher like me does not accept this subscription offer.

On Demand Cloud Proxy

Now, let us examine the available cloud services for your VPN/proxy adventures. You can create a VPN/proxy server on Cloud. For simplicity, let’s start with a proxy server on Google Cloud. Here’s how it is going to work:

Your PC
in Country A

SSH tunnel

Proxy on Google Cloud
in Country B

Target Website

The flowchart illustrates the setup of a proxy server on Google Cloud. Your PC (pc) is in Country A, and you want to access a target website (target) that is restricted or has content blocked by your location. You establish an SSH tunnel (ssh) from your PC to the proxy server (proxy) on Google Cloud, which is located in Country B. This allows you to bypass geographical restrictions and access the target website as if you were in Country B.

Provider 1. Google Cloud

Below is the terraform scripts to create a compute engine with the proxy (squid) on Google Cloud.

main.tf
resource "google_compute_instance" "default" { name = "proxy-server" machine_type = "e2-micro" zone = "us-west1-a" tags = ["ssh"] scheduling { provisioning_model = "SPOT" automatic_restart = false preemptible = true } boot_disk { initialize_params { image = "ubuntu-os-cloud/ubuntu-2004-lts" } } network_interface { network = "default" access_config { // Ephemeral public IP network_tier = "STANDARD" } } service_account { scopes = ["cloud-platform"] } metadata = { ssh-keys = format("%s:%s", var.ssh_username, var.ssh_public_key) startup-script = "sudo apt-get update;sudo apt-get install -y squid;sudo systemctl start squid" } }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/google/main.tf

variables.tf
variable "ssh_username" { type = string description = "username of SSH to the compute engine" } variable "ssh_public_key" { type = string description = "Public key for SSH" }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/google/variables.tf

output.tf
output "ip" { value = google_compute_instance.default.network_interface.0.access_config.0.nat_ip } output "command" { description = "Command to setup ssh tunnel to the proxy server" value = format("ssh-keygen -R %s; ssh -L3128:localhost:3128 %s@%s", google_compute_instance.default.network_interface.0.access_config.0.nat_ip, var.ssh_username, google_compute_instance.default.network_interface.0.access_config.0.nat_ip) }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/google/output.tf

Run terraform apply:

$ terraform apply

var.google_access_credentials
  The json file that contains key of your service account in Google Cloud

  Enter a value: a.josn

var.project
  Google Cloud Project Name

  Enter a value: a

var.ssh_public_key
  Public key for SSH

  Enter a value: ssh-rsa AAAAB...

var.ssh_username
  username of SSH to the compute engine

  Enter a value: neo

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_instance.default will be created
  + resource "google_compute_instance" "default" {
      ...
      + machine_type         = "e2-micro"
      + metadata             = {
          + "ssh-keys"       = "neo:ssh-rsa AAAAB..."
          + "startup-script" = "sudo apt-get update;sudo apt-get install -y squid;sudo systemctl start squid"
        }
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + command = (known after apply)
  + ip      = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

google_compute_instance.default: Creating...
google_compute_instance.default: Still creating... [10s elapsed]
google_compute_instance.default: Creation complete after 17s [id=projects/a/zones/us-west1-a/instances/proxy-server]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

command = "ssh -L3128:localhost:3128 neo@123.123.123.123"
ip = "123.123.123.123"

To set up an SSH tunnel to the proxy, use the command provided in the output command. You may need to wait a few moments until the proxy is ready. Once the proxy is ready, your browser can use localhost:3128 as the proxy.

When a cloud service reuses an IP address to create a new compute instance, you may experience a host validation error if you had SSH to the IP address before. This occurs because the new compute instance generates a new host key, which does not match the key you trusted in .ssh/known_hosts. To resolve this issue, you can either remove the trusted host key using ssh-keygen -R or send the private key from your local machine to the new compute instance.

Remember to destroy the compute engine once you have finished with it:

$ terraform destroy

google_compute_instance.default: Refreshing state... [id=projects/f-01man-com/zones/us-west1-a/instances/proxy-server]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  - destroy

Terraform will perform the following actions:

  # google_compute_instance.default will be destroyed
  - resource "google_compute_instance" "default" {
      ...
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

google_compute_instance.default: Destroying... [id=projects/a/zones/us-west1-a/instances/proxy-server]
google_compute_instance.default: Still destroying... [id=projects/a/zones/us-west1-a/instances/proxy-server, 10s elapsed]
google_compute_instance.default: Destruction complete after 16s

Destroy complete! Resources: 1 destroyed.

Given my extremely low usage, like 30 minutes a month, Google charges me around USD $0.20 a month. However, that doesn’t stop me from exploring other cheaper alternatives.

Provider 2. Azure

main.tf
resource "azurerm_resource_group" "rg" { name = "squid-rg" location = "West US" } resource "azurerm_virtual_machine" "proxy" { name = "squid-proxy-vm" # charge you if you dont delete delete_data_disks_on_termination = true delete_os_disk_on_termination = true resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location network_interface_ids = [azurerm_network_interface.nic.id] vm_size = "Standard_B1s" storage_os_disk { name = "os" caching = "ReadWrite" managed_disk_type = "Standard_LRS" create_option = "FromImage" os_type = "Linux" } storage_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts" version = "latest" } os_profile { admin_username = var.ssh_username computer_name = "proxy" custom_data = base64encode(<<CUSTOM_DATA #!/bin/bash sudo apt-get update;sudo apt-get install -y squid;sudo systemctl start squid CUSTOM_DATA ) } os_profile_linux_config { disable_password_authentication = true ssh_keys { path = "/home/${var.ssh_username}/.ssh/authorized_keys" key_data = var.ssh_public_key } } } resource "azurerm_network_interface" "nic" { name = "squid-nic" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location ip_configuration { name = "squid-ipconfig" subnet_id = azurerm_subnet.subnet.id private_ip_address_allocation = "Dynamic" public_ip_address_id = azurerm_public_ip.proxy.id } } resource "azurerm_subnet" "subnet" { name = "squid-subnet" resource_group_name = azurerm_resource_group.rg.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefixes = ["10.0.0.0/24"] } resource "azurerm_virtual_network" "vnet" { name = "squid-vnet" resource_group_name = azurerm_resource_group.rg.name address_space = ["10.0.0.0/8"] location = "West US" } resource "azurerm_public_ip" "proxy" { name = "squidPublicIp1" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location allocation_method = "Static" lifecycle { create_before_destroy = true } }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/azure/main.tf

variables.tf
variable "ssh_username" { type = string description = "username of SSH to the compute engine" } variable "ssh_public_key" { type = string description = "Public key for SSH" }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/azure/variables.tf

output.tf
output "ip" { value = azurerm_public_ip.proxy.ip_address } output "command" { description = "Command to setup ssh tunnel to the proxy server" value = format("ssh-keygen -R %s; ssh -L3128:localhost:3128 %s@%s", azurerm_public_ip.proxy.ip_address, var.ssh_username, azurerm_public_ip.proxy.ip_address) }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/modules/azure/output.tf

It takes time to create and destroy. You can check /var/log/cloud-init.log and look for subp.py and part to troubleshoot, eg:

2024-05-07 14:14:02,864 - subp.py[DEBUG]: Running command ['/var/lib/cloud/instance/scripts/part-001'] with allowed return codes [0] (shell=False, capture=False)
2024-05-07 14:14:02,864 - subp.py[DEBUG]: Exec format error. Missing #! in script?
Command: ['/var/lib/cloud/instance/scripts/part-001']
Exit code: -
Reason: [Errno 8] Exec format error: b'/var/lib/cloud/instance/scripts/part-001'

Other Cloud Service Provider

I have also tried Alibaba Cloud and Huawei Cloud. However, Alibaba Cloud requires account verification after a few uses of IP addresses and resources from a country other than China, which asks me to upload my passport, etc. Also, the minimum compute service is monthly instead of per consumption like Google Cloud.

On the other hand, Huawei Cloud is better; compute service can be consumption-based. However, bandwidth charges are per day subscription and not metered, resulting a daily fee of USD 2! Therefore, I do not recommend Alibaba Cloud and Huawei Cloud for those who are penny pinchers.

Cloud Agnostic Terraform Script

Now we have 2 cloud provider options, Azure and Google. We want to create cloud-agnostic Terraform scripts because it allows us to maintain a single set of code and apply it across multiple cloud providers. This approach allowing us to easily switch between different cloud service providers if needed. A cloud-agnostic architecture plus money saving!

Let’s structure the folder as below:

\ - root
    \ - main.tf
      - variables.tf
      - output.tf
      - provider.tf
    \ - modules
        \ - google
            \ - main.tf
              - variables.tf
              - output.tf
        \ - azure
            \ - main.tf
              - variables.tf
              - output.tf

The root folder serves as a cloud agnostic abstract layer, while subfolders under modules, ie modules/azure and modules/google, serve as cloud specific implementation. What you can expect from running root scripts is to provision a cloud server by providing your username and public key, and the return command to set up an SSH tunnel from the output. Use of which provider depends on cloud_service_provider variable, either azure or google from the example.

/variables.tf
variable "cloud_service_provider" { type = string description = "Cloud Service Provider: azure or google" validation { condition = contains(["azure", "google"], var.cloud_service_provider) error_message = "Valid values for var: cloud_service_provider are (azure, google)." } } variable "ssh_username" { type = string description = "username of SSH to the compute engine" } variable "ssh_public_key" { type = string description = "Public key for SSH" } variable "google_project" { type = string default = "no project" description = "Google Cloud Project Name." } locals { # cross variables validation could be improved in Terraform v1.9.0 # tflint-ignore: terraform_unused_declarations validate_project = (var.google_project == "no project" && var.cloud_service_provider == "google") ? tobool( "google_project must be provided when the provider is 'google'.") : true }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/variables.tf

/main.tf is very simple, it enables module to implement cloud proxy per requirement and disable the other:

/main.tf
module "azure_server" { source = "./modules/azure" count = (var.cloud_service_provider == "azure") ? 1 : 0 ssh_public_key = var.ssh_public_key ssh_username = var.ssh_username } module "google_server" { source = "./modules/google" count = (var.cloud_service_provider == "google") ? 1 : 0 ssh_public_key = var.ssh_public_key ssh_username = var.ssh_username }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/main.tf

/output.tf is similar to /main.tf, which returns ip and command as well:

/output.tf
output "ip" { value = (var.cloud_service_provider == "azure") ? module.azure_server[0].ip : module.google_server[0].ip } output "command" { description = "Command to setup ssh tunnel to the proxy server" value = (var.cloud_service_provider == "azure") ? module.azure_server[0].command : module.google_server[0].command }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/output.tf

Providers in terrafrom scripts are removed from modules, and put togather into /provider.tf.

/provider.tf
terraform { required_providers { azapi = { source = "Azure/azapi" } azurerm = { source = "hashicorp/azurerm" } google = { source = "hashicorp/google" } } } provider "azapi" { } provider "azurerm" { features {} } provider "google" { project = var.google_project region = "us-central1" }

https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/provider.tf

Full source code: https://github.com/neoalienson/cloud_vpn_proxy/blob/main/server/

Notification on Proxy Ready

Comming soon…

Price comparision

Comming soon…

VPN with WireGuard

Comming soon…

User friendly on and off

Comming soon…

Reminder to switch off

Comming soon…

Share