A hands-on, beginner-friendly guide to understanding Virtual Private Clouds and building isolated networks on a single Linux machine
Table of Contents
- Introduction: What Problem Are We Solving?
- Real-World Scenario: The Coffee Shop Network
- Understanding the Basics
- How vpcctl Works Under the Hood
- Prerequisites and Installation
- Your First VPC: Step-by-Step Tutorial
- Understanding Every vpcctl Function
- Advanced Scenarios
- Troubleshooting Common Issues
- Complete Code Reference
Introduction: What Problem Are We Solving?
Imagine you're a developer working on a web application. Your app has:
- A web server that customers access
- A database that stores customer data
- An admin panel for internal staff
You want to:
- Isolate your database so random people on the internet can't access it
- Allow your web server to talk to the database
- Control which ports and services are accessible from outside
- Test everything locally before deploying to the cloud
This is exactly what cloud providers like AWS, Azure, and Google Cloud do with their VPC (Virtual Private Cloud) services. But what if you want to:
- Learn how these systems work without spending money?
- Test network configurations on your laptop?
- Understand the underlying Linux networking magic?
That's where vpcctl comes in. It's a tool that simulates cloud-style VPCs on a single Linux computer using native Linux features.
Real-World Scenario: The Coffee Shop Network
Let's use a simple analogy to understand VPCs and subnets.
The Coffee Shop
Imagine you own a coffee shop with different areas:
-
The Customer Area (Public Subnet)
- Customers can walk in freely
- They can order coffee and use WiFi
- They can access the internet
-
The Kitchen (Private Subnet)
- Only staff can enter
- This is where coffee is made
- No direct internet access needed
- But staff need to send orders from the customer area to the kitchen
-
The Office (Another Private Subnet)
- Where you do accounting and management
- Only the owner can access
- Needs internet to process payments
- Completely separate from the kitchen
In networking terms:
- The entire coffee shop is your VPC (Virtual Private Cloud)
- Each area (customer area, kitchen, office) is a subnet
- The staff walking between areas is routing
- The front door that controls who enters is your firewall/security group
- The WiFi router that lets customers access the internet is NAT (Network Address Translation)
Translating to vpcctl
When you create a VPC with vpcctl, you're building a virtual "coffee shop":
# Create the coffee shop (VPC)
sudo vpcctl create coffeeshop --cidr 10.1.0.0/16
# Create the customer area (public subnet)
sudo vpcctl add-subnet coffeeshop customer-area --cidr 10.1.1.0/24
# Create the kitchen (private subnet)
sudo vpcctl add-subnet coffeeshop kitchen --cidr 10.1.2.0/24
# Create the office (private subnet)
sudo vpcctl add-subnet coffeeshop office --cidr 10.1.3.0/24
Now you have isolated areas that you can control independently!
Understanding the Basics
What is a VPC?
VPC stands for Virtual Private Cloud.
Think of it as your own private piece of the internet. In the real world (AWS, Azure), it's a logically isolated section of the cloud where you can launch resources. On your Linux machine with vpcctl, it's a simulated network environment.
Key characteristics:
- Has its own IP address range (CIDR block)
- Everything inside can talk to each other by default
- Isolated from other VPCs
- You control all the networking rules
Example:
sudo vpcctl create myapp --cidr 10.10.0.0/16
This creates a VPC named "myapp" with IP addresses ranging from 10.10.0.1 to 10.10.255.254.
What is a Subnet?
A subnet is a subdivision of your VPC.
If a VPC is a building, subnets are the individual rooms. Each subnet has:
- A smaller IP range (part of the VPC's range)
- A specific purpose (public-facing, private, database, etc.)
- Its own security rules
Types of subnets:
-
Public Subnet
- Can access the internet
- Can be accessed from the internet (with proper rules)
- Example: Web servers, load balancers
-
Private Subnet
- Cannot be directly accessed from the internet
- Can access internet through NAT (if configured)
- Example: Databases, internal services
Example:
# Public subnet for web servers
sudo vpcctl add-subnet myapp web --cidr 10.10.1.0/24
# Private subnet for databases
sudo vpcctl add-subnet myapp database --cidr 10.10.2.0/24
What is NAT?
NAT stands for Network Address Translation.
The Problem: Your private subnet (like the database) might need to download security updates from the internet, but you don't want the internet to directly access your database.
The Solution: NAT acts like a receptionist:
- Your database makes a request to download something
- The NAT gateway forwards that request using its own public IP
- The response comes back to NAT
- NAT forwards it to your database
- From the internet's perspective, everything came from NAT
Think of it like a hotel:
- You're in room 205 (private IP: 10.10.2.5)
- You call reception and ask them to order pizza
- Reception calls the pizza place using the hotel's number (public IP)
- Pizza arrives at reception, they call you to pick it up
- The pizza place never knew your room number
Example:
# Enable internet access for your VPC through NAT
sudo vpcctl enable-nat myapp --interface eth0
What is Peering?
Peering connects two separate VPCs so they can talk to each other.
Real-world scenario: You have two applications:
- VPC 1: Your main web application
- VPC 2: Your analytics service
You want the web app to send data to analytics, but you want to keep them in separate VPCs for security and organization.
Peering creates a private bridge between them.
Example:
# Create two VPCs
sudo vpcctl create webapp --cidr 10.10.0.0/16
sudo vpcctl create analytics --cidr 10.20.0.0/16
# Connect them via peering
sudo vpcctl peer webapp analytics --allow-cidrs 10.10.1.0/24,10.20.1.0/24
Now specific subnets in webapp and analytics can communicate!
How vpcctl Works Under the Hood
vpcctl uses Linux networking primitives. Here's what's really happening:
1. Network Namespaces
What they are: Isolated network environments on Linux. Each namespace has its own network interfaces, routing tables, and firewall rules.
Why we use them: Each subnet you create is actually a network namespace. This provides true isolation—processes in one namespace can't see or access another namespace's network.
Real command:
# vpcctl runs this for you:
sudo ip netns add ns-myapp-web
2. Virtual Ethernet (veth) Pairs
What they are: Like a virtual network cable. Data sent into one end comes out the other.
Why we use them: To connect a namespace (subnet) to the VPC bridge.
Think of it as: A network cable plugged into your computer's network card.
Real command:
# vpcctl creates a veth pair:
sudo ip link add v-myapp-web-a type veth peer name v-myapp-web-b
3. Linux Bridge
What it is: A virtual network switch inside your computer.
Why we use it: Each VPC is a bridge. All subnets (namespaces) in that VPC connect to this bridge, allowing them to communicate.
Think of it as: A network switch in an office that connects all computers.
Real command:
# vpcctl creates a bridge for each VPC:
sudo ip link add br-myapp type bridge
4. iptables
What it is: Linux's firewall system.
Why we use it: To control traffic flow, implement NAT, enforce security policies, and manage peering rules.
Real command:
# NAT rule example:
sudo iptables -t nat -A POSTROUTING -s 10.10.0.0/16 -o eth0 -j MASQUERADE
The Complete Picture
When you run:
sudo vpcctl create myapp --cidr 10.10.0.0/16
sudo vpcctl add-subnet myapp web --cidr 10.10.1.0/24
Here's what happens:
-
VPC Creation:
- Creates a Linux bridge named
br-myapp - Assigns IP
10.10.0.1to the bridge - Creates an iptables chain
vpc-myappfor filtering traffic - Saves metadata to
.vpcctl_data/vpc_myapp.json
- Creates a Linux bridge named
-
Subnet Creation:
- Creates a network namespace
ns-myapp-web - Creates a veth pair: one end in the namespace, one on the bridge
- Assigns IP
10.10.1.2inside the namespace - Sets up routing inside the namespace
- Generates and applies default security policies
- Updates the metadata JSON
- Creates a network namespace
Visual representation:
VPC Peering (VPC-to-VPC Connection):
Prerequisites and Installation
System Requirements
Operating System:
- Linux (Ubuntu 20.04+ or Debian 10+ recommended)
- You can use a virtual machine if you're on Windows or Mac
Required Tools:
- Python 3.6 or higher
- iproute2 (provides
ipcommand) - iptables
- curl (for testing)
Privileges:
- Root access (sudo) is required
Installation Steps
Step 1: Install Required Packages
On Ubuntu/Debian:
sudo apt update
sudo apt install -y python3 iproute2 iptables curl tcpdump bridge-utils
On CentOS/RHEL:
sudo yum install -y python3 iproute iptables curl tcpdump bridge-utils
Step 2: Download vpcctl
Option A: Clone from GitHub
git clone https://github.com/DestinyObs/HNGi13-Stage4-vpcctl/
cd vpcctl
Option B: Download the Single File
curl -O https://github.com/DestinyObs/HNGi13-Stage4-vpcctl/blob/main/vpcctl.py
chmod +x vpcctl.py
Step 3: Install vpcctl as a Command (Recommended)
Install vpcctl so you can use it as a system command:
# Make it executable
chmod +x vpcctl.py
# Create a symlink in your PATH
sudo ln -s "$(pwd)/vpcctl.py" /usr/local/bin/vpcctl
# Verify installation
vpcctl --help
Step 4: Verify Everything Works
Run the parser check (this is safe and makes no system changes):
sudo vpcctl flag-check
What this validates (no system changes):
- Loads the CLI argument parser and the command dispatch table
- Verifies subcommands/flags can be parsed correctly
- Exits immediately without creating namespaces, bridges, or iptables rules
You should see: "Parser check OK"
Your First VPC: Step-by-Step Tutorial
Let's build a realistic setup: a simple web application with a database.
Scenario
You're building a blog platform with:
- A web server (public, accessible from internet)
- A database server (private, not accessible from internet)
- The web server needs to access the database
- The database needs to download updates from the internet (via NAT)
Step 1: Create the VPC
sudo vpcctl create blog --cidr 10.20.0.0/16
Under the hood (what vpcctl runs):
# Create a dedicated virtual switch (bridge) for this VPC
ip link add br-blog type bridge
# Assign the VPC gateway IP to the bridge (first IP of the CIDR)
ip addr add 10.20.0.1/16 dev br-blog
# Bring the bridge up so interfaces can attach and pass traffic
ip link set br-blog up
Explanation:
- The bridge br-blog is the "switch" interconnecting all subnets of the blog VPC
- 10.20.0.1/16 becomes the VPC gateway used by subnets inside the VPC
- Bringing the bridge up activates it so later subnets can attach via veth
What this does:
- Creates a VPC named "blog"
- Assigns IP range 10.20.0.0/16 (65,534 possible addresses)
- Creates a bridge
br-blog
Expected output:
>>> ip link add br-blog type bridge
>>> ip addr add 10.20.0.1/16 dev br-blog
>>> ip link set br-blog up
Created VPC: blog (10.20.0.0/16, bridge: br-blog)
Step 2: Add a Public Subnet for the Web Server
sudo vpcctl add-subnet blog webserver --cidr 10.20.1.0/24
Under the hood, this wires a namespace to the VPC bridge:
# Create an isolated Linux network namespace for the subnet
ip netns add ns-blog-webserver
# Create a veth pair (two virtual Ethernet interfaces back-to-back)
ip link add v-blog-webser-a type veth peer name v-blog-webser-b
# Move one end into the namespace (becomes the subnet's gateway-facing NIC)
ip link set v-blog-webser-a netns ns-blog-webserver
# Attach the other end to the VPC bridge so it can talk to other subnets
ip link set v-blog-webser-b master br-blog
# Inside the namespace, assign IPs and bring interfaces up (simplified)
ip netns exec ns-blog-webserver ip addr add 10.20.1.1/24 dev v-blog-webser-a
ip netns exec ns-blog-webserver ip link set v-blog-webser-a up
ip link set v-blog-webser-b up
Key points:
- veth pair acts like a virtual patch cable between the subnet and the bridge
- Namespace holds the subnet's processes and interfaces isolated from the host
- The .1 address typically serves as the subnet's internal default gateway
What this does:
- Creates a subnet named "webserver" in the blog VPC
- Assigns IP range 10.20.1.0/24 (254 addresses)
- Creates namespace
ns-blog-webserver - Sets up connectivity
Expected output:
>>> ip netns add ns-blog-webserver
>>> ip link add v-blog-webser-a type veth peer name v-blog-webser-b
>>> ip link set v-blog-webser-a netns ns-blog-webserver
>>> ip link set v-blog-webser-b master br-blog
...
Added subnet webserver (10.20.1.0/24) to VPC blog
Step 3: Add a Private Subnet for the Database
sudo vpcctl add-subnet blog database --cidr 10.20.2.0/24
Under the hood, very similar steps repeat with different names/IP:
ip netns add ns-blog-database
ip link add v-blog-datab-a type veth peer name v-blog-datab-b
ip link set v-blog-datab-a netns ns-blog-database
ip link set v-blog-datab-b master br-blog
ip netns exec ns-blog-database ip addr add 10.20.2.1/24 dev v-blog-datab-a
ip netns exec ns-blog-database ip link set v-blog-datab-a up
ip link set v-blog-datab-b up
Difference from the public subnet:
- Same isolation pattern; only policies later will differentiate exposure
- Separate namespace keeps database traffic segregated from web traffic
What this does:
- Creates another subnet for the database
- Separate namespace
ns-blog-database - IP range 10.20.2.0/24
Step 4: Deploy Test Applications
Let's deploy a simple HTTP server in each subnet to test connectivity:
# Deploy web server on port 8080
sudo vpcctl deploy-app blog webserver --port 8080
# Deploy database mock on port 5432
sudo vpcctl deploy-app blog database --port 5432
Under the hood (per deploy-app invocation):
# Execute a simple Python HTTP server inside the target subnet namespace
ip netns exec ns-blog-webserver python3 -m http.server 8080 &
# PID captured and stored in .vpcctl_data/blog/apps/webserver.json
# For the database mock (simplified HTTP listener substitute)
ip netns exec ns-blog-database python3 -m http.server 5432 &
What metadata tracks:
- Namespace name, port, PID, timestamp
- Enables later stop-app and inspect operations
What this does:
- Starts a Python HTTP server inside each namespace
- Web server: accessible on 10.20.1.2:8080
- Database: accessible on 10.20.2.2:5432
Expected output:
Deployed app in subnet webserver (ns-blog-webserver) on port 8080, PID=12345
Step 5: Test Intra-VPC Connectivity
Let's verify that the web server can reach the database (they're in the same VPC):
sudo ip netns exec ns-blog-webserver curl -s http://10.20.2.2:5432
Expected result: You should see HTML output from the Python server running in the database namespace.
What this command does:
-
ip netns exec ns-blog-webserver: Run the following command inside the webserver namespace -
curl http://10.20.2.2:5432: Make an HTTP request to the database
✅ Success! Your web server can talk to your database.
Step 6: Enable Internet Access (NAT)
Right now, neither subnet can access the internet. Let's add NAT:
# Find your host's network interface (usually eth0, enp0s3, or wlan0)
ip route | grep default
# Enable NAT (replace eth0 with your interface)
sudo vpcctl enable-nat blog --interface eth0
Under the hood (simplified):
# Turn on IPv4 forwarding globally (allows routing between namespaces and uplink)
sysctl -w net.ipv4.ip_forward=1
# Masquerade outbound packets from the VPC CIDR so they appear from the host interface
iptables -t nat -A POSTROUTING -s 10.20.0.0/16 -o eth0 -j MASQUERADE -m comment --comment vpcctl-nat-blog
Notes:
- MASQUERADE rewrites source IPs so return traffic finds its way back
- Comment tag lets vpcctl avoid duplicating the rule and clean it up later
What this does:
- Adds iptables MASQUERADE rule
- Enables IP forwarding on the host
- Allows namespaces to access the internet using the host's IP
Step 7: Test Internet Access
From the web server namespace:
sudo ip netns exec ns-blog-webserver curl -s http://10.20.1.2:8080 | head -n 5
Expected result: HTML from example.com
✅ Success! Your subnets can now access the internet.
Step 8: Apply Security Policies
Let's restrict access to the database—block SSH (port 22) but allow HTTP (port 80):
First, create a policy file database_policy.json:
{
"subnet": "10.20.2.0/24",
"ingress": [
{"port": 5432, "protocol": "tcp", "action": "allow"},
{"port": 22, "protocol": "tcp", "action": "deny"}
],
"egress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"}
]
}
Apply the policy:
sudo vpcctl apply-policy blog database_policy.json
Under the hood (rules inside namespace):
# Allow PostgreSQL port
ip netns exec ns-blog-database iptables -A INPUT -p tcp --dport 5432 -j ACCEPT -m comment --comment policy-ingress-allow-5432
# Deny SSH port (placed before broader allows)
ip netns exec ns-blog-database iptables -A INPUT -p tcp --dport 22 -j DROP -m comment --comment policy-ingress-deny-22
# Allow outbound HTTP/HTTPS
ip netns exec ns-blog-database iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT -m comment --comment policy-egress-allow-80
ip netns exec ns-blog-database iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT -m comment --comment policy-egress-allow-443
Mechanics:
- Policy JSON is parsed; each rule becomes a namespaced iptables entry
- Comments let future apply-policy calls update or skip existing rules idempotently
- Separation of INPUT vs OUTPUT enforces ingress vs egress direction cleanly
What this does:
- Adds iptables rules inside the database namespace
- Blocks incoming SSH connections
- Allows incoming connections on port 5432 (PostgreSQL)
- Allows outgoing HTTP/HTTPS
Step 9: List Your VPCs
sudo vpcctl list
Under the hood (simplified):
# Enumerate metadata files and derive VPC names
ls .vpcctl_data | grep -E '^vpc_.*\.json$' | sed 's/^vpc_//; s/\.json$//'
Output:
VPCs:
- blog
Step 10: Inspect VPC Details
sudo vpcctl inspect blog
Under the hood:
# Read and format VPC metadata
cat .vpcctl_data/vpc_blog.json
# vpcctl aggregates and pretty-prints subnets, apps, peers, and policies
Output: Full JSON metadata showing subnets, IP addresses, running apps, and applied policies.
Step 11: Cleanup
When you're done experimenting:
sudo vpcctl delete blog
Under the hood (high-level teardown sequence):
# Stop app processes and remove policy/NAT rules
# Delete namespaces and their veth interfaces
ip netns del ns-blog-webserver || true
ip netns del ns-blog-database || true
# Remove bridge
ip link del br-blog || true
# Remove metadata file
rm -f .vpcctl_data/vpc_blog.json
All operations are idempotent: if something is already gone, vpcctl skips it safely.
What this does:
- Stops all running applications
- Deletes namespaces
- Removes the bridge
- Cleans up iptables rules
- Deletes metadata
Expected output:
Deleted VPC: blog
Understanding Every vpcctl Function
Let's walk through each command in detail.
1. create - Create a New VPC
Purpose: Initialize a new VPC with an IP address range.
Syntax:
sudo vpcctl create <vpc-name> --cidr <ip-range>
Parameters:
-
<vpc-name>: Any alphanumeric name (e.g., "myapp", "production", "test_vpc") -
--cidr: IP address range in CIDR notation (e.g., 10.0.0.0/16)
What happens internally:
- Creates a Linux bridge named
br-<vpc-name> - Assigns the first IP in the range to the bridge (gateway IP)
- Brings the bridge up
- Creates an iptables chain for the VPC
- Saves metadata to
.vpcctl_data/vpc_<name>.json
Examples:
# Development VPC
sudo vpcctl create dev --cidr 10.10.0.0/16
# Production VPC
sudo vpcctl create prod --cidr 10.20.0.0/16
# Testing VPC
sudo vpcctl create test --cidr 192.168.100.0/24
Common CIDR ranges:
-
/16: 65,534 hosts (10.0.0.0/16 → 10.0.0.1 to 10.0.255.254) -
/24: 254 hosts (10.0.1.0/24 → 10.0.1.1 to 10.0.1.254) -
/20: 4,094 hosts
Dry-run mode (preview without changes):
vpcctl --dry-run create dev --cidr 10.10.0.0/16
2. add-subnet - Create a Subnet
Purpose: Add a subnet (network namespace) to an existing VPC.
Syntax:
sudo vpcctl add-subnet <vpc-name> <subnet-name> --cidr <ip-range>
Parameters:
-
<vpc-name>: Name of the VPC -
<subnet-name>: Name for this subnet (e.g., "public", "private", "web", "db") -
--cidr: IP range for this subnet (must be within VPC's range) -
--gw(optional): Gateway IP (defaults to first IP in subnet range)
What happens internally:
- Creates a network namespace
ns-<vpc>-<subnet> - Creates a veth pair (virtual network cable)
- Attaches one end to the namespace, one to the VPC bridge
- Assigns IP addresses
- Sets up routing tables inside the namespace
- Generates default security policy
- Applies the policy
Examples:
# Public subnet for web servers
sudo vpcctl add-subnet myapp web --cidr 10.10.1.0/24
# Private subnet for databases
sudo vpcctl add-subnet myapp database --cidr 10.10.2.0/24
# Private subnet for backend services
sudo vpcctl add-subnet myapp backend --cidr 10.10.3.0/24
# Custom gateway IP
sudo vpcctl add-subnet myapp dmz --cidr 10.10.4.0/24 --gw 10.10.4.254
Subnet naming best practices:
-
public: Internet-facing resources -
private: Internal resources -
databaseordb: Database servers -
backend: Application backend services -
frontend: Frontend services
Default policy applied:
- Ingress: Allow TCP ports 80, 443; Deny TCP port 22
- Egress: No restrictions (allows all outbound)
3. deploy-app - Deploy a Test Application
Purpose: Start a simple Python HTTP server inside a subnet for testing.
Syntax:
sudo vpcctl deploy-app <vpc-name> <subnet-name> --port <port>
Parameters:
-
<vpc-name>: Name of the VPC -
<subnet-name>: Name of the subnet -
--port: Port number for the HTTP server
What happens internally:
- Runs
python3 -m http.server <port>inside the namespace - Uses
nohupto run in background - Records PID (process ID) in metadata
- Saves the command for later cleanup
Examples:
# Web server on standard HTTP port
sudo vpcctl deploy-app myapp web --port 8080
# Database mock on PostgreSQL port
sudo vpcctl deploy-app myapp database --port 5432
# API server
sudo vpcctl deploy-app myapp api --port 3000
Testing the deployed app:
# From another subnet in the same VPC
sudo ip netns exec ns-myapp-web curl http://10.10.2.2:5432
# From the host
curl http://10.10.1.2:8080
Note: This is for testing only. In real scenarios, you'd deploy actual applications (Node.js, Python Flask, PostgreSQL, etc.) inside the namespaces.
4. stop-app - Stop a Running Application
Purpose: Stop an application that was started with deploy-app.
Syntax:
sudo vpcctl stop-app <vpc-name> <subnet-name>
Under the hood (simplified):
# Gracefully stop the test server running inside the subnet namespace
ip netns exec ns-<vpc-name>-<subnet-name> pkill -TERM -f 'http.server' || true
# vpcctl also tracks PIDs in metadata to ensure reliable cleanup
What happens internally:
- Looks up the PID from metadata
- Kills the process
- Removes app entry from metadata
Examples:
sudo vpcctl stop-app myapp web
5. enable-nat - Enable Internet Access
Purpose: Allow subnets to access the internet using the host's IP address (NAT).
Syntax:
sudo vpcctl enable-nat <vpc-name> --interface <host-interface>
Parameters:
-
<vpc-name>: Name of the VPC -
--interface: Host network interface (eth0, enp0s3, wlan0, etc.)
Optional flags:
-
--subnet-filter: Only enable NAT for specific subnets (comma-separated)
What happens internally:
- Enables IP forwarding:
sysctl net.ipv4.ip_forward=1 - Adds MASQUERADE rule:
iptables -t nat -A POSTROUTING -s <vpc-cidr> -o <interface> -j MASQUERADE - Adds FORWARD rules to allow traffic
- Records rules in metadata for cleanup
Examples:
# Enable NAT for entire VPC
sudo vpcctl enable-nat myapp --interface eth0
# Enable NAT only for specific subnets
sudo vpcctl enable-nat myapp --interface eth0 --subnet-filter public,web
Find your host interface:
ip route | grep default
# Output: default via 192.168.1.1 dev eth0
# ^^^^ this is your interface
Common interfaces:
-
eth0: Ethernet -
enp0s3: VirtualBox VM -
wlan0: WiFi -
wlp2s0: WiFi (newer naming)
6. peer - Connect Two VPCs
Purpose: Create a connection between two VPCs so specific subnets can communicate.
Syntax:
sudo vpcctl peer <vpc1-name> <vpc2-name> --allow-cidrs <cidr1>,<cidr2>,...
Under the hood (simplified):
# Create a veth pair to bridge the two VPC bridges (logical link)
ip link add v-<vpc1>-<vpc2>-a type veth peer name v-<vpc1>-<vpc2>-b
ip link set v-<vpc1>-<vpc2>-a master br-<vpc1>
ip link set v-<vpc1>-<vpc2>-b master br-<vpc2>
ip link set v-<vpc1>-<vpc2>-a up
ip link set v-<vpc1>-<vpc2>-b up
# For each allowed CIDR create ACCEPT rules (chain names are namespaced)
iptables -N vpc-peer-<vpc1>-<vpc2> 2>/dev/null || true
iptables -A vpc-peer-<vpc1>-<vpc2> -s <cidr1> -d <cidr2> -j ACCEPT -m comment --comment peer-allow
Parameters:
-
<vpc1-name>,<vpc2-name>: Names of the VPCs to connect -
--allow-cidrs: Comma-separated list of CIDR blocks allowed to communicate
What happens internally:
- Creates a veth pair between the two VPC bridges
- Adds iptables rules to allow traffic for specified CIDRs
- Adds DROP rule for all other traffic (security)
- Records peering info in both VPCs' metadata
Examples:
# Create two VPCs
sudo vpcctl create app1 --cidr 10.10.0.0/16
sudo vpcctl create app2 --cidr 10.20.0.0/16
# Add subnets
sudo vpcctl add-subnet app1 web --cidr 10.10.1.0/24
sudo vpcctl add-subnet app2 api --cidr 10.20.1.0/24
# Peer them (allow only web and api subnets to talk)
sudo vpcctl peer app1 app2 --allow-cidrs 10.10.1.0/24,10.20.1.0/24
Use cases:
- Main app needs to talk to analytics service
- Frontend VPC needs to reach backend VPC
- Staging environment needs to access shared services
Security: Only the CIDRs you specify can communicate. Everything else is blocked.
7. apply-policy - Apply Firewall Rules
Purpose: Apply custom ingress and egress firewall rules to a subnet.
Syntax:
sudo vpcctl apply-policy <vpc-name> <policy-file.json>
Parameters:
-
<vpc-name>: Name of the VPC -
<policy-file.json>: Path to JSON policy file
Policy file format:
{
"subnet": "10.10.1.0/24",
"ingress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"},
{"port": 22, "protocol": "tcp", "action": "deny"}
],
"egress": [
{"port": 25, "protocol": "tcp", "action": "deny"},
{"port": 80, "protocol": "tcp", "action": "allow"}
]
}
What happens internally:
- Reads the JSON policy
- Translates rules to iptables commands
- Executes commands inside the namespace using
ip netns exec - Records policy in metadata
Examples:
Example 1: Web Server (allow HTTP/HTTPS, block SSH)
{
"subnet": "10.10.1.0/24",
"ingress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"},
{"port": 22, "protocol": "tcp", "action": "deny"}
],
"egress": []
}
sudo vpcctl apply-policy myapp web_policy.json
Example 2: Database (allow only PostgreSQL, block everything else)
{
"subnet": "10.10.2.0/24",
"ingress": [
{"port": 5432, "protocol": "tcp", "action": "allow"}
],
"egress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"}
]
}
Actions:
-
allow: Permit traffic -
deny: Block traffic
Protocols:
-
tcp: TCP traffic -
udp: UDP traffic -
icmp: Ping and ICMP
8. list - List All VPCs
Purpose: Show all VPCs you've created.
Syntax:
sudo vpcctl list
Under the hood (simplified):
# Enumerate metadata files and derive VPC names
ls .vpcctl_data | grep -E '^vpc_.*\.json$' | sed 's/^vpc_//; s/\.json$//'
Output example:
VPCs:
- myapp
- production
- test_vpc
9. inspect - View VPC Details
Purpose: Show detailed information about a specific VPC.
Syntax:
sudo vpcctl inspect <vpc-name>
Output: JSON metadata including:
- VPC name and CIDR
- Bridge name
- All subnets with their IPs and namespaces
- Running applications (PIDs)
- Peering connections
- NAT configuration
- Applied iptables rules
Example:
sudo vpcctl inspect myapp
Output:
{
"name": "myapp",
"cidr": "10.10.0.0/16",
"bridge": "br-myapp",
"subnets": [
{
"name": "web",
"cidr": "10.10.1.0/24",
"ns": "ns-myapp-web",
"gw": "10.10.1.1",
"host_ip": "10.10.1.2"
}
],
"apps": [
{
"ns": "ns-myapp-web",
"port": 8080,
"pid": 12345
}
]
}
10. delete - Delete a VPC
Purpose: Completely remove a VPC and all its subnets.
Syntax:
sudo vpcctl delete <vpc-name>
What happens internally:
- Stops all running applications
- Removes all iptables rules (using recorded commands from metadata)
- Deletes all network namespaces
- Removes veth pairs
- Deletes the bridge
- Removes peering connections
- Deletes metadata file
Example:
sudo vpcctl delete myapp
Warning: This is destructive and cannot be undone!
11. cleanup-all - Emergency Cleanup
Purpose: Delete ALL VPCs at once.
Syntax:
sudo vpcctl cleanup-all
Use cases:
- Starting fresh
- Something went wrong and you want to reset
- Clearing test environments
Warning: Deletes everything created by vpcctl!
12. verify - Run System Checks
Purpose: Verify your system has all required tools and permissions.
Syntax:
sudo vpcctl verify
Checks performed:
- Root/sudo access
-
ipcommand availability -
iptablescommand availability -
bridgecommand availability - Existing VPCs and their states
13. test-connectivity - Test Network Connectivity
Purpose: Test if one subnet can reach another.
Syntax:
sudo vpcctl test-connectivity <vpc-name> <source-subnet> <target-ip> --port <port>
Example:
# Test if web subnet can reach database
sudo vpcctl test-connectivity myapp web 10.10.2.2 --port 5432
14. flag-check - Validate CLI Parser
Purpose: Test that the command-line parser works correctly (safe, no system changes).
Syntax:
sudo vpcctl flag-check
Output: "Parser check OK" or error messages
15. --dry-run - Preview Mode (Global Flag)
Purpose: See what commands would be executed without actually running them.
Syntax:
vpcctl --dry-run <any-command>
Examples:
# Preview VPC creation
vpcctl --dry-run create test --cidr 10.99.0.0/16
# Preview subnet addition
vpcctl --dry-run add-subnet test web --cidr 10.99.1.0/24
Use cases:
- Learning what vpcctl does
- Debugging issues
- Verifying commands before execution
Advanced Scenarios
Scenario 1: Multi-Tier Web Application
Build a realistic 3-tier architecture:
# Create VPC
sudo vpcctl create webapp --cidr 10.30.0.0/16
# Tier 1: Load balancer (public)
sudo vpcctl add-subnet webapp loadbalancer --cidr 10.30.1.0/24
# Tier 2: Application servers (private)
sudo vpcctl add-subnet webapp appservers --cidr 10.30.2.0/24
# Tier 3: Database (private)
sudo vpcctl add-subnet webapp database --cidr 10.30.3.0/24
# Deploy apps
sudo vpcctl deploy-app webapp loadbalancer --port 80
sudo vpcctl deploy-app webapp appservers --port 8080
sudo vpcctl deploy-app webapp database --port 5432
# Enable NAT for updates
sudo vpcctl enable-nat webapp --interface eth0
# Apply strict database policy
cat > db_policy.json << 'EOF'
{
"subnet": "10.30.3.0/24",
"ingress": [
{"port": 5432, "protocol": "tcp", "action": "allow"}
],
"egress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"}
]
}
EOF
sudo vpcctl apply-policy webapp db_policy.json
# Test connectivity
# Load balancer → App server
sudo ip netns exec ns-webapp-loadbalancer curl http://10.30.2.2:8080
# App server → Database
sudo ip netns exec ns-webapp-appservers curl http://10.30.3.2:5432
Scenario 2: Microservices with Service Mesh
Simulate a microservices architecture:
# Create VPC for microservices
sudo vpcctl create microservices --cidr 10.40.0.0/16
# API Gateway (public)
sudo vpcctl add-subnet microservices api-gateway --cidr 10.40.1.0/24
# User Service (private)
sudo vpcctl add-subnet microservices user-service --cidr 10.40.2.0/24
# Order Service (private)
sudo vpcctl add-subnet microservices order-service --cidr 10.40.3.0/24
# Payment Service (private)
sudo vpcctl add-subnet microservices payment-service --cidr 10.40.4.0/24
# Shared Database
sudo vpcctl add-subnet microservices shared-db --cidr 10.40.10.0/24
# Deploy services
sudo vpcctl deploy-app microservices api-gateway --port 8080
sudo vpcctl deploy-app microservices user-service --port 8081
sudo vpcctl deploy-app microservices order-service --port 8082
sudo vpcctl deploy-app microservices payment-service --port 8083
sudo vpcctl deploy-app microservices shared-db --port 5432
# Enable internet access
sudo vpcctl enable-nat microservices --interface eth0
# Test inter-service communication
sudo ip netns exec ns-microservices-api-gateway curl http://10.40.2.2:8081
sudo ip netns exec ns-microservices-order-service curl http://10.40.10.2:5432
Scenario 3: Development and Production Isolation with Peering
# Create Development VPC
sudo vpcctl create dev --cidr 10.50.0.0/16
sudo vpcctl add-subnet dev web --cidr 10.50.1.0/24
sudo vpcctl add-subnet dev db --cidr 10.50.2.0/24
# Create Production VPC
sudo vpcctl create prod --cidr 10.60.0.0/16
sudo vpcctl add-subnet prod web --cidr 10.60.1.0/24
sudo vpcctl add-subnet prod db --cidr 10.60.2.0/24
# Create Shared Services VPC (logging, monitoring)
sudo vpcctl create shared --cidr 10.70.0.0/16
sudo vpcctl add-subnet shared logging --cidr 10.70.1.0/24
sudo vpcctl add-subnet shared monitoring --cidr 10.70.2.0/24
# Peer dev and shared (so dev can send logs)
sudo vpcctl peer dev shared --allow-cidrs 10.50.1.0/24,10.70.1.0/24
# Peer prod and shared (so prod can send logs)
sudo vpcctl peer prod shared --allow-cidrs 10.60.1.0/24,10.70.1.0/24
# Note: dev and prod are NOT peered (isolated from each other)
# Deploy apps
sudo vpcctl deploy-app dev web --port 8080
sudo vpcctl deploy-app prod web --port 8080
sudo vpcctl deploy-app shared logging --port 9200
# Test: dev can reach shared logging
sudo ip netns exec ns-dev-web curl http://10.70.1.2:9200
# Test: prod can reach shared logging
sudo ip netns exec ns-prod-web curl http://10.70.1.2:9200
# Verify: dev CANNOT reach prod (should fail)
sudo ip netns exec ns-dev-web curl --max-time 5 http://10.60.1.2:8080 || echo "Correctly blocked!"
Troubleshooting Common Issues
Issue 1: "Permission denied" or "Operation not permitted"
Symptom:
Error: Operation not permitted
Cause: Not running with root privileges.
Solution:
# Always use sudo
sudo vpcctl create myapp --cidr 10.10.0.0/16
Issue 2: "Cannot find command: ip"
Symptom:
Error: Cannot find required command: ip
Cause: Missing iproute2 package.
Solution:
# Ubuntu/Debian
sudo apt install -y iproute2
# CentOS/RHEL
sudo yum install -y iproute
Issue 3: "Bridge already exists"
Symptom:
Error: RTNETLINK answers: File exists
Cause: You already created a VPC with this name.
Solutions:
Option A: Use a different name
sudo vpcctl create myapp2 --cidr 10.10.0.0/16
Option B: Delete the existing VPC first
sudo vpcctl delete myapp
sudo vpcctl create myapp --cidr 10.10.0.0/16
Issue 4: Namespace Cannot Access Internet
Symptom:
sudo ip netns exec ns-myapp-web curl http://google.com
# Hangs or fails
Possible causes and solutions:
Cause 1: NAT not enabled
sudo vpcctl enable-nat myapp --interface eth0
Cause 2: Wrong host interface
# Find correct interface
ip route | grep default
# Use the interface shown
sudo vpcctl enable-nat myapp --interface <correct-interface>
Cause 3: Firewall blocking
# Check iptables
sudo iptables -t nat -L -n -v
# Check for MASQUERADE rule
sudo iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE
Cause 4: DNS not configured in namespace
# Copy resolv.conf into namespace
sudo mkdir -p /etc/netns/ns-myapp-web
sudo cp /etc/resolv.conf /etc/netns/ns-myapp-web/
# Test again
sudo ip netns exec ns-myapp-web curl http://google.com
Issue 5: Subnets Cannot Communicate
Symptom:
# From subnet A trying to reach subnet B
sudo ip netns exec ns-myapp-web curl http://10.10.2.2:5432
# Connection refused or timeout
Debugging steps:
Step 1: Verify both subnets are in the same VPC
sudo vpcctl inspect myapp
# Check that both subnets appear in the output
Step 2: Check if application is running in target subnet
sudo vpcctl inspect myapp | grep apps
# Or
sudo ip netns exec ns-myapp-database netstat -tlnp
Step 3: Verify routing
# Check routing table in source namespace
sudo ip netns exec ns-myapp-web ip route
Step 4: Check firewall rules
# Check iptables in target namespace
sudo ip netns exec ns-myapp-database iptables -L -n -v
Step 5: Test basic connectivity (ping)
sudo ip netns exec ns-myapp-web ping -c 3 10.10.2.2
Issue 6: "Cannot delete VPC: bridge is busy"
Symptom:
Error: RTNETLINK answers: Device or resource busy
Cause: Something is still using the bridge (running app, existing veth).
Solution:
# Force stop all apps first
sudo vpcctl cleanup-all
# Or manually kill processes
sudo pkill -f "ip netns exec ns-myapp"
# Then try deleting again
sudo vpcctl delete myapp
Issue 7: Peered VPCs Cannot Communicate
Symptom:
sudo vpcctl peer vpc1 vpc2 --allow-cidrs 10.10.1.0/24,10.20.1.0/24
# But curl between VPCs times out
Debugging steps:
Step 1: Verify peering exists
sudo vpcctl inspect vpc1 | grep peers
sudo vpcctl inspect vpc2 | grep peers
Step 2: Check allowed CIDRs
# Make sure the source and destination subnets are in the allowed list
sudo vpcctl inspect vpc1
Step 3: Check iptables rules
sudo iptables -L vpc-vpc1 -n -v
# Look for ACCEPT rules with peer CIDRs
Step 4: Test with specific IPs
# From vpc1 subnet to vpc2 subnet
sudo ip netns exec ns-vpc1-web curl http://10.20.1.2:8080
Issue 8: High CPU Usage from Python HTTP Server
Symptom: System becomes slow after deploying multiple apps.
Cause: Python's simple HTTP server is not designed for high performance.
Solution:
Option A: Limit number of test apps
# Only deploy what you need
sudo vpcctl stop-app myapp web
Option B: Use lighter alternatives
Instead of using deploy-app, manually run a lighter server:
# Use busybox httpd (if available)
sudo ip netns exec ns-myapp-web busybox httpd -f -p 8080
Issue 9: "Address already in use"
Symptom:
Error: bind: address already in use
Cause: Port is already taken by another application.
Solution:
Option 1: Use a different port
sudo vpcctl deploy-app myapp web --port 8081
Option 2: Stop the conflicting app
# Find what's using the port
sudo lsof -i :8080
# Stop it
sudo kill <PID>
# Or use vpcctl
sudo vpcctl stop-app myapp web
Issue 10: Metadata File Corruption
Symptom:
Error: JSON decode error
Cause: Metadata file .vpcctl_data/vpc_<name>.json got corrupted.
Solution:
Option 1: Delete and recreate
# Remove corrupted metadata
rm .vpcctl_data/vpc_myapp.json
# Recreate VPC
sudo vpcctl create myapp --cidr 10.10.0.0/16
Option 2: Manual cleanup
# Remove namespace
sudo ip netns del ns-myapp-web
# Remove bridge
sudo ip link del br-myapp
# Remove metadata
rm .vpcctl_data/vpc_myapp.json
Debugging Tips
Enable verbose output:
# Add debugging to see all iptables operations
sudo vpcctl create myapp --cidr 10.10.0.0/16 2>&1 | tee debug.log
Check system logs:
# View kernel network messages
dmesg | tail -50
# System logs
journalctl -xe
Verify bridge state:
# List all bridges
ip link show type bridge
# Show bridge details
bridge link show
Verify namespace state:
# List all namespaces
sudo ip netns list
# Show interfaces in a namespace
sudo ip netns exec ns-myapp-web ip addr
Verify iptables rules:
# NAT table
sudo iptables -t nat -L -n -v
# Filter table
sudo iptables -L -n -v
# Check specific chain
sudo iptables -L vpc-myapp -n -v
Complete Code Reference
Metadata File Structure
Every VPC has a JSON metadata file stored in .vpcctl_data/vpc_<name>.json:
{
"name": "myapp",
"cidr": "10.10.0.0/16",
"bridge": "br-myapp",
"chain": "vpc-myapp",
"subnets": [
{
"name": "web",
"cidr": "10.10.1.0/24",
"ns": "ns-myapp-web",
"gw": "10.10.1.1",
"host_ip": "10.10.1.2",
"veth": "v-myapp-web"
}
],
"host_iptables": [
["iptables", "-A", "vpc-myapp", "-s", "10.10.0.0/16", "-d", "10.10.0.0/16", "-j", "ACCEPT"]
],
"apps": [
{
"ns": "ns-myapp-web",
"port": 8080,
"pid": 12345,
"cmd": ["ip", "netns", "exec", "ns-myapp-web", "python3", "-m", "http.server", "8080"]
}
],
"peers": [
{
"peer_vpc": "otherapp",
"veth_a": "pv-myapp-other-va",
"veth_b": "pv-myapp-other-vb",
"allowed": ["10.10.1.0/24", "10.20.1.0/24"]
}
],
"nat": {
"interface": "eth0"
}
}
Policy File Structure
Policy files define ingress and egress firewall rules:
{
"subnet": "10.10.1.0/24",
"ingress": [
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"},
{"port": 22, "protocol": "tcp", "action": "deny"},
{"port": 3389, "protocol": "tcp", "action": "deny"}
],
"egress": [
{"port": 25, "protocol": "tcp", "action": "deny"},
{"port": 80, "protocol": "tcp", "action": "allow"},
{"port": 443, "protocol": "tcp", "action": "allow"}
]
}
Fields:
-
subnet: CIDR of the subnet this policy applies to -
ingress: Rules for incoming traffic -
egress: Rules for outgoing traffic -
port: Port number -
protocol:tcp,udp, oricmp -
action:allowordeny
Common IP Address Ranges (CIDR)
Private IP ranges (safe to use):
-
10.0.0.0/8: 10.0.0.0 to 10.255.255.255 (16 million IPs) -
172.16.0.0/12: 172.16.0.0 to 172.31.255.255 (1 million IPs) -
192.168.0.0/16: 192.168.0.0 to 192.168.255.255 (65,534 IPs)
CIDR notation explained:
-
/8: 16,777,216 addresses -
/16: 65,536 addresses -
/24: 256 addresses -
/28: 16 addresses -
/32: 1 address
Examples:
-
10.0.0.0/16= 10.0.0.1 to 10.0.255.254 -
192.168.1.0/24= 192.168.1.1 to 192.168.1.254 -
172.16.0.0/20= 172.16.0.1 to 172.16.15.254
Quick Command Cheat Sheet
# Create VPC
sudo vpcctl create <name> --cidr <ip-range>
# Add subnet
sudo vpcctl add-subnet <vpc> <subnet-name> --cidr <ip-range>
# Deploy test app
sudo vpcctl deploy-app <vpc> <subnet> --port <port>
# Enable internet
sudo vpcctl enable-nat <vpc> --interface <iface>
# Peer VPCs
sudo vpcctl peer <vpc1> <vpc2> --allow-cidrs <cidr1>,<cidr2>
# Apply policy
sudo vpcctl apply-policy <vpc> <policy-file.json>
# List VPCs
sudo vpcctl list
# Inspect VPC
sudo vpcctl inspect <vpc>
# Delete VPC
sudo vpcctl delete <vpc>
# Test connectivity
sudo ip netns exec ns-<vpc>-<subnet> curl http://<ip>:<port>
# Run command in namespace
sudo ip netns exec ns-<vpc>-<subnet> <command>
# Check namespace IPs
sudo ip netns exec ns-<vpc>-<subnet> ip addr
# Check namespace routing
sudo ip netns exec ns-<vpc>-<subnet> ip route
# Check namespace firewall
sudo ip netns exec ns-<vpc>-<subnet> iptables -L -n -v
Learning Resources
Understanding Linux Networking
Concepts to research:
- Network namespaces
- Virtual Ethernet (veth) pairs
- Linux bridges
- iptables and netfilter
- IP routing and forwarding
- NAT and MASQUERADE
Conclusion
You now have a complete understanding of:
- What VPCs and subnets are
- Why we need network isolation
- How vpcctl works under the hood
- Every command and function in vpcctl
- How to build realistic network architectures
- How to troubleshoot common issues
Next steps:
- Follow the "Your First VPC" tutorial
- Build one of the advanced scenarios
- Create your own custom network topology
- Read the vpcctl source code to go deeper
Remember: vpcctl is a learning tool. It helps you understand cloud networking concepts without needing an AWS/Azure account. Once you master it, you'll find cloud provider VPCs much easier to work with!
Destiny's Note: This guide was written to be beginner-friendly. If you're reading this and something is unclear, that's a bug in the documentation, not in your understanding. Re-read the section, try the examples, and experiment!
Happy networking!
I'm DestinyObs, iBuild | iDeploy | iSecure | iSustain!


Top comments (0)