Skip to main content
  1. Posts/

Build Your Own Virtual Private Cloud (VPC) on Linux

·9 mins
Table of Contents

Ever wondered how Amazon, Google, or Microsoft assigns unique public IP addresses when using their services? Or you ever wanted to simulate your own mini Google Cloud!?!?! with subnets and custom IP addresses and CIDR and veth pairs and publicly accessible web servers in those subnets and like the entire internet?? all without typing 20+ commands? Because let’s face it, that’s some Houdini level stuff.

|480x304

Meet vpcctl. A CLI tool built exactly for that! VPC creation? it’s got it. Subnets, say less. Firewall? BETβ€”all with single commands!

No more 30 line bash scripts, or memorizing of iptables syntaxes.

Ready to build your personal cloud datacenter?

Getting Started

Prerequisites

  • Linux host
  • Root access or sudo privileges
  • Dependencies: ip, iptables, jq, brctl, nginx

Why Should I Run as Root?

vpcctl configures and manipulates operations that require root privileges (network namespaces and iptables). While you can run commands with sudo, running as root is recommended for best results.

Quick Start

  • Clone the repo

    git clone https://github.com/Clue-ess-coder/vpcctl ~/.vpcctl
    
  • Add vpcctl binary to PATH

    echo 'export PATH="$HOME/.vpcctl/bin:$PATH"' >> ~/.bashrc
    source ~/.bashrc
    

Verify Installation

# should show a beautiful help menu
vpcctl

(See walkthrough for a demo setup)

Wahh! It doesn’t work:

  • Run source ~/.bashrc or restart your shell
  • Ensure you’re root or using sudo

(See Troubleshooting for more.)

Design Philosophy

The tool is designed to use as little flags as possible (only 1 actually) to reduce the overhead needed to run commands. The syntax can take a little getting used to, but it ensures users think systematically about each intended action.

It also uses a configuration file (config.json) which acts as the single source of truth for all operations. This allows preview of all configuration/changes anytime (See Configuration).

High-Level Architecture

Architecture Diagram

CLI Usage

When in doubt, simply run vpcctl (with or without --help) and the beautiful help page below should populate your terminal.

VPC Management

Run the following commands to create, remove, or list all VPCs.

vpcctl create vpc <name> <cidr>
vpcctl rm vpc <name>
vpcctl ls vpcs

Example:

vpcctl create vpc vpc1 10.10.0.0/16
vpcctl rm vpc vpc1
vpcctl ls vpcs

Subnet Management

Run these to create, remove, and list subnets in a VPC:

vpcctl create subnet <vpc> <subnet> <cidr> <type>
vpcctl rm subnet <vpc> <subnet>
vpcctl ls subnets <vpc>

Example:

vpcctl create subnet vpc1 pub 10.10.1.0/24 public
vpcctl create subnet vpc1 priv 10.10.2.0/24 private

vpcctl ls subnets vpc1

vpcctl rm subnet vpc1 pub
vpcctl rm subnet vpc1 priv

Peering

VPCs are isolated by default. Enable peering to allow traffic flow between them.

vpcctl create peering <vpc1> <vpc2>
vpcctl rm peering <vpc1> <vpc2>

Firewall

Apply robust firewall rules via JSON formatted policy files.

# Subnet level
vpcctl firewall <vpc> <subnet> <policy-file.json>
vpcctl firewall show <vpc> <subnet>
vpcctl firewall remove <vpc> <subnet>

# VPC level
vpcctl firewall vpc <vpc> <policy-file.json>
vpcctl firewall vpc remove <vpc>

Sample policy file:

{
  "mode": "permissive",
  "enable_logging": false,
  "ingress": [
    {
      "protocol": "icmp",
      "action": "allow",
      "comment": "Allow all ICMP (ping)"
    }
  ],
  "egress": [
    {
      "port": 443,
      "protocol": "tcp",
      "action": "deny",
      "comment": "Block outbound HTTPS"
    },
    {
      "port": 22,
      "protocol": "tcp",
      "action": "deny",
      "comment": "Block outbound SSH"
    }
  ]
}

Find example configurations here

Deployment and Testing

Deploy and run tests on simple Nginx or Python servers natively in any subnet.

vpcctl deploy <vpc> <subnet> <port>
vpcctl get-ip <vpc> <subnet> [service]
vpcctl run <vpc> <subnet> <command>
vpcctl test <vpc> <subnet> <ip> <port> <protocol>

Examples:

## deploys nginx server in public subnet on port 8080
vpcctl deploy nginx vpc1 pub 8080 [--ip] 10.0.1.35

## pings google.com from public subnet in vpc1
vpcctl run vpc1 pub ping -c 4 8.8.8.8

## tcp/ping/icmp test on server listening on port 8080 in public subnet
vpcctl test vpc1 pub 10.0.1.10 8080 [tcp|ping|icmp]

Configuration and Logs

Show, set or get config details from a single config.json file.

vpcctl config get <path>
vpcctl config set <path> <value>
vpcctl config show

checks logs when needed:

vpcctl show-logs

Examples:

## prints all vpc
vpcctl config get '.vpcs'

## prints all subnets in vpc1
vpcctl config get '.vpcs["vpc1"].subnets'

## set automatic ip allocation to start from .20
vpcctl config set '.settings.ip_allocation.start_offset' 20

## print entire config
vpcctl config show

Demo Walkthrough

  1. Modify environment variables:

    cd ~/.vpcctl
    mv .env.example .env
    
    # edit variables
    vim .env
    ---
    VPC_NAME=vpc1
    CIDR_BLOCK=10.0.0.0/16
    PUBLIC_SUBNET=10.0.1.0/24
    PRIVATE_SUBNET=10.0.2.0/24
    PUBLIC_SUBNET_NAME=pub
    PRIVATE_SUBNET_NAME=priv
    INTERNET_INTERFACE=eth0
    
  2. Create VPC:

    > vpcctl create vpc [vpc1 10.0.0.0/16]      # optional manual input
    
    [2025-11-11 17:30:24] [INFO] Creating VPC: vpc1 with CIDR 10.0.0.0/16
    [2025-11-11 17:30:24] [INFO] Creating bridge: br-vpc1
    [2025-11-11 17:30:24] [INFO] Assigning gateway IP: 10.0.0.1/16
    [2025-11-11 17:30:24] [INFO] VPC vpc1 created successfully
    
  3. Create a public subnet

    # from environment variables
    > vpcctl create subnet "" "" "" public
    
    [2025-11-11 20:29:14] [INFO] Creating subnet: pub in VPC vpc1
    [2025-11-11 20:29:14] [INFO] Creating namespace: ns-vpc1-pub
    [2025-11-11 20:29:14] [INFO] Creating veth pair: veth-pub-h <-> veth-pub-ns
    [2025-11-11 20:29:14] [INFO] Attaching veth-pub-h to bridge br-vpc1
    [2025-11-11 20:29:14] [INFO] Moving veth-pub-ns into namespace ns-vpc1-pub
    [2025-11-11 20:29:14] [INFO] Configuring interface in namespace
    [2025-11-11 20:29:14] [INFO] Adding route to VPC CIDR 10.0.0.0/16
    [2025-11-11 20:29:14] [INFO] Adding default route
    [2025-11-11 20:29:14] [INFO] Enabling NAT for public subnet
    [2025-11-11 20:29:14] [INFO] Enabling NAT for 10.0.1.0/24 via eth0
    [2025-11-11 20:29:14] [INFO] NAT enabled for 10.0.1.0/24
    [2025-11-11 20:29:14] [INFO] Adding route from host to public subnet 10.0.1.0/24
    [2025-11-11 20:29:14] [INFO] Subnet pub created successfully
    
  4. Create a private subnet

    > vpcctl create subnet "" "" "" private
    
    [2025-11-11 23:19:06] [INFO] Creating subnet: priv in VPC vpc1
    [2025-11-11 23:19:06] [INFO] Creating namespace: ns-vpc1-priv
    [2025-11-11 23:19:06] [INFO] Creating veth pair: veth-priv-h <-> veth-priv-ns
    [2025-11-11 23:19:06] [INFO] Attaching veth-priv-h to bridge br-vpc1
    [2025-11-11 23:19:06] [INFO] Moving veth-priv-ns into namespace ns-vpc1-priv
    [2025-11-11 23:19:06] [INFO] Configuring interface in namespace
    [2025-11-11 23:19:06] [INFO] Adding route to VPC CIDR 10.0.0.0/16
    [2025-11-11 23:19:06] [INFO] Adding default route
    [2025-11-11 23:19:06] [INFO] Blocking host access to private subnet 10.0.2.0/24
    [2025-11-11 23:19:07] [INFO] Host access to private subnet 10.0.2.0/24 is blocked
    [2025-11-11 23:19:07] [INFO] Subnet priv created successfully
    
  5. Deploy nginx servers in subnets

    # optional custom IP address flag
    > vpcctl deploy nginx vpc1 pub 8080 [--ip] 10.0.1.35
    
    # ... deployment logs ...
    # Test:
    #    curl http://10.0.1.35
    
    > vpcctl deploy nginx vpc1 priv 8081     # automatic ip allocation (.10+)
    
    # ...more logs...
    # Test:
    #    curl http://10.0.2.10:8081
    
  6. Verify accessibility of server in subnets from host

    #### Public subnet
    > curl http://10.0.1.35
    
    # should return:
    VPC Test Server
    
    VPC: vpc1
    Subnet: pub
    IP: 10.0.1.35
    Port: 8080
    Server: nginx-1762903786
    
    Nginx is serving this response!
    
    ### Private subnet
    curl http://10.0.2.10:8081
    
    # ...NO RESPONSE (blocked as expected)
    
  7. Test communication within VPCs from either subnets

    #### public -> private
    > vpcctl test vpc1 pub 10.0.2.10 8081 tcp
    
    [2025-11-11 23:53:42] [INFO] Testing connectivity from pub to 10.0.2.10:8081 (tcp)
    [2025-11-11 23:53:42] [INFO] TCP connection to 10.0.2.10:8081 successful
    
    #### private -> public
    > vpcctl test vpc1 priv 10.0.1.35 8080 ping
    
    [2025-11-11 23:57:24] [INFO] Testing connectivity from priv to 10.0.1.35:8080 (ping)
    [2025-11-11 23:57:26] [INFO] ICMP ping to 10.0.1.35 successful
    
  8. Test outbound access from subnets

    #### ping google
    > vpcctl run vpc1 pub ping -c 3 8.8.8.8
    
    [2025-11-12 00:03:11] [INFO] Running command in subnet pub (namespace: ns-vpc1-pub)
    [2025-11-12 00:03:11] [INFO] Command: ping -c 3 8.8.8.8
    PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=61.8 ms
    64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=88.0 ms
    64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=160 ms
    
    --- 8.8.8.8 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2003ms
    rtt min/avg/max/mdev = 61.782/103.233/159.892/41.471 ms
    
    
    ## Private subnet (blocked)
    > vpcctl run vpc1 priv ping -c 3 8.8.8.8
    
    [2025-11-12 00:07:08] [INFO] Running command in subnet priv (namespace: ns-vpc1-priv)
    [2025-11-12 00:07:08] [INFO] Command: ping -c 3 8.8.8.8
    PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    
    --- 8.8.8.8 ping statistics ---
    3 packets transmitted, 0 received, 100% packet loss, time 2025ms
    
  9. Now run through these steps once more for another VPC and establish peering with the previously created VPC by running:

    vpcctl create peering vpc1 vpc2
    
  10. Run more ping, tcp and curl tests to verify services can now talk to each other:

    ## TCP test from public subnet in vpc1 to an nginx server at 192.168.1.11:8080 (which could exist in vpc2 VPC and pub2 subnet under that VPC)
    vpcctl test vpc1 pub 192.168.1.11 8080 tcp
    
  11. Run ./test-firewall.sh for a robust firewall test

  12. Run individual cleanups with the remove|rm commands for each section (VPCs, subnets, peering, and firewall), or run a robust cleanup operation with vpcctl-cleanup

  13. And if you mistakenly ran the wrong commands or broke something (which you might do a few times), simply checks the logs in .vpcctl/logs/

Sequence Diagrams

VPC and Subnet Creation

VPC and Subnet Creation

Service in Subnet β†’ Internet (firewall enabled)

Subnet to Internet

Inter-subnet Communication

Inter-subnet Comms

VPC Peering

VPC Peering

VPC Deletion

VPC Deletion

Firewall Application

sequenceDiagram
    participant User
    participant vpcctl
    participant iptables
    participant namespace

    User ->> vpcctl: firewall my-vpc public policy-config.json
    iptables -->> vpcctl: validate JSON
parse policy iptables -->> vpcctl: validate rules
(ports, protocols) vpcctl ->> namespace: flush existing rules in namespace namespace -->> iptables: iptables -F INPUT/OUTPUT vpcctl ->> namespace: set default policy namespace -->> iptables: iptables -P INPUT/OUTPUT/FORWARD DROP
(or ACCEPT if permissive) vpcctl ->> namespace: add stateful tracking namespace -->> iptables: iptables -I INPUT 1 -m conntrack... vpcctl ->> namespace: apply ingress, egress, forward rules namespace -->> iptables: rules applied vpcctl ->> namespace: add logging
(if enabled) namespace -->> iptables: iptables -A INPUT -j LOG iptables -->> vpcctl: save policy to config vpcctl -->> User: firewall applied

Troubleshooting

“Command Not found”

# reload bashrc
source ~/.bashrc

“Permission denied”

# are you root?
$ whoami
---
root
# or use sudo
sudo vpcctl ls vpcs

“NAT Not Working or No Internet in Public subnet”

# check if IP forwarding is enabled
$ sysctl net.ipv4.ip_forward
---
# output should be
net.ipv4.ip_forward = 1

“Firewall Rules not Blocking traffic”

# check if rules were applied
vpcctl firewall show vpc1 pub

# enable logging in policy file
{"enable_logging": true}

# tail logs
sudo journalctl -f | grep FW-DROP

“Resources Still Exist”

# force cleanup of all namespaces
ip netns list | grep <vpc-name> | xargs -I {} ip netns delete {}

# remove all bridges
ip link delete br-<vpc-name>
# run cleanup again
vpcctl-cleanup
---
[2025-11-11 11:43:56] Starting vpcctl cleanup...
[2025-11-11 11:43:56] Stopping all running servers...
[2025-11-11 11:43:56]   Killing process: 9907
[2025-11-11 11:43:56] Removing nginx server directories...
[2025-11-11 11:43:56]   Removed nginx directories from /tmp
[2025-11-11 11:43:56] Removing nginx server files...
[2025-11-11 11:43:56]   Removed nginx log files from /tmp
[2025-11-11 11:43:56] Cleaning up NAT rules...
[2025-11-11 11:43:56]   Removing NAT rule for 10.0.1.0/24
[2025-11-11 11:43:56] Removing peering connections...
[2025-11-11 11:43:56] Removing all network namespaces...
[2025-11-11 11:43:56]   Deleting namespace: ns-vpc1-pub
[2025-11-11 11:43:56]   Deleting namespace: ns-vpc1-priv
[2025-11-11 11:43:56] Removing any orphaned veth interfaces...
[2025-11-11 11:43:56]   Deleting orphaned veth: veth-priv-h@if124
[2025-11-11 11:43:56] Removing all VPC bridges...
[2025-11-11 11:43:56]   Deleting bridge: br-vpc1
[2025-11-11 11:43:56] Removing any orphaned bridges...
[2025-11-11 11:43:56] Resetting config file...
[2025-11-11 11:43:56] Running final cleanup checks...
[2025-11-11 11:43:56] Cleanup complete!
Reply by Email