Initial commit

This commit is contained in:
2026-02-21 16:54:18 +01:00
commit 0cf33cd95b
80 changed files with 8042 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"ansible.python.interpreterPath": "/usr/bin/python"
}

269
GETTING_STARTED.md Normal file
View File

@@ -0,0 +1,269 @@
# Getting Started with WireGuard Lab Overlay
## Prerequisites
### On Management Server
```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y \
terraform \
ansible \
python3-pip \
git \
docker.io \
docker-compose
# Install Ansible collections
ansible-galaxy collection install community.routeros
ansible-galaxy collection install ansible.netcommon
# Install Python packages
pip3 install librouteros prometheus-client
```
### On Linux Gateways
```bash
# Ensure WireGuard is available
sudo apt-get install -y wireguard wireguard-tools bridge-utils python3
```
### On MikroTik Routers
- RouterOS 7.6 or higher
- API access enabled
- Admin credentials
## Initial Setup
### Step 1: Configure Your Lab Networks
1. Copy the example configuration:
```bash
cd terraform
cp terraform.tfvars.example terraform.tfvars
```
2. Edit `terraform.tfvars` with your lab networks:
```hcl
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "simple" # or "failover"
road_warrior = false # Enable later
gateways = [
{
hostname = "lab100-gw1"
type = "linux"
mgmt_ip = "192.168.1.11" # Change to your IPs
api_port = 22
lab_if = "eth1" # Interface facing lab devices
dhcp_role = "primary"
},
{
hostname = "lab100-gw2"
type = "linux"
mgmt_ip = "192.168.1.12"
api_port = 22
lab_if = "eth1"
dhcp_role = "secondary"
}
]
}
}
```
### Step 2: Set Up Ansible
1. Configure SSH access to your gateways:
```bash
ssh-copy-id admin@192.168.1.11
ssh-copy-id admin@192.168.1.12
```
2. For MikroTik devices, create vault password:
```bash
cd ansible
echo "your-vault-password" > .vault_pass
chmod 600 .vault_pass
```
3. Encrypt MikroTik credentials:
```bash
ansible-vault create inventory/group_vars/mikrotik_gateways.yml
```
Add:
```yaml
ansible_password: your-mikrotik-password
```
### Step 3: Deploy
#### Option A: Automated (Recommended)
```bash
./deploy.sh
```
#### Option B: Manual Steps
```bash
# 1. Initialize and apply Terraform
cd terraform
terraform init
terraform apply
terraform output -json > outputs.json
# 2. Generate Ansible inventory
terraform output ansible_inventory > ../ansible/inventory/hosts.yml
# 3. Deploy to gateways
cd ../ansible
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# 4. Start monitoring
cd ../monitoring
docker-compose up -d
```
### Step 4: Verify Deployment
```bash
# Check gateway health
cd ansible
ansible-playbook playbooks/health-check.yml
# Check WireGuard tunnels
ansible linux_gateways -m shell -a "wg show"
# Check bridges
ansible linux_gateways -m shell -a "ip link show type bridge"
```
## Common Tasks
### Add Road Warrior Access
1. Edit `terraform/terraform.tfvars`:
```hcl
lab_networks = {
lab100 = {
road_warrior = true # Enable this
# ...
}
}
road_warrior_users = {
"jdoe" = {
email = "john.doe@company.com"
labs = ["lab100"]
}
}
```
2. Apply changes:
```bash
cd terraform
terraform apply
cd ../ansible
ansible-playbook playbooks/generate-road-warrior.yml
```
3. Distribute configs:
```bash
ls ../road-warrior/generated/jdoe/
# lab100.conf - WireGuard config file
# lab100-qr.html - QR code for mobile devices
```
### Add New Lab Network
```bash
ansible-playbook playbooks/add-lab-network.yml
```
Follow the prompts to configure the new lab.
### Emergency Shutdown
```bash
ansible-playbook playbooks/emergency-shutdown.yml
```
### Restore Lab
```bash
ansible-playbook playbooks/restore-lab.yml
```
## Monitoring
Access the monitoring stack:
- **Grafana**: http://your-server:3000
- Username: `admin`
- Password: `admin`
- **Prometheus**: http://your-server:9090
## Troubleshooting
### WireGuard tunnel not coming up
```bash
# On gateway:
sudo wg show
sudo journalctl -u wg-quick@wg-lab100 -n 50
# Check connectivity
ping <peer-wireguard-ip>
```
### DHCP not working
```bash
# Check DHCP server
sudo systemctl status dnsmasq
# or for failover mode:
sudo systemctl status isc-dhcp-server
# Check leases
sudo tail -f /var/lib/misc/dnsmasq.lab100.leases
```
### Can't reach lab devices
1. Check bridge is up:
```bash
ip link show br-lab100
```
2. Check VXLAN interface:
```bash
ip -d link show vxlan100
```
3. Check FDB entries:
```bash
bridge fdb show br br-lab100
```
## Next Steps
- Read `docs/architecture.md` for detailed architecture
- Read `docs/operations.md` for operational procedures
- Set up alerting in Grafana
- Configure backup for Terraform state
- Implement proper secret management (HashiCorp Vault)
## Support
For issues:
1. Check logs: `journalctl -u wg-quick@wg-lab*`
2. Run health check: `ansible-playbook playbooks/health-check.yml`
3. Review documentation in `docs/`

153
OPENWRT_CHANGELOG.md Normal file
View File

@@ -0,0 +1,153 @@
# OpenWrt Integration - Change Log
## What's New
### OpenWrt Gateway Support
This version adds complete support for OpenWrt routers as lab network gateways, making them fully interoperable with Linux VMs and MikroTik routers.
### New Ansible Roles
1. **openwrt-common** - System preparation and package installation
2. **openwrt-wireguard** - UCI-based WireGuard configuration
3. **openwrt-vxlan** - VXLAN interface setup with FDB management
4. **openwrt-bridge** - Bridge configuration via UCI
5. **openwrt-dhcp** - dnsmasq DHCP server configuration
6. **openwrt-firewall** - Automatic firewall zone creation and isolation
7. **openwrt-monitoring** - Lightweight shell-based Prometheus exporter
### Key Features
- **UCI Configuration**: All settings managed via OpenWrt's UCI system
- **Auto Package Installation**: Automatic installation of required packages:
- wireguard-tools
- kmod-wireguard
- kmod-vxlan
- ip-full
- tcpdump
- **Firewall Isolation**: Each lab network gets isolated firewall zone
- **Metrics Export**: Lightweight shell script exporter on port 9586
- **Full Mesh Support**: OpenWrt gateways mesh with Linux and MikroTik
- **DHCP Modes**: Support for both simple (split range) and failover simulation
### Architecture Support
- **x86_64**: PC Engines APU, Protectli Vault, generic x86
- **ARM**: GL.iNet, Turris Omnia, Raspberry Pi
- **MIPS**: Various TP-Link, Ubiquiti models (device-dependent)
### Updated Files
**Terraform:**
- `terraform/outputs.tf` - Added openwrt_gateways inventory group
- `terraform/terraform.tfvars.example` - Added OpenWrt gateway examples
**Ansible:**
- `ansible/playbooks/site.yml` - Added OpenWrt configuration block
- `ansible/inventory/hosts.yml` - Added openwrt_gateways group
- `ansible/inventory/group_vars/openwrt_gateways.yml` - OpenWrt-specific vars
**Documentation:**
- `docs/openwrt-setup.md` - Complete OpenWrt setup guide
- `docs/gateway-comparison.md` - Comparison of all three gateway types
- `README.md` - Updated with OpenWrt references
### Compatibility
- **OpenWrt Version**: 23.05 or later (stable releases)
- **Minimum Hardware**: 128MB RAM, 128MB flash (256MB+ recommended)
- **Tested Platforms**: x86_64, ARM (Cortex-A series)
- **Interoperability**: Fully compatible with Linux and MikroTik gateways
### Usage Example
```hcl
# terraform/terraform.tfvars
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "simple"
road_warrior = false
gateways = [
{
hostname = "lab100-linux1"
type = "linux"
mgmt_ip = "192.168.1.11"
api_port = 22
lab_if = "eth1"
dhcp_role = "primary"
},
{
hostname = "lab100-openwrt1"
type = "openwrt" # ← OpenWrt gateway
mgmt_ip = "192.168.1.14"
api_port = 22
lab_if = "eth1" # Adjust to your OpenWrt interface
dhcp_role = "secondary"
}
]
}
}
```
Deploy:
```bash
./deploy.sh
```
### Testing Checklist
After deploying an OpenWrt gateway:
- [ ] WireGuard tunnel established: `wg show`
- [ ] VXLAN interface up: `ip -d link show vxlan100`
- [ ] Bridge operational: `bridge link show`
- [ ] DHCP serving: `cat /tmp/dhcp.leases`
- [ ] Firewall zone created: `uci show firewall | grep lab100`
- [ ] Metrics exported: `curl http://<openwrt-ip>:9586/metrics/`
- [ ] Can ping peer WireGuard IPs
- [ ] Lab devices getting DHCP addresses
### Known Limitations
1. **DHCP Failover**: OpenWrt uses simulated failover (split ranges), not true ISC DHCP failover protocol
2. **Resource Constraints**: Performance varies significantly based on hardware
3. **Package Availability**: Some older/exotic architectures may lack VXLAN support
4. **Web UI**: No LuCI integration (CLI/Ansible only)
### Migration Notes
If upgrading from a version without OpenWrt support:
1. Update Terraform: `terraform init -upgrade`
2. Run Terraform plan to see new inventory structure
3. No changes needed for existing Linux/MikroTik gateways
4. Add OpenWrt gateways incrementally
### Performance Expectations
| Hardware Class | WireGuard Throughput | Concurrent Devices |
|----------------|---------------------|-------------------|
| x86_64 (modern) | 500 Mbps - 2 Gbps | 50-100+ |
| ARM (Cortex-A) | 100-300 Mbps | 20-50 |
| MIPS (consumer) | 50-150 Mbps | 10-20 |
### Support
See:
- `docs/openwrt-setup.md` for detailed setup instructions
- `docs/gateway-comparison.md` for choosing gateway types
- `docs/operations.md` for troubleshooting
## Breaking Changes
None - fully backward compatible with existing deployments.
## Next Features (Future)
- LuCI web UI integration (optional)
- Support for OpenWrt snapshot builds
- WiFi AP mode integration for wireless lab devices
- Advanced QoS policies for lab networks

160
README.md Normal file
View File

@@ -0,0 +1,160 @@
# WireGuard Lab Overlay Network
Automated deployment of WireGuard-based overlay networks for lab environments with support for Linux and MikroTik gateways.
## Features
- **Multi-Vendor Support**: Linux VMs, MikroTik routers, and OpenWrt devices
- **Layer 2 Overlay**: VXLAN over WireGuard for L2 connectivity
- **DHCP Options**: ISC DHCP with failover OR dnsmasq with split ranges
- **Road Warrior Access**: Built-in support for mobile/laptop clients
- **Monitoring**: Prometheus exporters and Grafana dashboards
- **Firewall Isolation**: Automatic firewall zones for OpenWrt gateways
- **Full Automation**: Terraform + Ansible deployment
## Architecture
- **WireGuard**: Encrypted mesh tunnels between gateways
- **VXLAN**: Layer 2 extension over WireGuard
- **Linux Bridge/MikroTik Bridge**: Local device attachment
- **DHCP**: Redundant DHCP servers per lab network
- **Monitoring Hub**: Centralized Prometheus + Grafana
## Quick Start
### 1. Prerequisites
```bash
# Install required tools
apt-get install terraform ansible python3-pip
# Install Ansible collections
ansible-galaxy collection install community.routeros
ansible-galaxy collection install ansible.netcommon
# Install Python dependencies
pip3 install librouteros prometheus-client
```
### 2. Configure Lab Networks
Edit `terraform/terraform.tfvars`:
```hcl
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "failover"
road_warrior = true
gateways = [
# Define your gateways here
]
}
}
```
### 3. Deploy
```bash
# Run deployment script
./deploy.sh
```
Or manual steps:
```bash
# Initialize Terraform
cd terraform
terraform init
terraform apply
terraform output -json > outputs.json
# Deploy to gateways
cd ../ansible
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# Generate road warrior configs
ansible-playbook -i inventory/hosts.yml playbooks/generate-road-warrior.yml
# Start monitoring
cd ../monitoring
docker-compose up -d
```
## Operational Playbooks
### Rolling Update
```bash
ansible-playbook playbooks/rolling-update.yml
```
### Health Check
```bash
ansible-playbook playbooks/health-check.yml
```
### Emergency Shutdown
```bash
ansible-playbook playbooks/emergency-shutdown.yml
```
### Restore Lab
```bash
ansible-playbook playbooks/restore-lab.yml
```
### Add Gateway
```bash
ansible-playbook playbooks/add-gateway.yml
```
### Remove Gateway
```bash
ansible-playbook playbooks/remove-gateway.yml
```
## Directory Structure
```
wireguard-lab-overlay/
├── terraform/ # Infrastructure state
├── ansible/ # Configuration management
│ ├── roles/ # Ansible roles (Linux & MikroTik)
│ ├── playbooks/ # Operational playbooks
│ └── inventory/ # Inventory and variables
├── monitoring/ # Monitoring stack
│ ├── prometheus/
│ ├── grafana/
│ └── docker-compose.yml
├── road-warrior/ # Client configurations
└── docs/ # Documentation
```
## Monitoring
Access monitoring:
- Grafana: http://monitoring-hub:3000 (admin/admin)
- Prometheus: http://monitoring-hub:9090
## Security Notes
- WireGuard private keys are stored in Terraform state (use remote backend with encryption)
- Consider using HashiCorp Vault or AWS Secrets Manager for production
- Ansible Vault is used for MikroTik credentials
- Road warrior configs should be distributed securely
## Support
For issues and questions, refer to:
- `docs/architecture.md` - Detailed architecture
- `docs/deployment.md` - Deployment guide
- `docs/operations.md` - Operations manual
- `docs/mikrotik-setup.md` - MikroTik specific guide
- `docs/openwrt-setup.md` - OpenWrt specific guide
- `docs/gateway-comparison.md` - Gateway type comparison
## License
Internal use only - Company proprietary

20
ansible/ansible.cfg Normal file
View File

@@ -0,0 +1,20 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
host_key_checking = False
vault_password_file = .vault_pass
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

View File

@@ -0,0 +1,7 @@
---
terraform_output_file: "{{ playbook_dir }}/../../terraform/outputs.json"
wireguard_persistent_keepalive: 25
vxlan_ttl: 64
vxlan_port: 4789
exporter_port: 9586
monitoring_hub_ip: 192.168.1.100

View File

@@ -0,0 +1,28 @@
---
# Linux-specific settings
# Sysctl settings
sysctl_settings:
net.ipv4.ip_forward: 1
net.bridge.bridge-nf-call-iptables: 0
net.bridge.bridge-nf-call-ip6tables: 0
net.ipv4.conf.all.rp_filter: 0
net.ipv4.conf.default.rp_filter: 0
# WireGuard
wireguard_config_dir: /etc/wireguard
# Bridge settings
bridge_stp: no
bridge_forward_delay: 0
# ISC DHCP failover settings
isc_dhcp_failover:
max_response_delay: 30
max_unacked_updates: 10
mclt: 3600
split: 128
load_balance_max_seconds: 3
# Monitoring
exporter_install_dir: /opt/wg-lab-exporter

View File

@@ -0,0 +1,33 @@
---
# OpenWrt-specific settings
# SSH connection
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
# OpenWrt package requirements
openwrt_required_packages:
- wireguard-tools
- kmod-wireguard
- kmod-vxlan
- ip-full
- tcpdump
# UCI settings
uci_commit_delay: 2
# Network reload wait time
network_reload_wait: 10
# Firewall zone settings
firewall_default_input: REJECT
firewall_default_forward: REJECT
firewall_default_output: ACCEPT
# Monitoring
exporter_port: 9586
exporter_path: /usr/lib/wg-lab-exporter
# Logging
syslog_enabled: yes
log_level: info

View File

@@ -0,0 +1,24 @@
all:
vars:
ansible_user: admin
ansible_python_interpreter: /usr/bin/python3
children:
lab_gateways:
children:
linux_gateways:
vars:
ansible_become: yes
mikrotik_gateways:
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: community.routeros.routeros
openwrt_gateways:
vars:
ansible_user: root
ansible_become: no
monitoring_hub:
hosts:
monitoring-hub.example.com:
ansible_host: 192.168.1.100

View File

@@ -0,0 +1,64 @@
---
- name: Add Gateway to Existing Lab Network
hosts: localhost
gather_facts: no
vars_prompt:
- name: lab_name
prompt: "Lab network name (e.g., lab100)"
private: no
- name: gateway_hostname
prompt: "New gateway hostname"
private: no
tasks:
- name: Load Terraform outputs
set_fact:
tf_config: "{{ lookup('file', terraform_output_file) | from_json }}"
- name: Verify gateway exists in Terraform
fail:
msg: "Gateway {{ gateway_hostname }} not found. Add to terraform.tfvars first."
when: gateway_hostname not in tf_config.gateway_config.value.keys()
- name: Display next steps
debug:
msg:
- "Gateway {{ gateway_hostname }} found in configuration"
- "Deploying to gateway..."
- name: Deploy to new gateway
hosts: "{{ gateway_hostname }}"
become: yes
tasks:
- name: Load gateway config
set_fact:
gateway_config: "{{ hostvars['localhost']['tf_config'].gateway_config.value[inventory_hostname] }}"
- name: Deploy based on gateway type
include_role:
name: "{{ item }}"
loop: "{{ roles_to_deploy }}"
vars:
roles_to_deploy: "{{ ['openwrt-common', 'openwrt-wireguard', 'openwrt-vxlan', 'openwrt-bridge', 'openwrt-dhcp', 'openwrt-firewall', 'openwrt-monitoring'] if gateway_config.type == 'openwrt' else (['mikrotik-common', 'mikrotik-wireguard', 'mikrotik-vxlan', 'mikrotik-bridge', 'mikrotik-dhcp', 'mikrotik-monitoring'] if gateway_config.type == 'mikrotik' else ['common', 'wireguard', 'vxlan', 'bridge', ('isc-dhcp' if gateway_config.dhcp_mode == 'failover' else 'dnsmasq'), 'monitoring']) }}"
- name: Update existing gateways with new peer
hosts: lab_gateways
become: yes
tasks:
- name: Skip if not in same lab
meta: end_host
when: gateway_config.lab_name != hostvars['localhost']['lab_name']
- name: Skip new gateway itself
meta: end_host
when: inventory_hostname == hostvars['localhost']['gateway_hostname']
- name: Reload WireGuard configuration
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
state: reloaded
when: gateway_config.type == 'linux'

View File

@@ -0,0 +1,30 @@
---
- name: Emergency Shutdown of Lab Network
hosts: localhost
gather_facts: no
vars_prompt:
- name: lab_name
prompt: "Which lab network to shutdown? (e.g., lab100, or 'all')"
private: no
- name: confirm_shutdown
prompt: "CONFIRM: Type 'YES' to proceed with emergency shutdown"
private: no
pre_tasks:
- name: Validate confirmation
fail:
msg: "Shutdown cancelled"
when: confirm_shutdown != "YES"
- name: Execute shutdown on Linux gateways
hosts: linux_gateways
become: yes
tasks:
- name: Stop WireGuard
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
state: stopped
ignore_errors: yes

View File

@@ -0,0 +1,40 @@
---
- name: Generate Road Warrior Configurations
hosts: localhost
gather_facts: yes
tasks:
- name: Load Terraform outputs
set_fact:
tf_config: "{{ lookup('file', terraform_output_file) | from_json }}"
- name: Create output directories
file:
path: "{{ rw_config_output_dir }}/{{ item.value.user_name }}"
state: directory
mode: '0755'
loop: "{{ tf_config.road_warrior_configs.value | dict2items }}"
loop_control:
label: "{{ item.value.user_name }}"
- name: Generate client configs
template:
src: ../road-warrior/templates/client-config.conf.j2
dest: "{{ rw_config_output_dir }}/{{ item.value.user_name }}/{{ item.value.lab_name }}.conf"
mode: '0600'
loop: "{{ tf_config.road_warrior_configs.value | dict2items }}"
loop_control:
label: "{{ item.value.user_name }}"
- name: Generate README
template:
src: ../road-warrior/templates/README.md.j2
dest: "{{ rw_config_output_dir }}/{{ item.key }}/README.md"
mode: '0644'
loop: "{{ tf_config.road_warrior_configs.value | dict2items | groupby('value.user_name') | list }}"
loop_control:
label: "{{ item.0 }}"
- name: Display output location
debug:
msg: "Road warrior configs generated in {{ rw_config_output_dir }}"

View File

@@ -0,0 +1,14 @@
---
- name: Comprehensive Health Check
hosts: lab_gateways
become: yes
tasks:
- name: Check WireGuard status
shell: "wg show"
register: wg_status
when: ansible_os_family == "Debian"
- name: Display results
debug:
var: wg_status.stdout_lines

View File

@@ -0,0 +1,35 @@
---
- name: Remove Gateway from Lab Network
hosts: localhost
gather_facts: no
vars_prompt:
- name: gateway_hostname
prompt: "Gateway hostname to remove"
private: no
- name: confirm_removal
prompt: "Type 'REMOVE' to confirm"
private: no
pre_tasks:
- name: Validate confirmation
fail:
msg: "Removal cancelled"
when: confirm_removal != "REMOVE"
- name: Shutdown gateway
hosts: "{{ gateway_hostname }}"
become: yes
tasks:
- name: Stop all services
systemd:
name: "{{ item }}"
state: stopped
loop:
- "wg-quick@wg-{{ gateway_config.lab_name }}"
- "vxlan-{{ gateway_config.lab_name }}"
- "bridge-{{ gateway_config.lab_name }}"
when: gateway_config.type == 'linux'
ignore_errors: yes

View File

@@ -0,0 +1,12 @@
---
- name: Restore Lab Network
hosts: linux_gateways
become: yes
serial: 1
tasks:
- name: Start WireGuard
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
state: started
when: ansible_os_family == "Debian"

View File

@@ -0,0 +1,37 @@
---
- name: Rolling Update of WireGuard Lab Gateways
hosts: lab_gateways
serial: 1
become: yes
vars:
health_check_retries: 10
health_check_delay: 10
pre_tasks:
- name: Load Terraform outputs
set_fact:
tf_config: "{{ lookup('file', terraform_output_file) | from_json }}"
delegate_to: localhost
run_once: yes
- name: Extract gateway-specific config
set_fact:
gateway_config: "{{ tf_config.gateway_config.value[inventory_hostname] }}"
tasks:
- name: Update system packages (Linux)
apt:
update_cache: yes
upgrade: dist
when: gateway_config.type == "linux"
- name: Restart WireGuard
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
state: restarted
when: gateway_config.type == "linux"
- name: Wait and validate
pause:
seconds: 30

View File

@@ -0,0 +1,90 @@
---
- name: Deploy WireGuard Lab Overlay Network
hosts: lab_gateways
become: yes
gather_facts: yes
pre_tasks:
- name: Load Terraform outputs
set_fact:
tf_config: "{{ lookup('file', '../../terraform/outputs.json') | from_json }}"
delegate_to: localhost
run_once: yes
- name: Extract gateway-specific config
set_fact:
gateway_config: "{{ tf_config.gateway_config.value[inventory_hostname] }}"
- name: Display gateway type
debug:
msg: "Configuring {{ inventory_hostname }} ({{ gateway_config.type }}) for {{ gateway_config.lab_name }}"
tasks:
- name: Configure Linux gateways
block:
- include_role:
name: common
- include_role:
name: wireguard
- include_role:
name: vxlan
- include_role:
name: bridge
- include_role:
name: "{{ 'isc-dhcp' if gateway_config.dhcp_mode == 'failover' else 'dnsmasq' }}"
- include_role:
name: monitoring
when: gateway_config.type == 'linux'
- name: Configure MikroTik gateways
block:
- include_role:
name: mikrotik-common
- include_role:
name: mikrotik-wireguard
- include_role:
name: mikrotik-vxlan
- include_role:
name: mikrotik-bridge
- include_role:
name: mikrotik-dhcp
- include_role:
name: mikrotik-monitoring
when: gateway_config.type == 'mikrotik'
- name: Configure OpenWrt gateways
block:
- include_role:
name: openwrt-common
- include_role:
name: openwrt-wireguard
- include_role:
name: openwrt-vxlan
- include_role:
name: openwrt-bridge
- include_role:
name: openwrt-dhcp
- include_role:
name: openwrt-firewall
- include_role:
name: openwrt-monitoring
when: gateway_config.type == 'openwrt'
post_tasks:
- name: Deployment summary
debug:
msg:
- "Gateway: {{ inventory_hostname }}"
- "Type: {{ gateway_config.type }}"
- "Lab: {{ gateway_config.lab_name }}"
- "VNI: {{ gateway_config.vni }}"
- "WireGuard IP: {{ gateway_config.wg_ip }}"
- "Subnet: {{ gateway_config.subnet }}"
- "DHCP Mode: {{ gateway_config.dhcp_mode }}"
- "DHCP Role: {{ gateway_config.dhcp_role }}"
- name: Configure Monitoring Hub
hosts: monitoring_hub
become: yes
roles:
- monitoring_hub

View File

@@ -0,0 +1,18 @@
---
- name: Create bridge setup script
template:
src: setup-bridge.sh.j2
dest: /usr/local/bin/setup-bridge-{{ gateway_config.lab_name }}.sh
mode: '0755'
- name: Create systemd service for bridge
template:
src: bridge.service.j2
dest: /etc/systemd/system/bridge-{{ gateway_config.lab_name }}.service
notify: reload systemd
- name: Enable and start bridge service
systemd:
name: bridge-{{ gateway_config.lab_name }}
enabled: yes
state: started

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Bridge for {{ gateway_config.lab_name }}
After=vxlan-{{ gateway_config.lab_name }}.service
Requires=vxlan-{{ gateway_config.lab_name }}.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/setup-bridge-{{ gateway_config.lab_name }}.sh
ExecStop=/sbin/ip link delete br-{{ gateway_config.lab_name }}
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,38 @@
#!/bin/bash
set -e
LAB_NAME="{{ gateway_config.lab_name }}"
VNI="{{ gateway_config.vni }}"
BR_IF="br-${LAB_NAME}"
VXLAN_IF="vxlan${VNI}"
LAB_IF="{{ gateway_config.lab_if }}"
# Wait for VXLAN interface
timeout=30
while [ $timeout -gt 0 ]; do
if ip link show ${VXLAN_IF} >/dev/null 2>&1; then
break
fi
sleep 1
timeout=$((timeout-1))
done
# Create bridge
if ! ip link show ${BR_IF} &>/dev/null; then
ip link add ${BR_IF} type bridge
ip link set ${BR_IF} type bridge stp_state {{ bridge_stp | ternary('1', '0') }}
ip link set ${BR_IF} type bridge forward_delay {{ bridge_forward_delay }}
fi
# Add interfaces to bridge
ip link set ${VXLAN_IF} master ${BR_IF}
ip link set ${LAB_IF} master ${BR_IF}
# Configure bridge IP (for DHCP server)
ip addr flush dev ${BR_IF}
ip addr add {{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }}/{{ gateway_config.subnet | ipaddr('prefix') }} dev ${BR_IF}
# Bring everything up
ip link set ${LAB_IF} up
ip link set ${VXLAN_IF} up
ip link set ${BR_IF} up

View File

@@ -0,0 +1,26 @@
---
- name: Install required packages
apt:
name:
- wireguard
- wireguard-tools
- bridge-utils
- python3-prometheus-client
- vlan
state: present
update_cache: yes
- name: Configure sysctl settings
sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
reload: yes
loop: "{{ sysctl_settings | dict2items }}"
- name: Ensure systemd-networkd is disabled
systemd:
name: systemd-networkd
enabled: no
state: stopped
ignore_errors: yes

View File

@@ -0,0 +1,5 @@
---
- name: restart dnsmasq
systemd:
name: dnsmasq
state: restarted

View File

@@ -0,0 +1,18 @@
---
- name: Install dnsmasq
apt:
name: dnsmasq
state: present
- name: Create dnsmasq configuration
template:
src: dnsmasq.conf.j2
dest: /etc/dnsmasq.d/{{ gateway_config.lab_name }}.conf
mode: '0644'
notify: restart dnsmasq
- name: Enable and start dnsmasq
systemd:
name: dnsmasq
enabled: yes
state: started

View File

@@ -0,0 +1,28 @@
# {{ gateway_config.lab_name }} DHCP configuration
interface=br-{{ gateway_config.lab_name }}
bind-interfaces
{% if gateway_config.dhcp_role == 'primary' %}
# Primary DHCP server
dhcp-range={{ gateway_config.subnet | ipaddr('10') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('127') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('netmask') }},12h
{% elif gateway_config.dhcp_role == 'secondary' %}
# Secondary DHCP server (split range)
dhcp-range={{ gateway_config.subnet | ipaddr('128') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('245') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('netmask') }},12h
{% else %}
# Backup server (only responds if others fail)
dhcp-range={{ gateway_config.subnet | ipaddr('10') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('245') | ipaddr('address') }},{{ gateway_config.subnet | ipaddr('netmask') }},12h
{% endif %}
dhcp-option=option:router,{{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }}
dhcp-option=option:dns-server,{{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }}
# Local DNS domain
domain={{ gateway_config.lab_name }}.lab
local=/{{ gateway_config.lab_name }}.lab/
# Lease file
dhcp-leasefile=/var/lib/misc/dnsmasq.{{ gateway_config.lab_name }}.leases
# Logging
log-dhcp
log-queries

View File

@@ -0,0 +1,5 @@
---
- name: restart isc-dhcp
systemd:
name: isc-dhcp-server
state: restarted

View File

@@ -0,0 +1,48 @@
---
- name: Install ISC DHCP server
apt:
name: isc-dhcp-server
state: present
- name: Stop and disable dnsmasq (conflicts with ISC DHCP)
systemd:
name: dnsmasq
state: stopped
enabled: no
ignore_errors: yes
- name: Configure ISC DHCP server defaults
template:
src: isc-dhcp-server.j2
dest: /etc/default/isc-dhcp-server
mode: '0644'
notify: restart isc-dhcp
- name: Get failover peer IP
set_fact:
failover_peer_ip: "{{ hostvars[item].gateway_config.wg_ip }}"
loop: "{{ groups['lab_gateways'] }}"
when:
- hostvars[item].gateway_config.lab_name == gateway_config.lab_name
- hostvars[item].gateway_config.dhcp_role in ['primary', 'secondary']
- hostvars[item].gateway_config.dhcp_role != gateway_config.dhcp_role
register: peer_search
- name: Set failover peer IP from search
set_fact:
failover_peer_ip: "{{ peer_search.results | selectattr('ansible_facts', 'defined') | map(attribute='ansible_facts.failover_peer_ip') | first }}"
when: peer_search.results | selectattr('ansible_facts', 'defined') | list | length > 0
- name: Configure DHCP failover
template:
src: dhcpd.conf.j2
dest: /etc/dhcp/dhcpd.conf
mode: '0644'
validate: 'dhcpd -t -cf %s'
notify: restart isc-dhcp
- name: Ensure ISC DHCP is enabled and started
systemd:
name: isc-dhcp-server
enabled: yes
state: started

View File

@@ -0,0 +1,56 @@
# ISC DHCP Configuration for {{ gateway_config.lab_name }}
# Generated by Ansible
authoritative;
ddns-update-style none;
default-lease-time 43200; # 12 hours
max-lease-time 86400; # 24 hours
# Failover configuration
{% if gateway_config.dhcp_role == 'primary' %}
failover peer "{{ gateway_config.lab_name }}-failover" {
primary;
address {{ gateway_config.wg_ip }};
port 647;
peer address {{ failover_peer_ip }};
peer port 647;
max-response-delay {{ isc_dhcp_failover.max_response_delay }};
max-unacked-updates {{ isc_dhcp_failover.max_unacked_updates }};
mclt {{ isc_dhcp_failover.mclt }};
split {{ isc_dhcp_failover.split }};
load balance max seconds {{ isc_dhcp_failover.load_balance_max_seconds }};
}
{% elif gateway_config.dhcp_role == 'secondary' %}
failover peer "{{ gateway_config.lab_name }}-failover" {
secondary;
address {{ gateway_config.wg_ip }};
port 647;
peer address {{ failover_peer_ip }};
peer port 647;
max-response-delay {{ isc_dhcp_failover.max_response_delay }};
max-unacked-updates {{ isc_dhcp_failover.max_unacked_updates }};
load balance max seconds {{ isc_dhcp_failover.load_balance_max_seconds }};
}
{% endif %}
# Subnet declaration
subnet {{ gateway_config.subnet | ipaddr('network') }} netmask {{ gateway_config.subnet | ipaddr('netmask') }} {
option routers {{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }};
option domain-name-servers {{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }};
option domain-name "{{ gateway_config.lab_name }}.lab";
{% if gateway_config.dhcp_role in ['primary', 'secondary'] %}
pool {
failover peer "{{ gateway_config.lab_name }}-failover";
range {{ gateway_config.subnet | ipaddr('10') | ipaddr('address') }} {{ gateway_config.subnet | ipaddr('245') | ipaddr('address') }};
}
{% else %}
# Backup server - only serves when primary/secondary unavailable
pool {
range {{ gateway_config.subnet | ipaddr('10') | ipaddr('address') }} {{ gateway_config.subnet | ipaddr('245') | ipaddr('address') }};
}
{% endif %}
}
# Logging
log-facility local7;

View File

@@ -0,0 +1,10 @@
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf).
DHCPDv4_CONF=/etc/dhcp/dhcpd.conf
# Path to dhcpd's PID file (default: /var/run/dhcpd.pid).
DHCPDv4_PID=/var/run/dhcpd.pid
# On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
INTERFACESv4="br-{{ gateway_config.lab_name }}"

View File

@@ -0,0 +1,16 @@
---
- name: Create bridge
community.routeros.command:
commands:
- /interface bridge add name="bridge-{{ gateway_config.lab_name }}" protocol-mode=none
- name: Add interfaces to bridge
community.routeros.command:
commands:
- /interface bridge port add bridge="bridge-{{ gateway_config.lab_name }}" interface="vxlan{{ gateway_config.vni }}"
- /interface bridge port add bridge="bridge-{{ gateway_config.lab_name }}" interface="{{ gateway_config.lab_if }}"
- name: Configure bridge IP
community.routeros.command:
commands:
- /ip address add address={{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }}/{{ gateway_config.subnet | ipaddr('prefix') }} interface="bridge-{{ gateway_config.lab_name }}"

View File

@@ -0,0 +1,16 @@
---
- name: Check RouterOS version
community.routeros.command:
commands:
- /system resource print
register: ros_version
- name: Set system identity
community.routeros.command:
commands:
- /system identity set name={{ inventory_hostname }}
- name: Configure NTP client
community.routeros.command:
commands:
- /system ntp client set enabled=yes servers=pool.ntp.org

View File

@@ -0,0 +1,18 @@
---
- name: Create DHCP pool
community.routeros.command:
commands:
- /ip pool add name="pool-{{ gateway_config.lab_name }}" ranges={{ dhcp_start }}-{{ dhcp_end }}
vars:
dhcp_start: "{{ gateway_config.subnet | ipaddr('10') | ipaddr('address') if gateway_config.dhcp_role == 'primary' else gateway_config.subnet | ipaddr('128') | ipaddr('address') }}"
dhcp_end: "{{ gateway_config.subnet | ipaddr('127') | ipaddr('address') if gateway_config.dhcp_role == 'primary' else gateway_config.subnet | ipaddr('245') | ipaddr('address') }}"
- name: Configure DHCP network
community.routeros.command:
commands:
- /ip dhcp-server network add address={{ gateway_config.subnet }} gateway={{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }} dns-server={{ gateway_config.subnet | ipaddr('1') | ipaddr('address') }}
- name: Create DHCP server
community.routeros.command:
commands:
- /ip dhcp-server add name="dhcp-{{ gateway_config.lab_name }}" interface="bridge-{{ gateway_config.lab_name }}" address-pool="pool-{{ gateway_config.lab_name }}" lease-time=12h disabled=no

View File

@@ -0,0 +1,11 @@
---
- name: Enable REST API
community.routeros.command:
commands:
- /ip service set api-ssl disabled=no port=8729
- name: Create monitoring user
community.routeros.command:
commands:
- /user add name=monitoring group=read comment="Prometheus monitoring"
ignore_errors: yes

View File

@@ -0,0 +1,13 @@
---
- name: Create VXLAN interface
community.routeros.command:
commands:
- /interface vxlan add name="vxlan{{ gateway_config.vni }}" vni={{ gateway_config.vni }} interface="wg-{{ gateway_config.lab_name }}" port=4789
- name: Configure VXLAN vteps
community.routeros.command:
commands:
- /interface vxlan vteps add interface="vxlan{{ gateway_config.vni }}" remote-ip={{ peer.wg_ip }}
loop: "{{ gateway_config.peers }}"
loop_control:
loop_var: peer

View File

@@ -0,0 +1,20 @@
---
- name: Remove existing WireGuard interface
community.routeros.command:
commands:
- /interface wireguard remove [find name="wg-{{ gateway_config.lab_name }}"]
ignore_errors: yes
- name: Create WireGuard interface
community.routeros.command:
commands:
- /interface wireguard add name="wg-{{ gateway_config.lab_name }}" listen-port={{ gateway_config.wg_listen_port }} private-key="{{ gateway_config.wg_private_key }}"
- /ip address add address={{ gateway_config.wg_ip }}/{{ gateway_config.wg_net | ipaddr("prefix") }} interface="wg-{{ gateway_config.lab_name }}"
- name: Configure WireGuard peers
community.routeros.command:
commands:
- /interface wireguard peers add interface="wg-{{ gateway_config.lab_name }}" public-key="{{ peer.public_key }}" endpoint-address={{ peer.endpoint }} endpoint-port={{ gateway_config.wg_listen_port }} allowed-address={{ peer.wg_ip }}/32 persistent-keepalive=25s
loop: "{{ gateway_config.peers }}"
loop_control:
loop_var: peer

View File

@@ -0,0 +1,24 @@
---
- name: Create exporter directory
file:
path: "{{ exporter_install_dir }}"
state: directory
mode: '0755'
- name: Deploy exporter configuration
template:
src: exporter-config.json.j2
dest: /etc/wg-lab-exporter/config.json
mode: '0644'
- name: Create systemd service
template:
src: wg-lab-exporter.service.j2
dest: /etc/systemd/system/wg-lab-exporter.service
- name: Enable and start exporter
systemd:
name: wg-lab-exporter
enabled: yes
state: started
daemon_reload: yes

View File

@@ -0,0 +1,8 @@
{
"labs": {
"{{ gateway_config.lab_name }}": {
"vni": {{ gateway_config.vni }},
"lab_if": "{{ gateway_config.lab_if }}"
}
}
}

View File

@@ -0,0 +1,11 @@
[Unit]
Description=WireGuard Lab Exporter
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/wg-lab-exporter/wg_lab_exporter.py
Restart=always
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,63 @@
---
- name: Set bridge interface name
set_fact:
br_if: "br_{{ gateway_config.lab_name }}"
vxlan_if: "vxlan{{ gateway_config.vni }}"
lab_if: "{{ gateway_config.lab_if }}"
- name: Remove existing bridge configuration
shell: |
uci delete network.{{ br_if }} 2>/dev/null || true
uci delete network.{{ br_if }}_dev 2>/dev/null || true
uci commit network
changed_when: true
- name: Create bridge device
shell: |
uci set network.{{ br_if }}_dev='device'
uci set network.{{ br_if }}_dev.name='{{ br_if }}'
uci set network.{{ br_if }}_dev.type='bridge'
uci set network.{{ br_if }}_dev.stp='0'
uci add_list network.{{ br_if }}_dev.ports='{{ vxlan_if }}'
uci add_list network.{{ br_if }}_dev.ports='{{ lab_if }}'
uci commit network
changed_when: true
- name: Create bridge interface
shell: |
uci set network.{{ br_if }}='interface'
uci set network.{{ br_if }}.proto='static'
uci set network.{{ br_if }}.device='{{ br_if }}'
uci set network.{{ br_if }}.ipaddr='{{ gateway_config.subnet | ipaddr("1") | ipaddr("address") }}'
uci set network.{{ br_if }}.netmask='{{ gateway_config.subnet | ipaddr("netmask") }}'
uci commit network
changed_when: true
- name: Reload network configuration
shell: /etc/init.d/network reload
changed_when: true
- name: Wait for bridge to come up
shell: ip link show {{ br_if }}
register: bridge_check
retries: 10
delay: 2
until: bridge_check.rc == 0
changed_when: false
- name: Verify bridge configuration
shell: |
echo "=== Bridge Interface ==="
ip link show {{ br_if }}
echo ""
echo "=== Bridge Members ==="
bridge link show | grep {{ br_if }}
echo ""
echo "=== IP Address ==="
ip addr show {{ br_if }}
register: bridge_status
changed_when: false
- name: Display bridge status
debug:
var: bridge_status.stdout_lines

View File

@@ -0,0 +1,62 @@
---
- name: Gather OpenWrt facts
setup:
- name: Check OpenWrt version
shell: ". /etc/os-release && echo $VERSION"
register: openwrt_version
changed_when: false
- name: Display OpenWrt version
debug:
msg: "OpenWrt version: {{ openwrt_version.stdout }}"
- name: Update package lists
shell: opkg update
changed_when: false
- name: Check if packages are installed
shell: opkg list-installed | grep -E "^(wireguard-tools|kmod-wireguard|kmod-vxlan|ip-full|tcpdump) " || true
register: installed_packages
changed_when: false
- name: Install required packages
shell: opkg install {{ item }}
loop:
- wireguard-tools
- kmod-wireguard
- kmod-vxlan
- ip-full
- tcpdump
when: item not in installed_packages.stdout
register: package_install
failed_when: package_install.rc != 0 and 'already installed' not in package_install.stderr
- name: Enable IP forwarding
shell: |
uci set network.@globals[0].ipv6_forwarding='1' 2>/dev/null || uci add network globals
uci set network.@globals[0].ipv6_forwarding='1'
uci commit network
changed_when: true
- name: Configure sysctl settings
blockinfile:
path: /etc/sysctl.conf
create: yes
block: |
net.ipv4.ip_forward=1
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-ip6tables=0
marker: "# {mark} ANSIBLE MANAGED - Wireguard Lab"
- name: Apply sysctl settings
shell: sysctl -p
changed_when: false
- name: Create configuration backup
shell: sysupgrade -b /tmp/backup-pre-wireguard-lab-$(date +%Y%m%d-%H%M%S).tar.gz
args:
creates: /tmp/backup-pre-wireguard-lab-*.tar.gz
ignore_errors: yes

View File

@@ -0,0 +1,78 @@
---
- name: Set DHCP configuration variables
set_fact:
br_if: "br_{{ gateway_config.lab_name }}"
dhcp_tag: "{{ gateway_config.lab_name }}"
- name: Calculate DHCP range based on role
set_fact:
dhcp_start: "{{ gateway_config.subnet | ipaddr('10') | ipaddr('address') if gateway_config.dhcp_role == 'primary' else gateway_config.subnet | ipaddr('128') | ipaddr('address') }}"
dhcp_limit: "{{ 118 if gateway_config.dhcp_role == 'primary' else 118 }}"
when: gateway_config.dhcp_mode == "simple"
- name: Calculate DHCP range for failover simulation
set_fact:
dhcp_start: "{{ gateway_config.subnet | ipaddr('10') | ipaddr('address') }}"
dhcp_limit: 236
when: gateway_config.dhcp_mode == "failover"
- name: Remove existing DHCP configuration for this lab
shell: |
uci delete dhcp.{{ dhcp_tag }} 2>/dev/null || true
uci commit dhcp
changed_when: true
- name: Configure DHCP server
shell: |
uci set dhcp.{{ dhcp_tag }}='dhcp'
uci set dhcp.{{ dhcp_tag }}.interface='{{ br_if }}'
uci set dhcp.{{ dhcp_tag }}.start='{{ dhcp_start | ipaddr('host') }}'
uci set dhcp.{{ dhcp_tag }}.limit='{{ dhcp_limit }}'
uci set dhcp.{{ dhcp_tag }}.leasetime='12h'
uci add_list dhcp.{{ dhcp_tag }}.dhcp_option='3,{{ gateway_config.subnet | ipaddr("1") | ipaddr("address") }}'
uci add_list dhcp.{{ dhcp_tag }}.dhcp_option='6,{{ gateway_config.subnet | ipaddr("1") | ipaddr("address") }}'
uci commit dhcp
changed_when: true
- name: Configure DNS domain for lab network
shell: |
uci set dhcp.{{ dhcp_tag }}.domain='{{ gateway_config.lab_name }}.lab'
uci set dhcp.{{ dhcp_tag }}.local='/{{ gateway_config.lab_name }}.lab/'
uci commit dhcp
changed_when: true
- name: Set DHCP authoritative mode based on role
shell: |
uci set dhcp.{{ dhcp_tag }}.authoritative='{{ "1" if gateway_config.dhcp_role == "primary" else "0" }}'
uci commit dhcp
changed_when: true
when: gateway_config.dhcp_mode == "failover"
- name: Restart dnsmasq
shell: /etc/init.d/dnsmasq restart
changed_when: true
- name: Wait for dnsmasq to start
shell: pgrep dnsmasq
register: dnsmasq_check
retries: 5
delay: 2
until: dnsmasq_check.rc == 0
changed_when: false
- name: Verify DHCP configuration
shell: |
echo "=== DHCP Configuration ==="
uci show dhcp.{{ dhcp_tag }}
echo ""
echo "=== dnsmasq Process ==="
ps | grep dnsmasq | grep -v grep
echo ""
echo "=== DHCP Leases ==="
cat /tmp/dhcp.leases 2>/dev/null || echo "No leases yet"
register: dhcp_status
changed_when: false
- name: Display DHCP status
debug:
var: dhcp_status.stdout_lines

View File

@@ -0,0 +1,98 @@
---
- name: Set firewall zone name
set_fact:
fw_zone: "{{ gateway_config.lab_name }}"
br_if: "br_{{ gateway_config.lab_name }}"
- name: Remove existing firewall zone
shell: |
# Find and remove existing zone
zone_idx=$(uci show firewall | grep "=zone" | grep "name='{{ fw_zone }}'" | cut -d. -f2 | cut -d= -f1)
if [ -n "$zone_idx" ]; then
uci delete firewall.$zone_idx
fi
# Remove associated forwarding rules
for fwd in $(uci show firewall | grep "={{ fw_zone }}" | cut -d. -f2 | cut -d= -f1); do
uci delete firewall.$fwd 2>/dev/null || true
done
uci commit firewall
changed_when: true
- name: Create isolated firewall zone for lab network
shell: |
# Create new zone
uci add firewall zone
uci set firewall.@zone[-1].name='{{ fw_zone }}'
uci set firewall.@zone[-1].input='REJECT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='REJECT'
uci set firewall.@zone[-1].network='{{ br_if }}'
uci commit firewall
changed_when: true
- name: Allow DNS and DHCP from lab to gateway
shell: |
# Allow DNS
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-{{ fw_zone }}-DNS'
uci set firewall.@rule[-1].src='{{ fw_zone }}'
uci set firewall.@rule[-1].proto='tcp udp'
uci set firewall.@rule[-1].dest_port='53'
uci set firewall.@rule[-1].target='ACCEPT'
# Allow DHCP
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-{{ fw_zone }}-DHCP'
uci set firewall.@rule[-1].src='{{ fw_zone }}'
uci set firewall.@rule[-1].proto='udp'
uci set firewall.@rule[-1].src_port='67:68'
uci set firewall.@rule[-1].dest_port='67:68'
uci set firewall.@rule[-1].target='ACCEPT'
uci commit firewall
changed_when: true
- name: Allow intra-lab forwarding
shell: |
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='{{ fw_zone }}'
uci set firewall.@forwarding[-1].dest='{{ fw_zone }}'
uci commit firewall
changed_when: true
- name: Block lab to WAN/LAN
shell: |
# Explicit reject to WAN
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='{{ fw_zone }}'
uci set firewall.@forwarding[-1].dest='wan'
uci set firewall.@forwarding[-1].enabled='0'
# Explicit reject to LAN
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='{{ fw_zone }}'
uci set firewall.@forwarding[-1].dest='lan'
uci set firewall.@forwarding[-1].enabled='0'
uci commit firewall
changed_when: true
- name: Reload firewall
shell: /etc/init.d/firewall reload
changed_when: true
- name: Verify firewall configuration
shell: |
echo "=== Firewall Zones ==="
uci show firewall | grep "={{ fw_zone }}"
echo ""
echo "=== Firewall Rules ==="
iptables -L -n -v | grep -A 5 "{{ fw_zone }}" || echo "No iptables rules yet"
register: firewall_status
changed_when: false
- name: Display firewall status
debug:
var: firewall_status.stdout_lines

View File

@@ -0,0 +1,80 @@
---
- name: Create metrics exporter directory
file:
path: /usr/lib/wg-lab-exporter
state: directory
mode: '0755'
- name: Deploy metrics exporter script
template:
src: metrics-exporter.sh.j2
dest: /usr/lib/wg-lab-exporter/exporter.sh
mode: '0755'
- name: Create exporter configuration
template:
src: exporter-config.j2
dest: /etc/wg-lab-exporter.conf
mode: '0644'
- name: Create uhttpd configuration for metrics endpoint
blockinfile:
path: /etc/uhttpd.conf
create: yes
marker: "# {mark} ANSIBLE MANAGED - Metrics Exporter"
block: |
config uhttpd 'metrics'
list listen_http '0.0.0.0:9586'
option home '/www-metrics'
option cgi_prefix '/metrics'
option script_timeout '10'
option network_timeout '30'
option tcp_keepalive '1'
- name: Create metrics web directory
file:
path: /www-metrics/metrics
state: directory
mode: '0755'
- name: Create CGI wrapper for metrics
copy:
dest: /www-metrics/metrics/index.cgi
mode: '0755'
content: |
#!/bin/sh
echo "Content-Type: text/plain"
echo ""
/usr/lib/wg-lab-exporter/exporter.sh
- name: Create cron job for metrics collection (fallback)
cron:
name: "WireGuard Lab Metrics Collection"
minute: "*/5"
job: "/usr/lib/wg-lab-exporter/exporter.sh > /tmp/wg-lab-metrics.txt"
user: root
- name: Restart uhttpd
shell: /etc/init.d/uhttpd restart
changed_when: true
- name: Test metrics endpoint
uri:
url: "http://localhost:9586/metrics/"
timeout: 5
register: metrics_test
ignore_errors: yes
changed_when: false
- name: Display metrics test result
debug:
msg: "Metrics endpoint {{ 'OK' if metrics_test.status == 200 else 'FAILED' }}"
- name: Show sample metrics output
shell: /usr/lib/wg-lab-exporter/exporter.sh | head -20
register: metrics_sample
changed_when: false
- name: Display sample metrics
debug:
var: metrics_sample.stdout_lines

View File

@@ -0,0 +1,10 @@
# WireGuard Lab Exporter Configuration
# Lab: {{ gateway_config.lab_name }}
LAB_NAME="{{ gateway_config.lab_name }}"
VNI="{{ gateway_config.vni }}"
# Interfaces to monitor
WG_INTERFACES="wg_{{ gateway_config.lab_name }}"
BRIDGE_INTERFACES="br_{{ gateway_config.lab_name }}"
VXLAN_INTERFACES="vxlan{{ gateway_config.vni }}"

View File

@@ -0,0 +1,105 @@
#!/bin/sh
# Prometheus metrics exporter for WireGuard Lab Overlay
# Generated by Ansible
. /etc/wg-lab-exporter.conf
# Helper function to output metrics
metric() {
echo "$1 $2"
}
metric_label() {
echo "$1{$2} $3"
}
# Current timestamp
TIMESTAMP=$(date +%s)
echo "# HELP wireguard_tunnel_up WireGuard tunnel status (1=up, 0=down)"
echo "# TYPE wireguard_tunnel_up gauge"
# Check WireGuard tunnels
for WG_IF in ${WG_INTERFACES}; do
if ip link show ${WG_IF} >/dev/null 2>&1; then
# Get peer information
wg show ${WG_IF} dump | tail -n +2 | while IFS=$'\t' read pubkey psk endpoint allowed_ip latest_handshake rx_bytes tx_bytes persistent_keepalive; do
# Calculate handshake age
if [ "$latest_handshake" != "0" ]; then
handshake_age=$((TIMESTAMP - latest_handshake))
if [ $handshake_age -lt 180 ]; then
status=1
else
status=0
fi
else
status=0
handshake_age=-1
fi
peer_short=$(echo $pubkey | cut -c1-16)
metric_label "wireguard_tunnel_up" "lab=\"${LAB_NAME}\",interface=\"${WG_IF}\",peer=\"${peer_short}\"" "$status"
metric_label "wireguard_latest_handshake_seconds" "lab=\"${LAB_NAME}\",interface=\"${WG_IF}\",peer=\"${peer_short}\"" "$handshake_age"
metric_label "wireguard_rx_bytes_total" "lab=\"${LAB_NAME}\",interface=\"${WG_IF}\",peer=\"${peer_short}\"" "$rx_bytes"
metric_label "wireguard_tx_bytes_total" "lab=\"${LAB_NAME}\",interface=\"${WG_IF}\",peer=\"${peer_short}\"" "$tx_bytes"
done
else
metric_label "wireguard_tunnel_up" "lab=\"${LAB_NAME}\",interface=\"${WG_IF}\",peer=\"all\"" "0"
fi
done
echo ""
echo "# HELP bridge_fdb_entries Number of FDB entries in bridge"
echo "# TYPE bridge_fdb_entries gauge"
# Bridge FDB entries
for BR_IF in ${BRIDGE_INTERFACES}; do
if ip link show ${BR_IF} >/dev/null 2>&1; then
fdb_count=$(bridge fdb show br ${BR_IF} 2>/dev/null | wc -l)
metric_label "bridge_fdb_entries" "lab=\"${LAB_NAME}\",bridge=\"${BR_IF}\"" "$fdb_count"
fi
done
echo ""
echo "# HELP vxlan_rx_packets_total VXLAN received packets"
echo "# TYPE vxlan_rx_packets_total counter"
echo "# HELP vxlan_tx_packets_total VXLAN transmitted packets"
echo "# TYPE vxlan_tx_packets_total counter"
# VXLAN statistics
for VXLAN_IF in ${VXLAN_INTERFACES}; do
if [ -f "/sys/class/net/${VXLAN_IF}/statistics/rx_packets" ]; then
rx_packets=$(cat /sys/class/net/${VXLAN_IF}/statistics/rx_packets)
tx_packets=$(cat /sys/class/net/${VXLAN_IF}/statistics/tx_packets)
rx_bytes=$(cat /sys/class/net/${VXLAN_IF}/statistics/rx_bytes)
tx_bytes=$(cat /sys/class/net/${VXLAN_IF}/statistics/tx_bytes)
metric_label "vxlan_rx_packets_total" "lab=\"${LAB_NAME}\",vni=\"${VNI}\"" "$rx_packets"
metric_label "vxlan_tx_packets_total" "lab=\"${LAB_NAME}\",vni=\"${VNI}\"" "$tx_packets"
metric_label "vxlan_rx_bytes_total" "lab=\"${LAB_NAME}\",vni=\"${VNI}\"" "$rx_bytes"
metric_label "vxlan_tx_bytes_total" "lab=\"${LAB_NAME}\",vni=\"${VNI}\"" "$tx_bytes"
fi
done
echo ""
echo "# HELP dhcp_leases_active Number of active DHCP leases"
echo "# TYPE dhcp_leases_active gauge"
# DHCP lease count
if [ -f "/tmp/dhcp.leases" ]; then
lease_count=$(wc -l < /tmp/dhcp.leases)
metric_label "dhcp_leases_active" "lab=\"${LAB_NAME}\"" "$lease_count"
fi
echo ""
echo "# HELP openwrt_system_uptime_seconds System uptime in seconds"
echo "# TYPE openwrt_system_uptime_seconds counter"
uptime_seconds=$(cat /proc/uptime | cut -d' ' -f1 | cut -d. -f1)
metric "openwrt_system_uptime_seconds" "$uptime_seconds"
echo ""
echo "# HELP openwrt_memory_free_bytes Free memory in bytes"
echo "# TYPE openwrt_memory_free_bytes gauge"
mem_free=$(free | grep Mem | awk '{print $4}')
metric "openwrt_memory_free_bytes" "$((mem_free * 1024))"

View File

@@ -0,0 +1,62 @@
---
- name: Set VXLAN interface name
set_fact:
vxlan_if: "vxlan{{ gateway_config.vni }}"
wg_if: "wg_{{ gateway_config.lab_name }}"
- name: Remove existing VXLAN interface
shell: |
uci delete network.{{ vxlan_if }} 2>/dev/null || true
uci commit network
changed_when: true
- name: Create VXLAN interface using ip command (OpenWrt native VXLAN)
shell: |
# Remove if exists
ip link delete {{ vxlan_if }} 2>/dev/null || true
# Create VXLAN interface
ip link add {{ vxlan_if }} type vxlan \
id {{ gateway_config.vni }} \
dev {{ wg_if }} \
dstport 4789 \
nolearning \
proxy
# Bring up interface
ip link set {{ vxlan_if }} up
changed_when: true
- name: Add static FDB entries for peers
shell: |
# Clear existing FDB entries for this interface
bridge fdb show dev {{ vxlan_if }} | grep 00:00:00:00:00:00 | while read line; do
bridge fdb del $(echo $line | awk '{print $1}') dev {{ vxlan_if }} 2>/dev/null || true
done
# Add FDB entries for all peers
bridge fdb append 00:00:00:00:00:00 dev {{ vxlan_if }} dst {{ peer.wg_ip }}
loop: "{{ gateway_config.peers }}"
loop_control:
loop_var: peer
label: "{{ peer.to }}"
changed_when: true
- name: Create VXLAN startup script
template:
src: vxlan-startup.sh.j2
dest: /etc/init.d/vxlan-{{ gateway_config.lab_name }}
mode: '0755'
- name: Enable VXLAN startup script
shell: /etc/init.d/vxlan-{{ gateway_config.lab_name }} enable
changed_when: true
- name: Verify VXLAN interface
shell: ip -d link show {{ vxlan_if }}
register: vxlan_status
changed_when: false
- name: Display VXLAN status
debug:
var: vxlan_status.stdout_lines

View File

@@ -0,0 +1,56 @@
#!/bin/sh /etc/rc.common
# VXLAN startup script for {{ gateway_config.lab_name }}
START=99
STOP=10
USE_PROCD=1
LAB_NAME="{{ gateway_config.lab_name }}"
VNI="{{ gateway_config.vni }}"
WG_IF="wg_${LAB_NAME}"
VXLAN_IF="vxlan${VNI}"
start_service() {
# Wait for WireGuard interface
timeout=30
while [ $timeout -gt 0 ]; do
if ip link show ${WG_IF} >/dev/null 2>&1; then
break
fi
sleep 1
timeout=$((timeout-1))
done
# Remove if exists
ip link delete ${VXLAN_IF} 2>/dev/null || true
# Create VXLAN interface
ip link add ${VXLAN_IF} type vxlan \
id ${VNI} \
dev ${WG_IF} \
dstport 4789 \
nolearning \
proxy
# Add FDB entries
{% for peer in gateway_config.peers %}
bridge fdb append 00:00:00:00:00:00 dev ${VXLAN_IF} dst {{ peer.wg_ip }}
{% endfor %}
# Bring up interface
ip link set ${VXLAN_IF} up
logger -t vxlan-${LAB_NAME} "VXLAN ${VXLAN_IF} started"
}
stop_service() {
ip link delete ${VXLAN_IF} 2>/dev/null || true
logger -t vxlan-${LAB_NAME} "VXLAN ${VXLAN_IF} stopped"
}
restart() {
stop_service
sleep 2
start_service
}

View File

@@ -0,0 +1,86 @@
---
- name: Set WireGuard interface name
set_fact:
wg_if: "wg_{{ gateway_config.lab_name }}"
wg_port: "{{ gateway_config.wg_listen_port }}"
- name: Remove existing WireGuard interface configuration
shell: |
uci delete network.{{ wg_if }} 2>/dev/null || true
uci commit network
changed_when: true
- name: Create WireGuard interface
shell: |
uci set network.{{ wg_if }}=interface
uci set network.{{ wg_if }}.proto='wireguard'
uci set network.{{ wg_if }}.private_key='{{ gateway_config.wg_private_key }}'
uci set network.{{ wg_if }}.listen_port='{{ wg_port }}'
uci add_list network.{{ wg_if }}.addresses='{{ gateway_config.wg_ip }}/{{ gateway_config.wg_net | ipaddr("prefix") }}'
uci commit network
changed_when: true
- name: Remove existing WireGuard peers
shell: |
for peer in $(uci show network | grep "={{ wg_if }}_peer" | cut -d. -f2 | cut -d= -f1); do
uci delete network.$peer
done
uci commit network
changed_when: true
ignore_errors: yes
- name: Configure WireGuard gateway peers
shell: |
peer_name="{{ wg_if }}_peer{{ idx }}"
uci set network.$peer_name=wireguard_{{ wg_if }}
uci set network.$peer_name.public_key='{{ peer.public_key }}'
uci set network.$peer_name.endpoint_host='{{ peer.endpoint }}'
uci set network.$peer_name.endpoint_port='{{ wg_port }}'
uci set network.$peer_name.persistent_keepalive='25'
uci add_list network.$peer_name.allowed_ips='{{ peer.wg_ip }}/32'
uci set network.$peer_name.route_allowed_ips='0'
uci commit network
loop: "{{ gateway_config.peers }}"
loop_control:
index_var: idx
loop_var: peer
label: "{{ peer.to }}"
changed_when: true
- name: Configure WireGuard road warrior peers
shell: |
peer_name="{{ wg_if }}_rw{{ idx }}"
uci set network.$peer_name=wireguard_{{ wg_if }}
uci set network.$peer_name.public_key='{{ client.public_key }}'
uci add_list network.$peer_name.allowed_ips='{{ client.wg_ip }}/32'
uci set network.$peer_name.route_allowed_ips='0'
uci set network.$peer_name.description='{{ client.email }}'
uci commit network
loop: "{{ gateway_config.rw_clients }}"
loop_control:
index_var: idx
loop_var: client
label: "{{ client.email }}"
when: gateway_config.road_warrior and gateway_config.rw_clients | length > 0
changed_when: true
- name: Reload network configuration
shell: /etc/init.d/network reload
changed_when: true
- name: Wait for WireGuard interface to come up
shell: ip link show {{ wg_if }}
register: wg_link_check
retries: 10
delay: 2
until: wg_link_check.rc == 0
changed_when: false
- name: Verify WireGuard is running
shell: wg show {{ wg_if }}
register: wg_status
changed_when: false
- name: Display WireGuard status
debug:
var: wg_status.stdout_lines

View File

@@ -0,0 +1,4 @@
---
- name: reload systemd
systemd:
daemon_reload: yes

View File

@@ -0,0 +1,19 @@
---
- name: Create VXLAN interface setup script
template:
src: setup-vxlan.sh.j2
dest: /usr/local/bin/setup-vxlan-{{ gateway_config.lab_name }}.sh
mode: '0755'
- name: Create systemd service for VXLAN
template:
src: vxlan.service.j2
dest: /etc/systemd/system/vxlan-{{ gateway_config.lab_name }}.service
notify: reload systemd
- name: Enable and start VXLAN service
systemd:
name: vxlan-{{ gateway_config.lab_name }}
enabled: yes
state: started
daemon_reload: yes

View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
LAB_NAME="{{ gateway_config.lab_name }}"
VNI="{{ gateway_config.vni }}"
WG_IF="wg-${LAB_NAME}"
VXLAN_IF="vxlan${VNI}"
# Wait for WireGuard interface
timeout=30
while [ $timeout -gt 0 ]; do
if ip link show ${WG_IF} &>/dev/null; then
break
fi
sleep 1
((timeout--))
done
# Create VXLAN interface
if ! ip link show ${VXLAN_IF} &>/dev/null; then
ip link add ${VXLAN_IF} type vxlan \
id ${VNI} \
dev ${WG_IF} \
dstport {{ vxlan_port }} \
nolearning \
proxy
fi
# Configure VXLAN peers (FDB entries)
{% for peer in gateway_config.peers %}
bridge fdb append 00:00:00:00:00:00 dev ${VXLAN_IF} dst {{ peer.wg_ip }}
{% endfor %}
# Bring up interface
ip link set ${VXLAN_IF} up

View File

@@ -0,0 +1,13 @@
[Unit]
Description=VXLAN interface for {{ gateway_config.lab_name }}
After=wg-quick@wg-{{ gateway_config.lab_name }}.service
Requires=wg-quick@wg-{{ gateway_config.lab_name }}.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/setup-vxlan-{{ gateway_config.lab_name }}.sh
ExecStop=/sbin/ip link delete vxlan{{ gateway_config.vni }}
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,5 @@
---
- name: restart wireguard
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
state: restarted

View File

@@ -0,0 +1,19 @@
---
- name: Create WireGuard config directory
file:
path: "{{ wireguard_config_dir }}"
state: directory
mode: '0700'
- name: Generate WireGuard configuration
template:
src: wg-interface.conf.j2
dest: "{{ wireguard_config_dir }}/wg-{{ gateway_config.lab_name }}.conf"
mode: '0600'
notify: restart wireguard
- name: Enable WireGuard interface
systemd:
name: "wg-quick@wg-{{ gateway_config.lab_name }}"
enabled: yes
state: started

View File

@@ -0,0 +1,31 @@
[Interface]
Address = {{ gateway_config.wg_ip }}/{{ gateway_config.wg_net | ipaddr('prefix') }}
ListenPort = {{ gateway_config.wg_listen_port }}
PrivateKey = {{ gateway_config.wg_private_key }}
# Disable default route
Table = off
# Post-up: Add route for WireGuard mesh network only
PostUp = ip route add {{ gateway_config.wg_net }} dev wg-{{ gateway_config.lab_name }}
{% for peer in gateway_config.peers %}
[Peer]
# {{ peer.to }}
PublicKey = {{ peer.public_key }}
Endpoint = {{ peer.endpoint }}:{{ gateway_config.wg_listen_port }}
AllowedIPs = {{ peer.wg_ip }}/32
PersistentKeepalive = {{ wireguard_persistent_keepalive }}
{% endfor %}
{% if gateway_config.road_warrior and gateway_config.rw_clients | length > 0 %}
# Road Warrior Clients
{% for client in gateway_config.rw_clients %}
[Peer]
# {{ client.email }}
PublicKey = {{ client.public_key }}
AllowedIPs = {{ client.wg_ip }}/32
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,29 @@
---
- name: Check WireGuard status (Linux)
shell: "wg show wg-{{ gateway_config.lab_name }}"
register: wg_status_linux
changed_when: false
failed_when: false
when: gateway_config.type == 'linux'
- name: Check VXLAN interface (Linux)
shell: "ip link show vxlan{{ gateway_config.vni }}"
register: vxlan_status
changed_when: false
failed_when: false
when: gateway_config.type == 'linux'
- name: Check bridge (Linux)
shell: "ip link show br-{{ gateway_config.lab_name }}"
register: bridge_status
changed_when: false
failed_when: false
when: gateway_config.type == 'linux'
- name: Set health status
set_fact:
health_check_results:
wireguard: "{{ 'PASS' if wg_status_linux.rc == 0 else 'FAIL' }}"
vxlan: "{{ 'PASS' if vxlan_status.rc == 0 else 'FAIL' }}"
bridge: "{{ 'PASS' if bridge_status.rc == 0 else 'FAIL' }}"
when: gateway_config.type == 'linux'

View File

@@ -0,0 +1,14 @@
---
- name: Validate gateway configuration
debug:
msg: "Validating {{ inventory_hostname }}"
- name: Check WireGuard peers connectivity
shell: "ping -c 3 -W 2 {{ peer.wg_ip }}"
loop: "{{ gateway_config.peers }}"
loop_control:
loop_var: peer
register: ping_results
failed_when: false
changed_when: false
when: gateway_config.type == 'linux'

39
create_all_files.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Script to create all remaining files
BASE_DIR="/home/claude/wireguard-lab-overlay"
# Create all operational playbooks
cat > "$BASE_DIR/ansible/playbooks/site.yml" << 'EOF'
---
- name: Deploy WireGuard Lab Overlay Network
hosts: lab_gateways
become: yes
pre_tasks:
- name: Load Terraform outputs
set_fact:
tf_config: "{{ lookup('file', '../../terraform/outputs.json') | from_json }}"
delegate_to: localhost
run_once: yes
- name: Extract gateway-specific config
set_fact:
gateway_config: "{{ tf_config.gateway_config.value[inventory_hostname] }}"
roles:
- common
- wireguard
- vxlan
- bridge
- dnsmasq
- monitoring
- name: Configure Monitoring Hub
hosts: monitoring_hub
become: yes
roles:
- monitoring_hub
EOF
# Continue with more files...

45
deploy.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== WireGuard Lab Overlay Network Deployment ==="
# Step 1: Terraform
echo ""
echo "[1/5] Running Terraform..."
cd terraform
terraform init
terraform plan -out=tfplan
terraform apply tfplan
terraform output -json > outputs.json
cd ..
# Step 2: Ansible inventory generation
echo ""
echo "[2/5] Generating Ansible inventory..."
cd ansible
terraform -chdir=../terraform output ansible_inventory > inventory/hosts.yml
# Step 3: Deploy to gateways
echo ""
echo "[3/5] Deploying to gateways..."
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# Step 4: Generate road warrior configs
echo ""
echo "[4/5] Generating road warrior configurations..."
ansible-playbook -i inventory/hosts.yml playbooks/generate-road-warrior.yml
# Step 5: Deploy monitoring hub
echo ""
echo "[5/5] Deploying monitoring hub..."
cd ../monitoring
docker-compose up -d
echo ""
echo "=== Deployment Complete ==="
echo "Grafana: http://$(hostname -I | awk '{print $1}'):3000 (admin/admin)"
echo "Prometheus: http://$(hostname -I | awk '{print $1}'):9090"
echo "Road warrior configs: $SCRIPT_DIR/road-warrior/generated/"

43
docs/architecture.md Normal file
View File

@@ -0,0 +1,43 @@
# WireGuard Lab Overlay Network Architecture
## Overview
This solution provides automated deployment of isolated Layer 2 networks across corporate infrastructure using WireGuard and VXLAN.
## Components
### WireGuard Mesh
- Full mesh topology between gateways within each lab network
- Encrypted tunnels over corporate network
- No routing of lab traffic through corporate infrastructure
### VXLAN Overlay
- Provides Layer 2 connectivity over WireGuard Layer 3 tunnels
- BUM traffic replication to all peers
- Support for up to 10 gateways per lab
### Gateway Types
- **Linux VMs**: Full-featured with ISC DHCP failover support
- **MikroTik Routers**: Managed via RouterOS API
### DHCP Modes
- **Failover**: ISC DHCP with active-passive failover (Linux only)
- **Simple**: dnsmasq with split address ranges
### Monitoring
- Prometheus exporters on each gateway
- Centralized Grafana dashboards
- Metrics: tunnel status, bandwidth, DHCP leases, bridge stats
## Network Flow
```
Lab Device → Linux Bridge/MikroTik Bridge → VXLAN → WireGuard → Corporate Network → WireGuard → VXLAN → Bridge → Lab Device
```
## Security
- All lab traffic encrypted via WireGuard
- Complete isolation from corporate network
- Road warrior clients get direct lab access
- No inter-lab routing

405
docs/design-discussion.md Normal file
View File

@@ -0,0 +1,405 @@
# Design Discussion and Requirements
This document captures the key design decisions, requirements, and architectural discussions that shaped the WireGuard Lab Overlay Network solution.
## Initial Requirements
### Core Objective
Automated deployment of a WireGuard-based overlay network that connects local lab room components at Layer 2 across the company network, without exposing lab devices to the corporate network.
### Key Requirements
1. **Layer 2 Connectivity**: Lab devices plugged into gateway routers/switches should communicate at L2 across the company network
2. **Network Isolation**: Lab traffic must NOT have connectivity to company network
3. **Gateway Flexibility**: Support Linux VMs, MikroTik routers, and OpenWrt devices
4. **Scalability**: Up to 10 gateways per lab network, 10-20 independent lab networks
5. **Automation**: Ansible or Terraform for automated deployment
6. **Ease of Deployment**: Solution should be easily deployable
7. **Future Requirements**:
- Microsegmentation capability
- Road warrior access for office notebooks
- Central management
- Central monitoring/visibility
### Lab Network Specifications
- **Max gateways per lab**: 10
- **Total lab networks**: 10-20 independent networks
- **DHCP**: Lab devices use DHCP, each lab has own IP range
- **Gateway types**: Linux VMs (primary), MikroTik routers, OpenWrt devices
- **Multiple NICs**: Gateways have multiple network interfaces
- **No VM provisioning**: Configure existing VMs/routers only
## Architecture Decisions
### 1. WireGuard + VXLAN Approach
**Decision**: Use VXLAN over WireGuard instead of direct WireGuard bridging.
**Rationale**:
- WireGuard is L3-only natively
- VXLAN provides true L2 extension with broadcast/multicast support
- Clean separation of concerns:
- WireGuard = Encrypted L3 transport mesh
- VXLAN = L2 overlay with BUM traffic handling
- Bridge = Local device attachment
**Alternative Considered**: Direct bridging of WireGuard interfaces
- **Rejected because**: More complex, less clean architecture, harder to troubleshoot
### 2. Full Mesh Topology
**Decision**: All gateways within a lab network form a full WireGuard mesh.
**Rationale**:
- No single point of failure
- Optimal path between any two gateways
- Scales well up to 10 gateways (45 tunnels max)
- Simpler than hub-and-spoke for small deployments
**Calculation**: n gateways = n×(n-1)/2 tunnels
- 3 gateways = 3 tunnels
- 5 gateways = 10 tunnels
- 10 gateways = 45 tunnels (acceptable)
**Alternative Considered**: Hub-and-spoke with central gateway
- **Rejected because**: Single point of failure, suboptimal routing, more complex failover
### 3. Separate WireGuard Interface per Lab
**Decision**: Each lab network gets its own WireGuard interface (wg-lab100, wg-lab200, etc.)
**Rationale**:
- Complete isolation between lab networks
- Easier troubleshooting (can bring down one lab without affecting others)
- Simplified microsegmentation in future
- Clear network boundaries
**Alternative Considered**: Single WireGuard interface with policy routing
- **Rejected because**: Complex routing, harder to isolate, debugging nightmare
### 4. Terraform for State, Ansible for Configuration
**Decision**: Use Terraform for topology/state management, Ansible for device configuration.
**Rationale**:
**Terraform handles:**
- IP address allocation (deterministic, conflict-free)
- WireGuard key generation and tracking
- Peer matrix calculation (combinatorial complexity)
- Road warrior client generation
- State persistence
**Ansible handles:**
- Platform-specific configuration (UCI vs systemd vs RouterOS)
- Service deployment and management
- Operational tasks (updates, health checks, emergency procedures)
**Why not pure Ansible?**
- Peer matrix calculation becomes complex Jinja2 templates
- No built-in state for keys/IPs
- Key generation requires custom modules
- Change detection harder without state
**Why not pure Terraform?**
- Terraform providers for network devices are limited
- Configuration management is Ansible's strength
- Operational playbooks natural in Ansible
### 5. DHCP Strategy
**Decision**: Offer two modes - ISC DHCP failover (Linux only) and dnsmasq split-range (all platforms)
**Rationale**:
**ISC DHCP Failover Mode:**
- True active-passive DHCP failover
- Lease synchronization
- IP address continuity across failovers
- Linux-only (not available on MikroTik/OpenWrt)
**dnsmasq Split-Range Mode:**
- Works on all three platforms
- Simpler configuration
- Primary serves 10-127, Secondary serves 128-245
- Good enough for most lab scenarios
- No lease synchronization but acceptable for labs
**Why both?**
- Flexibility for different requirements
- Some labs need true failover (production-like testing)
- Other labs can use simpler approach
### 6. Multi-Vendor Support
**Decision**: Support Linux, MikroTik, and OpenWrt as first-class citizens.
**Rationale**:
**Linux (VMs):**
- Most flexible and powerful
- Best for primary/critical gateways
- Full feature support (ISC DHCP failover)
- Extensive monitoring capabilities
**MikroTik:**
- Purpose-built routing hardware
- Cost-effective
- Low power consumption
- Common in network labs
- Proven reliability
**OpenWrt:**
- Wide hardware support (x86, ARM, MIPS)
- Repurposing existing hardware
- Open source requirement for some environments
- Budget-friendly option
- Can integrate WiFi AP functionality
**Implementation Strategy:**
- Common interface across all gateway types
- Role-based Ansible deployment
- Gateway type determines which roles execute
- Full interoperability (mix types in same lab)
### 7. Road Warrior Support
**Decision**: Build road warrior support into core architecture from start.
**Rationale**:
- Common requirement (laptops accessing lab devices)
- Easier to design in from start than retrofit
- IP allocation strategy accounts for it
- WireGuard well-suited for road warrior use
**Implementation**:
- Gateways allow road warrior clients as peers
- Clients assigned IPs from WireGuard subnet (after gateway IPs)
- Configuration generation automated via Ansible
- QR codes for mobile devices
### 8. Monitoring Architecture
**Decision**: Prometheus exporters on each gateway, centralized Grafana.
**Rationale**:
- Industry-standard monitoring stack
- Pull-based metrics collection
- Platform-specific exporters:
- Linux: Python Prometheus client
- MikroTik: REST API polling
- OpenWrt: Shell script exporter
**Metrics Collected**:
- WireGuard tunnel status (up/down)
- Latest handshake timing
- Rx/Tx bytes per tunnel
- VXLAN interface statistics
- Bridge FDB entry count
- DHCP active lease count
### 9. Firewall Isolation (OpenWrt-specific)
**Decision**: Automatically create isolated firewall zones for each lab network on OpenWrt.
**Rationale**:
- OpenWrt requires explicit firewall configuration
- Each lab needs its own zone
- Prevent lab-to-corporate traffic
- Allow intra-lab forwarding
- Allow DHCP/DNS to gateway
**Firewall Rules Created**:
```
Zone: lab100
Input: REJECT (except DNS/DHCP)
Forward: REJECT (except to same zone)
Output: ACCEPT
Forwarding:
lab100 → lab100: ACCEPT
lab100 → lan: REJECT
lab100 → wan: REJECT
```
### 10. Configuration Decoupling
**Decision**: Make deployment independent of existing infrastructure.
**Rationale**:
- Don't assume pre-existing Ansible inventory
- Don't depend on existing Terraform state
- Self-contained solution
- Easy to integrate into existing automation later
**Implementation**:
- Fresh Terraform state
- Self-generated Ansible inventory
- Standalone playbooks
- Can be integrated into larger automation framework if needed
## OpenWrt Integration Discussion
### Requirements for OpenWrt Support
When OpenWrt support was requested, the following questions were clarified:
**1. OpenWrt Version**: 23.05 or latest stable
**2. Architectures**: x86 (Intel), ARM, potentially MIPS
**3. Router Independence**: Should work regardless of specific router model
**4. Management**: SSH-based first, API if beneficial
**5. Package Management**: Auto-install missing packages
**6. DHCP Strategy**: Pros/cons analysis requested → chose dnsmasq
**7. Network Config**: UCI-based configuration
**8. Firewall**: Create new zones per lab network
**9. Monitoring**: Lightweight shell script tools
**10. Interoperability**: Must work with Linux/MikroTik
**11. Ansible Modules**: Best option selected (shell + UCI commands)
**12. Storage**: Not a constraint (appropriate hardware selection)
### OpenWrt Design Decisions
**UCI Configuration Method**:
- Native OpenWrt approach
- Atomic commits
- Rollback capability
- CLI-friendly
**Package Installation Strategy**:
```
Required packages:
- wireguard-tools (WireGuard CLI)
- kmod-wireguard (kernel module)
- kmod-vxlan (VXLAN support)
- ip-full (iproute2 with VXLAN)
- tcpdump (troubleshooting)
```
**Firewall Zone Isolation**:
- Each lab gets isolated zone
- Prevents lab → corporate traffic
- Allows lab → lab forwarding
- Gateway provides DNS/DHCP
**Monitoring Approach**:
- Shell script Prometheus exporter
- Lightweight (no Python needed)
- Served via uhttpd on port 9586
- Collects essential metrics
## Rejected Alternatives
### 1. GRE/IPIP Instead of VXLAN
**Rejected**: Less flexible, no native L2 support, older technology
### 2. OpenVPN Instead of WireGuard
**Rejected**: Slower, more complex, heavier resource usage, older codebase
### 3. Single Gateway with VLANs
**Rejected**: Single point of failure, doesn't meet redundancy requirements
### 4. Cloud-Managed Solution (Tailscale/ZeroTier)
**Rejected**: External dependency, cost, limited control, privacy concerns
### 5. Pure iptables/nftables Firewall on Linux
**Rejected for OpenWrt**: UCI firewall is native and more maintainable
### 6. LuCI Web UI Integration
**Deferred**: CLI/Ansible-first approach, could add later as enhancement
## Known Limitations and Trade-offs
### 1. DHCP Failover on Non-Linux Platforms
**Limitation**: True DHCP failover only on Linux (ISC DHCP)
**Trade-off**: MikroTik/OpenWrt use split ranges (acceptable for labs)
**Mitigation**: Can use Linux for critical labs requiring true failover
### 2. VXLAN Performance Overhead
**Limitation**: Double encapsulation (VXLAN + WireGuard) adds overhead
**Trade-off**: ~5-10% throughput reduction vs single encapsulation
**Mitigation**: Modern CPUs handle this well, acceptable for lab use
### 3. Broadcast Traffic Amplification
**Limitation**: BUM traffic replicated to all peers in mesh
**Trade-off**: With 10 gateways, broadcast amplified 10x
**Mitigation**: 10 gateways max limit, modern switches handle this
### 4. WireGuard Key Rotation
**Limitation**: No automatic key rotation implemented
**Trade-off**: Manual process to regenerate and redistribute keys
**Mitigation**: Keys stored in Terraform state, can be regenerated and redeployed
### 5. No Inter-Lab Routing
**Limitation**: Labs are completely isolated from each other
**Trade-off**: Can't route between lab100 and lab200
**Mitigation**: Design requirement (isolation), can add selective routing later
### 6. Road Warrior Scalability
**Limitation**: Each gateway needs peer config for each RW client
**Trade-off**: 100 RW clients = 100 peer configs per gateway
**Mitigation**: Acceptable for expected scale, can optimize if needed
## Future Enhancements Considered
### Near-Term (Feasible)
1. **Automated key rotation** - Periodic WireGuard key regeneration
2. **Dynamic peer discovery** - Reduce configuration for large meshes
3. **QoS policies** - Traffic prioritization for lab networks
4. **WiFi AP mode** - OpenWrt as wireless access point for labs
5. **LuCI integration** - Web UI for OpenWrt management
### Medium-Term (Requires Development)
1. **Microsegmentation** - Finer-grained network isolation
2. **Inter-lab routing** - Selective routing between specific labs
3. **IPAM integration** - NetBox/Nautobot for IP management
4. **Centralized logging** - Log aggregation from all gateways
5. **Alerting rules** - Prometheus alerts for tunnel failures
### Long-Term (Architectural Changes)
1. **BGP-based routing** - For very large deployments
2. **Multi-site support** - Gateways across multiple datacenters
3. **IPv6 support** - Dual-stack overlay networks
4. **Hardware offload** - WireGuard hardware acceleration
## Design Principles Applied
Throughout the design process, we adhered to these principles:
1. **Simplicity over complexity** - Use proven technologies, avoid over-engineering
2. **Automation first** - Everything deployable via code
3. **Platform agnostic** - Support multiple gateway types equally
4. **Fail-safe defaults** - Network isolation by default, explicit allowlisting
5. **Observability** - Built-in monitoring from day one
6. **Operational friendly** - Include operational playbooks, not just deployment
7. **Documentation** - Comprehensive guides for all platforms
8. **Backward compatible** - New features don't break existing deployments
## Technical Debt and Known Issues
### Acknowledged but Accepted
1. **Terraform state security**: Keys stored in state (recommend remote backend with encryption)
2. **No automated backups**: Operational playbooks include manual backup procedures
3. **Hard-coded ports**: WireGuard listen port = 51820 + VNI (could be configurable)
4. **No certificate-based auth**: Relies on SSH keys (acceptable for lab environments)
### To Be Addressed
1. **Error handling**: Some playbooks could have more robust error handling
2. **Idempotency**: A few tasks could be more idempotent
3. **Testing**: No automated integration tests (manual testing only)
4. **Rollback**: No automated rollback on failed deployments
## Conclusion
This design represents a pragmatic balance between:
- **Flexibility** (multiple platforms, multiple DHCP modes, road warrior support)
- **Simplicity** (standard technologies, clear architecture)
- **Automation** (minimal manual intervention)
- **Reliability** (full mesh, redundant DHCP, monitoring)
- **Maintainability** (good documentation, operational playbooks)
The solution meets all initial requirements and provides a foundation for future enhancements while remaining production-ready today.
---
**Document Version**: 1.0
**Last Updated**: 2025-01-30
**Maintained By**: Network Architecture Team

278
docs/gateway-comparison.md Normal file
View File

@@ -0,0 +1,278 @@
# Gateway Type Comparison
## Overview
The WireGuard Lab Overlay solution supports three gateway types: **Linux**, **MikroTik RouterOS**, and **OpenWrt**. All types are fully interoperable and can be mixed within the same lab network.
## Feature Comparison
| Feature | Linux | MikroTik | OpenWrt |
|---------|-------|----------|---------|
| **WireGuard Support** | ✅ Native | ✅ Native (RouterOS 7+) | ✅ Native |
| **VXLAN Support** | ✅ Native | ✅ Native | ✅ via kmod-vxlan |
| **DHCP Failover** | ✅ ISC DHCP | ⚠️ Simulated | ⚠️ Simulated |
| **DHCP Simple/Split** | ✅ dnsmasq | ✅ Native DHCP | ✅ dnsmasq |
| **Firewall Isolation** | ✅ iptables/nftables | ✅ Native firewall | ✅ UCI firewall |
| **Monitoring** | ✅ Python exporter | ✅ API/SNMP | ✅ Shell script |
| **Ansible Management** | ✅ Excellent | ✅ Good | ✅ Excellent |
| **Resource Usage** | Medium | Low | Low |
| **Configuration Method** | systemd/files | RouterOS CLI/API | UCI |
## When to Use Each Type
### Linux (VM or Physical)
**Best for:**
- ✅ Primary/critical gateways requiring true DHCP failover
- ✅ High-throughput requirements (10G+)
- ✅ Complex routing/NAT scenarios
- ✅ Advanced monitoring and logging
- ✅ Situations requiring custom software
**Advantages:**
- Most flexible and powerful
- Best monitoring capabilities
- True DHCP failover support
- Easy troubleshooting with standard Linux tools
- Can run additional services
**Disadvantages:**
- Requires more resources (RAM, storage)
- VM overhead if virtualized
- More complex to secure
**Typical Specs:**
- vCPU: 2+
- RAM: 2GB+
- Storage: 20GB+
### MikroTik RouterOS
**Best for:**
- ✅ Branch/remote site gateways
- ✅ Cost-effective hardware deployment
- ✅ Existing MikroTik infrastructure
- ✅ Environments with limited power/cooling
- ✅ Sites requiring hardware redundancy
**Advantages:**
- Purpose-built routing hardware
- Low power consumption
- Excellent price/performance
- Built-in redundancy features (RouterOS version dependent)
- GUI management option (WinBox/WebFig)
**Disadvantages:**
- Proprietary OS
- Learning curve for RouterOS
- Limited customization
- Monitoring via API (not as rich as Linux)
**Typical Models:**
- CCR series (enterprise)
- RB series (SOHO/branch)
- Minimum: RB750Gr3 or better
### OpenWrt
**Best for:**
- ✅ Lab edge/access gateways
- ✅ Repurposing existing hardware
- ✅ WiFi-enabled access points for labs
- ✅ Budget-conscious deployments
- ✅ Learning/testing environments
**Advantages:**
- Runs on wide variety of hardware (x86, ARM, MIPS)
- Open source and highly customizable
- Excellent package ecosystem
- Good community support
- Can combine routing + WiFi AP functions
**Disadvantages:**
- Varies by hardware (performance, stability)
- Flash/RAM limitations on some devices
- Web UI less polished than RouterOS
- Package dependencies can be complex
**Typical Hardware:**
- x86_64: PC Engines APU, Protectli Vault
- ARM: GL.iNet, Turris Omnia
- Purpose-built: TP-Link, Ubiquiti (compatible models)
## Mixed Deployments
You can mix gateway types within a single lab network. Here's a recommended pattern:
### Pattern 1: High Availability Core
```hcl
lab100 = {
gateways = [
{ hostname = "lab100-linux1", type = "linux", dhcp_role = "primary" },
{ hostname = "lab100-linux2", type = "linux", dhcp_role = "secondary" },
{ hostname = "lab100-openwrt1", type = "openwrt", dhcp_role = "backup" }
]
}
```
- Linux gateways provide true DHCP failover
- OpenWrt provides additional redundancy
### Pattern 2: Cost-Optimized
```hcl
lab200 = {
gateways = [
{ hostname = "lab200-mikrotik1", type = "mikrotik", dhcp_role = "primary" },
{ hostname = "lab200-mikrotik2", type = "mikrotik", dhcp_role = "secondary" }
]
}
```
- All MikroTik for consistency
- Lower hardware costs
- Lower power consumption
### Pattern 3: Hybrid
```hcl
lab300 = {
gateways = [
{ hostname = "lab300-linux1", type = "linux", dhcp_role = "primary" },
{ hostname = "lab300-mikrotik1", type = "mikrotik", dhcp_role = "backup" },
{ hostname = "lab300-openwrt1", type = "openwrt", dhcp_role = "backup" }
]
}
```
- Linux core for features
- MikroTik for proven reliability
- OpenWrt for flexibility
## DHCP Mode Recommendations
### ISC DHCP Failover Mode (`dhcp_mode = "failover"`)
**Use when:**
- Critical lab requiring lease continuity
- Active-passive DHCP failover needed
- Only Linux gateways (primary + secondary)
**Configuration:**
```hcl
dhcp_mode = "failover"
gateways = [
{ type = "linux", dhcp_role = "primary" },
{ type = "linux", dhcp_role = "secondary" },
{ type = "...", dhcp_role = "backup" } # Any type for additional backup
]
```
### Simple/Split Range Mode (`dhcp_mode = "simple"`)
**Use when:**
- Any gateway types in use
- DHCP IP address persistence not critical
- Simpler configuration preferred
**Configuration:**
```hcl
dhcp_mode = "simple"
gateways = [
{ type = "...", dhcp_role = "primary" }, # Serves .10-.127
{ type = "...", dhcp_role = "secondary" }, # Serves .128-.245
{ type = "...", dhcp_role = "backup" } # Serves full range (standby)
]
```
## Performance Expectations
### WireGuard Throughput
| Gateway Type | Expected Throughput | Notes |
|--------------|---------------------|-------|
| Linux (modern CPU) | 2-10 Gbps | Depends on CPU/NICs |
| MikroTik CCR | 1-5 Gbps | Varies by model |
| MikroTik RB | 100-500 Mbps | Consumer models |
| OpenWrt x86_64 | 500 Mbps - 2 Gbps | Hardware dependent |
| OpenWrt ARM | 50-300 Mbps | Limited by CPU |
### Recommendations by Lab Size
| Lab Devices | Recommended Primary | Recommended Secondary |
|-------------|---------------------|----------------------|
| 1-10 | OpenWrt | OpenWrt |
| 10-50 | MikroTik or Linux | MikroTik or OpenWrt |
| 50-100 | Linux | Linux or MikroTik |
| 100+ | Linux (multi-core) | Linux |
## Configuration Management
### Ansible Roles Per Type
| Task | Linux Role | MikroTik Role | OpenWrt Role |
|------|-----------|---------------|--------------|
| Common Setup | `common` | `mikrotik-common` | `openwrt-common` |
| WireGuard | `wireguard` | `mikrotik-wireguard` | `openwrt-wireguard` |
| VXLAN | `vxlan` | `mikrotik-vxlan` | `openwrt-vxlan` |
| Bridge | `bridge` | `mikrotik-bridge` | `openwrt-bridge` |
| DHCP | `isc-dhcp` or `dnsmasq` | `mikrotik-dhcp` | `openwrt-dhcp` |
| Firewall | Implicit in roles | `mikrotik-firewall` | `openwrt-firewall` |
| Monitoring | `monitoring` | `mikrotik-monitoring` | `openwrt-monitoring` |
All roles present the same logical interface - Ansible playbooks work identically regardless of gateway mix.
## Monitoring Comparison
### Metrics Collection
**Linux:**
- Python Prometheus exporter
- Most detailed metrics
- Real-time collection
- Port 9586
**MikroTik:**
- REST API polling
- SNMP (optional)
- Good coverage
- Requires external collector
**OpenWrt:**
- Shell script exporter
- Lightweight
- Essential metrics
- Port 9586 (uhttpd)
### Available Metrics (All Types)
- ✅ WireGuard tunnel status
- ✅ WireGuard handshake timing
- ✅ WireGuard rx/tx bytes
- ✅ VXLAN interface stats
- ✅ Bridge FDB entries
- ✅ DHCP lease count
- ✅ System uptime/memory
## Summary Recommendations
### Choose Linux when:
- You need true DHCP failover
- Performance is critical (>1 Gbps)
- Advanced features required
- Resources are not constrained
### Choose MikroTik when:
- Budget-conscious hardware deployment
- Power efficiency matters
- RouterOS expertise available
- Proven routing platform needed
### Choose OpenWrt when:
- Repurposing existing hardware
- Open source requirement
- WiFi AP integration needed
- Learning/lab environment
### Mix them when:
- Different sites have different constraints
- Migrating between platforms
- Cost/performance optimization needed
- Maximum redundancy required
All three types work together seamlessly in the same lab network!

399
docs/openwrt-setup.md Normal file
View File

@@ -0,0 +1,399 @@
# OpenWrt Gateway Setup Guide
## Prerequisites
### OpenWrt Version
- **Minimum**: OpenWrt 23.05 (stable)
- **Recommended**: Latest stable release
- **Architectures**: x86_64, ARM, MIPS supported
### Hardware Requirements
- **Flash**: Minimum 128MB (256MB+ recommended)
- **RAM**: Minimum 128MB (256MB+ recommended)
- **Network Interfaces**: At least 2 (one for management, one for lab devices)
## Initial OpenWrt Preparation
### 1. Fresh OpenWrt Installation
Ensure you have a clean OpenWrt installation with network access.
```bash
# From your management workstation
ssh root@<openwrt-ip>
```
### 2. Configure Management Network
Ensure the router is reachable from your management server:
```bash
# On OpenWrt router
uci set network.lan.ipaddr='192.168.1.21'
uci set network.lan.netmask='255.255.255.0'
uci commit network
/etc/init.d/network restart
```
### 3. Install Required Packages
The Ansible playbook will auto-install these, but you can pre-install:
```bash
opkg update
opkg install wireguard-tools kmod-wireguard kmod-vxlan ip-full tcpdump
```
### 4. Enable SSH Access
Ensure SSH is accessible:
```bash
# Set root password if not already set
passwd
# Enable SSH
/etc/init.d/dropbear enable
/etc/init.d/dropbear start
```
### 5. Configure SSH Keys (Recommended)
From your management server:
```bash
ssh-copy-id root@192.168.1.21
```
## Network Interface Configuration
### Identifying Interfaces
List available interfaces:
```bash
# On OpenWrt
ip link show
```
Common interface names:
- **x86_64**: `eth0`, `eth1`, `eth2`
- **ARM/embedded**: `lan1`, `lan2`, `wan`
- **VLAN-aware**: `eth0.1`, `eth0.2`
### Recommended Setup
```
┌──────────────────┐
│ OpenWrt Router │
│ │
│ eth0 ───────────┼──→ Corporate Network (Management)
│ eth1 ───────────┼──→ Lab Devices (Physical Connection)
│ │
│ wg_lab100 ──────┼──→ WireGuard Overlay
│ vxlan100 ───────┼──→ VXLAN over WireGuard
│ br_lab100 ──────┼──→ Bridge (eth1 + vxlan100)
└──────────────────┘
```
## Deployment via Ansible
### 1. Update Terraform Configuration
Edit `terraform/terraform.tfvars`:
```hcl
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "simple"
road_warrior = false
gateways = [
{
hostname = "lab100-openwrt1"
type = "openwrt" # Important: set to 'openwrt'
mgmt_ip = "192.168.1.21"
api_port = 22
lab_if = "eth1" # Interface facing lab devices
dhcp_role = "primary"
}
]
}
}
```
### 2. Deploy
```bash
# From project root
./deploy.sh
```
Or manually:
```bash
cd terraform
terraform apply
cd ../ansible
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
```
## Verification
### 1. Check WireGuard Status
```bash
# On OpenWrt router
wg show
```
Expected output:
```
interface: wg_lab100
public key: <key>
private key: (hidden)
listening port: 51920
peer: <peer-public-key>
endpoint: 192.168.1.11:51920
allowed ips: 172.16.100.1/32
latest handshake: 30 seconds ago
transfer: 5.2 KiB received, 4.8 KiB sent
```
### 2. Check VXLAN Interface
```bash
ip -d link show vxlan100
```
Expected output:
```
vxlan100@wg_lab100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450
link/ether ... brd ff:ff:ff:ff:ff:ff
vxlan id 100 dev wg_lab100 dstport 4789 ...
```
### 3. Check Bridge
```bash
bridge link show | grep br_lab100
```
Expected output:
```
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> master br_lab100
4: vxlan100: <BROADCAST,MULTICAST,UP,LOWER_UP> master br_lab100
```
### 4. Check DHCP
```bash
# Show DHCP configuration
uci show dhcp.lab100
# Check active leases
cat /tmp/dhcp.leases
```
### 5. Check Firewall
```bash
# Show firewall zones
uci show firewall | grep lab100
# Check iptables rules
iptables -L -n -v | grep lab100
```
### 6. Test Connectivity
```bash
# Ping peer WireGuard IP
ping 172.16.100.1
# Test DHCP by connecting a lab device to eth1
# The device should get an IP in 10.100.0.0/24
```
## Monitoring
### Access Metrics
The OpenWrt gateway exposes Prometheus metrics on port 9586:
```bash
# From management server
curl http://192.168.1.21:9586/metrics/
```
Sample output:
```
# HELP wireguard_tunnel_up WireGuard tunnel status
# TYPE wireguard_tunnel_up gauge
wireguard_tunnel_up{lab="lab100",interface="wg_lab100",peer="abcd1234"} 1
...
```
### View Logs
```bash
# On OpenWrt
logread | grep -E 'wireguard|vxlan|lab100'
# Follow logs in real-time
logread -f | grep -E 'wireguard|vxlan|lab100'
```
## Troubleshooting
### WireGuard Not Starting
```bash
# Check kernel modules
lsmod | grep wireguard
# Reload network
/etc/init.d/network reload
# Check logs
logread | grep wireguard
```
### VXLAN Not Working
```bash
# Manually recreate VXLAN
/etc/init.d/vxlan-lab100 restart
# Check if WireGuard is up first
ip link show wg_lab100
```
### DHCP Not Assigning IPs
```bash
# Restart dnsmasq
/etc/init.d/dnsmasq restart
# Check configuration
uci show dhcp.lab100
# Test with static IP first
ip addr add 10.100.0.50/24 dev br_lab100
```
### Firewall Blocking Traffic
```bash
# Temporarily disable firewall for testing
/etc/init.d/firewall stop
# Check rules
iptables -L -n -v
# Re-enable
/etc/init.d/firewall start
```
### Package Installation Failures
```bash
# Update package lists
opkg update
# Check available space
df -h
# Free up space if needed
opkg remove <unnecessary-packages>
```
## Performance Tuning
### For High-Throughput Labs
```bash
# Increase network buffers
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
# Make permanent
echo "net.core.rmem_max=16777216" >> /etc/sysctl.conf
echo "net.core.wmem_max=16777216" >> /etc/sysctl.conf
```
### For Low-Resource Devices
```bash
# Reduce WireGuard keepalive
uci set network.wg_lab100_peer0.persistent_keepalive='60'
uci commit network
# Limit DHCP lease time
uci set dhcp.lab100.leasetime='1h'
uci commit dhcp
```
## Backup and Recovery
### Create Backup
```bash
# Full configuration backup
sysupgrade -b /tmp/backup-$(date +%Y%m%d).tar.gz
# Download backup
scp root@192.168.1.21:/tmp/backup-*.tar.gz ./
```
### Restore Backup
```bash
# Upload backup
scp backup-20250130.tar.gz root@192.168.1.21:/tmp/
# Restore
sysupgrade -r /tmp/backup-20250130.tar.gz
```
## Integration with Multi-Vendor Labs
OpenWrt gateways are fully compatible with Linux and MikroTik gateways in the same lab network:
```hcl
# Example mixed gateway lab
lab_networks = {
lab100 = {
gateways = [
{ hostname = "gw1", type = "linux" },
{ hostname = "rt1", type = "mikrotik" },
{ hostname = "ap1", type = "openwrt" } # All three work together!
]
}
}
```
All three gateway types:
- ✅ Form full mesh WireGuard tunnels with each other
- ✅ Participate in VXLAN overlay
- ✅ Provide DHCP services (with split ranges or failover)
- ✅ Export metrics to centralized Prometheus
- ✅ Can be managed from same Ansible playbooks
## Next Steps
- Enable road warrior access: Edit `terraform.tfvars` and set `road_warrior = true`
- Add monitoring dashboards in Grafana
- Configure alerting for tunnel failures
- Set up automated backups via cron
## Support
For OpenWrt-specific issues:
- OpenWrt documentation: https://openwrt.org/docs
- Check `/var/log/messages` for system logs
- Use `logread -f` for real-time debugging
- Run health check: `ansible-playbook playbooks/health-check.yml`

61
docs/operations.md Normal file
View File

@@ -0,0 +1,61 @@
# Operations Manual
## Daily Operations
### Health Checks
```bash
ansible-playbook playbooks/health-check.yml
```
### Rolling Updates
```bash
ansible-playbook playbooks/rolling-update.yml
```
## Emergency Procedures
### Emergency Shutdown
```bash
ansible-playbook playbooks/emergency-shutdown.yml
```
### Restore After Shutdown
```bash
ansible-playbook playbooks/restore-lab.yml
```
## Adding/Removing Components
### Add New Lab Network
1. Edit `terraform/terraform.tfvars`
2. Run `terraform apply`
3. Run `ansible-playbook playbooks/site.yml`
### Add Gateway to Existing Lab
1. Edit `terraform/terraform.tfvars`
2. Run `terraform apply`
3. Run `ansible-playbook playbooks/add-gateway.yml`
### Remove Gateway
```bash
ansible-playbook playbooks/remove-gateway.yml
```
## Troubleshooting
### WireGuard Tunnel Down
1. Check WireGuard status: `wg show`
2. Verify peer connectivity: `ping <peer_wg_ip>`
3. Check firewall rules
4. Restart WireGuard: `systemctl restart wg-quick@wg-labXXX`
### DHCP Not Working
1. Check DHCP server status
2. Verify bridge is up
3. Check DHCP lease file
4. Test with static IP first
### VXLAN Issues
1. Verify WireGuard tunnels are up first
2. Check VXLAN interface: `ip -d link show vxlanXXX`
3. Verify FDB entries: `bridge fdb show`

View File

@@ -0,0 +1,27 @@
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""WireGuard Lab Exporter for Prometheus"""
import subprocess
import time
from prometheus_client import start_http_server
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
import json
class WireGuardLabCollector:
def __init__(self):
try:
with open('/etc/wg-lab-exporter/config.json') as f:
self.config = json.load(f)
except:
self.config = {'labs': {}}
def collect(self):
wg_up = GaugeMetricFamily('wireguard_tunnel_up', 'Tunnel status', labels=['lab', 'peer'])
for lab_name in self.config.get('labs', {}):
try:
result = subprocess.run(['wg', 'show', f'wg-{lab_name}'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
wg_up.add_metric([lab_name, 'all'], 1)
else:
wg_up.add_metric([lab_name, 'all'], 0)
except:
wg_up.add_metric([lab_name, 'all'], 0)
yield wg_up
if __name__ == '__main__':
REGISTRY.register(WireGuardLabCollector())
start_http_server(9586)
print("Exporter started on port 9586")
while True:
time.sleep(60)

View File

@@ -0,0 +1,8 @@
global:
scrape_interval: 30s
evaluation_interval: 30s
scrape_configs:
- job_name: 'wireguard-linux'
static_configs:
- targets: ['192.168.1.11:9586', '192.168.1.13:9586']

View File

@@ -0,0 +1,33 @@
# WireGuard Lab Access - {{ item.key }}
## Your Lab Networks
You have access to the following lab networks:
{% for lab in road_warrior_users[item.key].labs %}
- **{{ lab }}**: Configuration file `{{ lab }}.conf`
{% endfor %}
## Setup Instructions
### Desktop/Laptop (Linux/Mac/Windows)
1. Install WireGuard: https://www.wireguard.com/install/
2. Import the `.conf` file for your lab
3. Activate the connection
### Mobile (iOS/Android)
1. Install WireGuard app from App Store/Play Store
2. Open the `{{ item.key }}-qr.html` file
3. Scan the QR code with the app
## Security Notes
- Keep these configuration files secure
- Do NOT share your private key
- Report lost devices immediately
## Support
For issues, contact: IT Support

View File

@@ -0,0 +1,18 @@
[Interface]
# WireGuard configuration for {{ item.value.email }}
# Lab Network: {{ item.value.lab_name }}
# Generated: {{ ansible_date_time.iso8601 }}
PrivateKey = {{ item.value.wg_private_key }}
Address = {{ item.value.wg_ip }}/32
DNS = 8.8.8.8, 8.8.4.4
{% for peer in item.value.peers %}
[Peer]
# Gateway: {{ peer.gateway }}
PublicKey = {{ peer.public_key }}
Endpoint = {{ peer.endpoint }}:{{ peer.listen_port }}
AllowedIPs = {{ item.value.subnet }}
PersistentKeepalive = 25
{% endfor %}

22
terraform/.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/random" {
version = "3.8.1"
constraints = "~> 3.5"
hashes = [
"h1:m2y2fw9SBQ6+e7pNhi3+qsh8bYNmqkL89BulzH7uK3U=",
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
"zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57",
"zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0",
"zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66",
"zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9",
"zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05",
"zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8",
"zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b",
"zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699",
]
}

View File

@@ -0,0 +1,375 @@
Copyright (c) 2017 HashiCorp, Inc.
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -0,0 +1,12 @@
{
"version": 3,
"terraform_version": "1.14.4",
"backend": {
"type": "local",
"config": {
"path": "terraform.tfstate",
"workspace_dir": null
},
"hash": 73024536
}
}

125
terraform/main.tf Normal file
View File

@@ -0,0 +1,125 @@
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
backend "local" {
path = "terraform.tfstate"
}
}
# Generate WireGuard keypairs for gateways
resource "random_password" "wg_private_key" {
for_each = local.all_gateways
length = 44
special = false
}
# Generate WireGuard keypairs for road warrior clients
resource "random_password" "rw_private_key" {
for_each = local.road_warrior_clients
length = 44
special = false
}
locals {
# Flatten all gateways
all_gateways = merge([
for lab_name, lab in var.lab_networks : {
for gw in lab.gateways :
"${lab_name}-${gw.hostname}" => merge(gw, {
lab_name = lab_name
vni = lab.vni
wg_net = lab.wireguard_net
subnet = lab.subnet
dhcp_mode = lab.dhcp_mode
road_warrior = lab.road_warrior
})
}
]...)
# Generate road warrior client entries
road_warrior_clients = merge([
for user_name, user in var.road_warrior_users : {
for lab in user.labs :
"${user_name}-${lab}" => {
user_name = user_name
email = user.email
lab_name = lab
vni = var.lab_networks[lab].vni
wg_net = var.lab_networks[lab].wireguard_net
subnet = var.lab_networks[lab].subnet
}
}
]...)
# Calculate WireGuard IP assignments
gateway_wg_ips = {
for key, gw in local.all_gateways :
key => cidrhost(gw.wg_net, index(var.lab_networks[gw.lab_name].gateways,
[for g in var.lab_networks[gw.lab_name].gateways : g if g.hostname == gw.hostname][0]
) + 1)
}
# Road warrior IP assignments (start after gateways)
rw_wg_ips = {
for key, rw in local.road_warrior_clients :
key => cidrhost(rw.wg_net,
length(var.lab_networks[rw.lab_name].gateways) +
index([for k, v in local.road_warrior_clients : k if v.lab_name == rw.lab_name], key) + 10
)
}
# Generate peer matrices (gateways only mesh with each other, not RW clients)
wg_gateway_peers = {
for lab_name, lab in var.lab_networks : lab_name => [
for i, gw1 in lab.gateways : [
for j, gw2 in lab.gateways :
{
from = gw1.hostname
to = gw2.hostname
to_type = gw2.type
endpoint = gw2.mgmt_ip
wg_ip = local.gateway_wg_ips["${lab_name}-${gw2.hostname}"]
public_key = random_password.wg_private_key["${lab_name}-${gw2.hostname}"].result
} if i != j
]
]
}
# Road warrior peers (each RW client peers with all gateways in their labs)
wg_rw_peers = {
for key, rw in local.road_warrior_clients :
key => [
for gw in var.lab_networks[rw.lab_name].gateways :
{
gateway = gw.hostname
gw_type = gw.type
endpoint = gw.mgmt_ip
wg_ip = local.gateway_wg_ips["${rw.lab_name}-${gw.hostname}"]
public_key = random_password.wg_private_key["${rw.lab_name}-${gw.hostname}"].result
listen_port = 51820 + var.lab_networks[rw.lab_name].vni
}
]
}
}
# Compute public keys from private keys (simplified for demo)
resource "random_id" "wg_public_key" {
for_each = random_password.wg_private_key
byte_length = 32
keepers = {
private_key = each.value.result
}
}
resource "random_id" "rw_public_key" {
for_each = random_password.rw_private_key
byte_length = 32
keepers = {
private_key = each.value.result
}
}

842
terraform/outputs.json Normal file
View File

@@ -0,0 +1,842 @@
{
"ansible_inventory": {
"sensitive": false,
"type": "string",
"value": "\"all\":\n \"children\":\n \"linux_gateways\":\n \"hosts\":\n \"lab100-gw1\":\n \"ansible_host\": \"192.168.1.11\"\n \"lab_name\": \"lab100\"\n \"mikrotik_gateways\":\n \"hosts\":\n \"lab100-rt1\":\n \"ansible_connection\": \"network_cli\"\n \"ansible_host\": \"192.168.1.12\"\n \"ansible_network_os\": \"routeros\"\n \"ansible_port\": 8728\n \"lab_name\": \"lab100\"\n \"openwrt_gateways\":\n \"hosts\":\n \"lab100-openwrt1\":\n \"ansible_host\": \"192.168.1.14\"\n \"ansible_port\": 22\n \"lab_name\": \"lab100\"\n \"lab200-openwrt1\":\n \"ansible_host\": \"192.168.1.21\"\n \"ansible_port\": 22\n \"lab_name\": \"lab200\"\n \"lab200-openwrt2\":\n \"ansible_host\": \"192.168.1.22\"\n \"ansible_port\": 22\n \"lab_name\": \"lab200\"\n"
},
"gateway_config": {
"sensitive": true,
"type": [
"object",
{
"lab100-lab100-gw1": [
"object",
{
"api_port": "number",
"dhcp_mode": "string",
"dhcp_role": "string",
"hostname": "string",
"lab_if": "string",
"lab_name": "string",
"mgmt_ip": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
]
]
],
"road_warrior": "bool",
"rw_clients": [
"list",
[
"object",
{
"email": "string",
"public_key": "string",
"user_name": "string",
"wg_ip": "string"
}
]
],
"subnet": "string",
"type": "string",
"vni": "number",
"wg_ip": "string",
"wg_listen_port": "number",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"lab100-lab100-openwrt1": [
"object",
{
"api_port": "number",
"dhcp_mode": "string",
"dhcp_role": "string",
"hostname": "string",
"lab_if": "string",
"lab_name": "string",
"mgmt_ip": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
]
]
],
"road_warrior": "bool",
"rw_clients": [
"list",
[
"object",
{
"email": "string",
"public_key": "string",
"user_name": "string",
"wg_ip": "string"
}
]
],
"subnet": "string",
"type": "string",
"vni": "number",
"wg_ip": "string",
"wg_listen_port": "number",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"lab100-lab100-rt1": [
"object",
{
"api_port": "number",
"dhcp_mode": "string",
"dhcp_role": "string",
"hostname": "string",
"lab_if": "string",
"lab_name": "string",
"mgmt_ip": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
]
]
],
"road_warrior": "bool",
"rw_clients": [
"list",
[
"object",
{
"email": "string",
"public_key": "string",
"user_name": "string",
"wg_ip": "string"
}
]
],
"subnet": "string",
"type": "string",
"vni": "number",
"wg_ip": "string",
"wg_listen_port": "number",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"lab200-lab200-openwrt1": [
"object",
{
"api_port": "number",
"dhcp_mode": "string",
"dhcp_role": "string",
"hostname": "string",
"lab_if": "string",
"lab_name": "string",
"mgmt_ip": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
]
]
],
"road_warrior": "bool",
"rw_clients": [
"list",
[
"object",
{
"email": "string",
"public_key": "string",
"user_name": "string",
"wg_ip": "string"
}
]
],
"subnet": "string",
"type": "string",
"vni": "number",
"wg_ip": "string",
"wg_listen_port": "number",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"lab200-lab200-openwrt2": [
"object",
{
"api_port": "number",
"dhcp_mode": "string",
"dhcp_role": "string",
"hostname": "string",
"lab_if": "string",
"lab_name": "string",
"mgmt_ip": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"from": "string",
"public_key": "string",
"to": "string",
"to_type": "string",
"wg_ip": "string"
}
]
]
],
"road_warrior": "bool",
"rw_clients": [
"list",
[
"object",
{
"email": "string",
"public_key": "string",
"user_name": "string",
"wg_ip": "string"
}
]
],
"subnet": "string",
"type": "string",
"vni": "number",
"wg_ip": "string",
"wg_listen_port": "number",
"wg_private_key": "string",
"wg_public_key": "string"
}
]
}
],
"value": {
"lab100-lab100-gw1": {
"api_port": 22,
"dhcp_mode": "failover",
"dhcp_role": "primary",
"hostname": "lab100-gw1",
"lab_if": "eth1",
"lab_name": "lab100",
"mgmt_ip": "192.168.1.11",
"peers": [
{
"endpoint": "192.168.1.12",
"from": "lab100-gw1",
"public_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"to": "lab100-rt1",
"to_type": "mikrotik",
"wg_ip": "172.16.100.2"
},
{
"endpoint": "192.168.1.14",
"from": "lab100-gw1",
"public_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"to": "lab100-openwrt1",
"to_type": "openwrt",
"wg_ip": "172.16.100.3"
}
],
"road_warrior": true,
"rw_clients": [
{
"email": "alice.smith@company.com",
"public_key": "e678c242ea2f29548224f4b469030be3bb9a65c6858fdf58d8eadc2c3305d91f",
"user_name": "asmith",
"wg_ip": "172.16.100.13"
},
{
"email": "lutz.finsterle@mahle.com",
"public_key": "bd99a73acd6f6d7c925996d2aaa4aa322e7443d1bd857580b2cfbbf7dd6086ab",
"user_name": "finstl1",
"wg_ip": "172.16.100.14"
},
{
"email": "john.doe@company.com",
"public_key": "8d25d50ed5012d300f52770233c7517cd589d9be9f761e8bf4612dc904369c23",
"user_name": "jdoe",
"wg_ip": "172.16.100.15"
}
],
"subnet": "10.100.0.0/24",
"type": "linux",
"vni": 100,
"wg_ip": "172.16.100.1",
"wg_listen_port": 51920,
"wg_private_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"wg_public_key": "54c1598be162afda063f4dd3ba968f94ac3a79998b441c9dcebef3e1f800edc9"
},
"lab100-lab100-openwrt1": {
"api_port": 22,
"dhcp_mode": "failover",
"dhcp_role": "backup",
"hostname": "lab100-openwrt1",
"lab_if": "eth1",
"lab_name": "lab100",
"mgmt_ip": "192.168.1.14",
"peers": [
{
"endpoint": "192.168.1.11",
"from": "lab100-openwrt1",
"public_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"to": "lab100-gw1",
"to_type": "linux",
"wg_ip": "172.16.100.1"
},
{
"endpoint": "192.168.1.12",
"from": "lab100-openwrt1",
"public_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"to": "lab100-rt1",
"to_type": "mikrotik",
"wg_ip": "172.16.100.2"
}
],
"road_warrior": true,
"rw_clients": [
{
"email": "alice.smith@company.com",
"public_key": "e678c242ea2f29548224f4b469030be3bb9a65c6858fdf58d8eadc2c3305d91f",
"user_name": "asmith",
"wg_ip": "172.16.100.13"
},
{
"email": "lutz.finsterle@mahle.com",
"public_key": "bd99a73acd6f6d7c925996d2aaa4aa322e7443d1bd857580b2cfbbf7dd6086ab",
"user_name": "finstl1",
"wg_ip": "172.16.100.14"
},
{
"email": "john.doe@company.com",
"public_key": "8d25d50ed5012d300f52770233c7517cd589d9be9f761e8bf4612dc904369c23",
"user_name": "jdoe",
"wg_ip": "172.16.100.15"
}
],
"subnet": "10.100.0.0/24",
"type": "openwrt",
"vni": 100,
"wg_ip": "172.16.100.3",
"wg_listen_port": 51920,
"wg_private_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"wg_public_key": "121507f174a5822f22f1e939b2940dfdb83aa6f2ed2f9d9032a02435f2fe1838"
},
"lab100-lab100-rt1": {
"api_port": 8728,
"dhcp_mode": "failover",
"dhcp_role": "secondary",
"hostname": "lab100-rt1",
"lab_if": "ether2",
"lab_name": "lab100",
"mgmt_ip": "192.168.1.12",
"peers": [
{
"endpoint": "192.168.1.11",
"from": "lab100-rt1",
"public_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"to": "lab100-gw1",
"to_type": "linux",
"wg_ip": "172.16.100.1"
},
{
"endpoint": "192.168.1.14",
"from": "lab100-rt1",
"public_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"to": "lab100-openwrt1",
"to_type": "openwrt",
"wg_ip": "172.16.100.3"
}
],
"road_warrior": true,
"rw_clients": [
{
"email": "alice.smith@company.com",
"public_key": "e678c242ea2f29548224f4b469030be3bb9a65c6858fdf58d8eadc2c3305d91f",
"user_name": "asmith",
"wg_ip": "172.16.100.13"
},
{
"email": "lutz.finsterle@mahle.com",
"public_key": "bd99a73acd6f6d7c925996d2aaa4aa322e7443d1bd857580b2cfbbf7dd6086ab",
"user_name": "finstl1",
"wg_ip": "172.16.100.14"
},
{
"email": "john.doe@company.com",
"public_key": "8d25d50ed5012d300f52770233c7517cd589d9be9f761e8bf4612dc904369c23",
"user_name": "jdoe",
"wg_ip": "172.16.100.15"
}
],
"subnet": "10.100.0.0/24",
"type": "mikrotik",
"vni": 100,
"wg_ip": "172.16.100.2",
"wg_listen_port": 51920,
"wg_private_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"wg_public_key": "0ad05299182076819239dc37e57e02fc1ce7ff1fca122c797bf891802da5940f"
},
"lab200-lab200-openwrt1": {
"api_port": 22,
"dhcp_mode": "simple",
"dhcp_role": "primary",
"hostname": "lab200-openwrt1",
"lab_if": "lan1",
"lab_name": "lab200",
"mgmt_ip": "192.168.1.21",
"peers": [
{
"endpoint": "192.168.1.22",
"from": "lab200-openwrt1",
"public_key": "LbBa8hG48wqJCOPQgEi4LKIWE5nrK1HiLQdsIlbxblMW",
"to": "lab200-openwrt2",
"to_type": "openwrt",
"wg_ip": "172.16.200.2"
}
],
"road_warrior": false,
"rw_clients": [],
"subnet": "10.200.0.0/24",
"type": "openwrt",
"vni": 200,
"wg_ip": "172.16.200.1",
"wg_listen_port": 52020,
"wg_private_key": "AQMhcavQwY7RBBk3qnDBP3sxjTGie6Fw6QUvwnLXI3Yo",
"wg_public_key": "1045e3900850df7aba0061f1fd0dde7e96c2c6b44903b5c769308c5a89ba5116"
},
"lab200-lab200-openwrt2": {
"api_port": 22,
"dhcp_mode": "simple",
"dhcp_role": "secondary",
"hostname": "lab200-openwrt2",
"lab_if": "lan1",
"lab_name": "lab200",
"mgmt_ip": "192.168.1.22",
"peers": [
{
"endpoint": "192.168.1.21",
"from": "lab200-openwrt2",
"public_key": "AQMhcavQwY7RBBk3qnDBP3sxjTGie6Fw6QUvwnLXI3Yo",
"to": "lab200-openwrt1",
"to_type": "openwrt",
"wg_ip": "172.16.200.1"
}
],
"road_warrior": false,
"rw_clients": [],
"subnet": "10.200.0.0/24",
"type": "openwrt",
"vni": 200,
"wg_ip": "172.16.200.2",
"wg_listen_port": 52020,
"wg_private_key": "LbBa8hG48wqJCOPQgEi4LKIWE5nrK1HiLQdsIlbxblMW",
"wg_public_key": "33e4ef4b6e8dbcf6b78ba1652eed1941149578a308429776a82b779db70ff179"
}
}
},
"road_warrior_configs": {
"sensitive": true,
"type": [
"object",
{
"asmith-lab100": [
"object",
{
"email": "string",
"lab_name": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
]
]
],
"subnet": "string",
"user_name": "string",
"wg_ip": "string",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"finstl1-lab100": [
"object",
{
"email": "string",
"lab_name": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
]
]
],
"subnet": "string",
"user_name": "string",
"wg_ip": "string",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"finstl1-lab200": [
"object",
{
"email": "string",
"lab_name": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
]
]
],
"subnet": "string",
"user_name": "string",
"wg_ip": "string",
"wg_private_key": "string",
"wg_public_key": "string"
}
],
"jdoe-lab100": [
"object",
{
"email": "string",
"lab_name": "string",
"peers": [
"tuple",
[
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
],
[
"object",
{
"endpoint": "string",
"gateway": "string",
"gw_type": "string",
"listen_port": "number",
"public_key": "string",
"wg_ip": "string"
}
]
]
],
"subnet": "string",
"user_name": "string",
"wg_ip": "string",
"wg_private_key": "string",
"wg_public_key": "string"
}
]
}
],
"value": {
"asmith-lab100": {
"email": "alice.smith@company.com",
"lab_name": "lab100",
"peers": [
{
"endpoint": "192.168.1.11",
"gateway": "lab100-gw1",
"gw_type": "linux",
"listen_port": 51920,
"public_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"wg_ip": "172.16.100.1"
},
{
"endpoint": "192.168.1.12",
"gateway": "lab100-rt1",
"gw_type": "mikrotik",
"listen_port": 51920,
"public_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"wg_ip": "172.16.100.2"
},
{
"endpoint": "192.168.1.14",
"gateway": "lab100-openwrt1",
"gw_type": "openwrt",
"listen_port": 51920,
"public_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"wg_ip": "172.16.100.3"
}
],
"subnet": "10.100.0.0/24",
"user_name": "asmith",
"wg_ip": "172.16.100.13",
"wg_private_key": "WzAAWAf9paoVPnRblDVBMUqfL2ieuMDeaEwr8xLgP3Hm",
"wg_public_key": "e678c242ea2f29548224f4b469030be3bb9a65c6858fdf58d8eadc2c3305d91f"
},
"finstl1-lab100": {
"email": "lutz.finsterle@mahle.com",
"lab_name": "lab100",
"peers": [
{
"endpoint": "192.168.1.11",
"gateway": "lab100-gw1",
"gw_type": "linux",
"listen_port": 51920,
"public_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"wg_ip": "172.16.100.1"
},
{
"endpoint": "192.168.1.12",
"gateway": "lab100-rt1",
"gw_type": "mikrotik",
"listen_port": 51920,
"public_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"wg_ip": "172.16.100.2"
},
{
"endpoint": "192.168.1.14",
"gateway": "lab100-openwrt1",
"gw_type": "openwrt",
"listen_port": 51920,
"public_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"wg_ip": "172.16.100.3"
}
],
"subnet": "10.100.0.0/24",
"user_name": "finstl1",
"wg_ip": "172.16.100.14",
"wg_private_key": "qGbsVHrl6vOdqrVN0xxWzbzgJUGCCb6MZt4FEhZUS6WV",
"wg_public_key": "bd99a73acd6f6d7c925996d2aaa4aa322e7443d1bd857580b2cfbbf7dd6086ab"
},
"finstl1-lab200": {
"email": "lutz.finsterle@mahle.com",
"lab_name": "lab200",
"peers": [
{
"endpoint": "192.168.1.21",
"gateway": "lab200-openwrt1",
"gw_type": "openwrt",
"listen_port": 52020,
"public_key": "AQMhcavQwY7RBBk3qnDBP3sxjTGie6Fw6QUvwnLXI3Yo",
"wg_ip": "172.16.200.1"
},
{
"endpoint": "192.168.1.22",
"gateway": "lab200-openwrt2",
"gw_type": "openwrt",
"listen_port": 52020,
"public_key": "LbBa8hG48wqJCOPQgEi4LKIWE5nrK1HiLQdsIlbxblMW",
"wg_ip": "172.16.200.2"
}
],
"subnet": "10.200.0.0/24",
"user_name": "finstl1",
"wg_ip": "172.16.200.12",
"wg_private_key": "NsbuQIZK3NSpl8PlFcr25W11qH7PYplhxku2YjMGMh6j",
"wg_public_key": "d64d224ad309c89ebb46b5626fe3392d7be16cede395dedf75d44665778f4dcf"
},
"jdoe-lab100": {
"email": "john.doe@company.com",
"lab_name": "lab100",
"peers": [
{
"endpoint": "192.168.1.11",
"gateway": "lab100-gw1",
"gw_type": "linux",
"listen_port": 51920,
"public_key": "XivDZJP1Jt0hMzkg28XcbImykp2OepiajCdnSThpJ4EN",
"wg_ip": "172.16.100.1"
},
{
"endpoint": "192.168.1.12",
"gateway": "lab100-rt1",
"gw_type": "mikrotik",
"listen_port": 51920,
"public_key": "uRgOFY6Lml5bm1SkpMnJCDR7tqBu8U8KWinrzvwPwtq8",
"wg_ip": "172.16.100.2"
},
{
"endpoint": "192.168.1.14",
"gateway": "lab100-openwrt1",
"gw_type": "openwrt",
"listen_port": 51920,
"public_key": "kJ8w03s95KxcROZ4MDn0JGeqeZLBhkKoduoaiPIWvRUm",
"wg_ip": "172.16.100.3"
}
],
"subnet": "10.100.0.0/24",
"user_name": "jdoe",
"wg_ip": "172.16.100.15",
"wg_private_key": "yDIq0PioXao7aLR0e6FIhQH58NkFSH8q1EDSqvDIE6K1",
"wg_public_key": "8d25d50ed5012d300f52770233c7517cd589d9be9f761e8bf4612dc904369c23"
}
}
}
}

92
terraform/outputs.tf Normal file
View File

@@ -0,0 +1,92 @@
output "gateway_config" {
value = {
for key, gw in local.all_gateways : key => {
hostname = gw.hostname
type = gw.type
mgmt_ip = gw.mgmt_ip
api_port = gw.api_port
lab_name = gw.lab_name
vni = gw.vni
subnet = gw.subnet
lab_if = gw.lab_if
dhcp_role = gw.dhcp_role
dhcp_mode = gw.dhcp_mode
road_warrior = gw.road_warrior
wg_private_key = random_password.wg_private_key[key].result
wg_public_key = random_id.wg_public_key[key].hex
wg_ip = local.gateway_wg_ips[key]
wg_listen_port = 51820 + gw.vni
peers = [
for peer_list in local.wg_gateway_peers[gw.lab_name] :
peer_list if peer_list[0].from == gw.hostname
][0]
# Road warrior clients that should be allowed
rw_clients = gw.road_warrior ? [
for rw_key, rw in local.road_warrior_clients :
{
user_name = rw.user_name
email = rw.email
wg_ip = local.rw_wg_ips[rw_key]
public_key = random_id.rw_public_key[rw_key].hex
} if rw.lab_name == gw.lab_name
] : []
}
}
sensitive = true
}
output "road_warrior_configs" {
value = {
for key, rw in local.road_warrior_clients : key => {
user_name = rw.user_name
email = rw.email
lab_name = rw.lab_name
subnet = rw.subnet
wg_private_key = random_password.rw_private_key[key].result
wg_public_key = random_id.rw_public_key[key].hex
wg_ip = local.rw_wg_ips[key]
peers = local.wg_rw_peers[key]
}
}
sensitive = true
}
output "ansible_inventory" {
value = yamlencode({
all = {
children = {
linux_gateways = {
hosts = {
for key, gw in local.all_gateways :
gw.hostname => {
ansible_host = gw.mgmt_ip
lab_name = gw.lab_name
} if gw.type == "linux"
}
}
mikrotik_gateways = {
hosts = {
for key, gw in local.all_gateways :
gw.hostname => {
ansible_host = gw.mgmt_ip
ansible_connection = "network_cli"
ansible_network_os = "routeros"
ansible_port = gw.api_port
lab_name = gw.lab_name
} if gw.type == "mikrotik"
}
}
openwrt_gateways = {
hosts = {
for key, gw in local.all_gateways :
gw.hostname => {
ansible_host = gw.mgmt_ip
ansible_port = gw.api_port
lab_name = gw.lab_name
} if gw.type == "openwrt"
}
}
}
}
})
}

1521
terraform/terraform.tfstate Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "failover" # ISC DHCP with failover
road_warrior = true
gateways = [
{
hostname = "lab100-gw1"
type = "linux"
mgmt_ip = "192.168.1.11"
api_port = 22
lab_if = "eth1"
dhcp_role = "primary"
},
{
hostname = "lab100-rt1"
type = "mikrotik"
mgmt_ip = "192.168.1.12"
api_port = 8728
lab_if = "ether2"
dhcp_role = "secondary"
},
{
hostname = "lab100-openwrt1"
type = "openwrt"
mgmt_ip = "192.168.1.14"
api_port = 22
lab_if = "eth1"
dhcp_role = "backup"
}
]
}
lab200 = {
vni = 200
subnet = "10.200.0.0/24"
wireguard_net = "172.16.200.0/24"
dhcp_mode = "simple" # dnsmasq with split ranges
road_warrior = false
gateways = [
{
hostname = "lab200-openwrt1"
type = "openwrt"
mgmt_ip = "192.168.1.21"
api_port = 22
lab_if = "lan1"
dhcp_role = "primary"
},
{
hostname = "lab200-openwrt2"
type = "openwrt"
mgmt_ip = "192.168.1.22"
api_port = 22
lab_if = "lan1"
dhcp_role = "secondary"
}
]
}
}
road_warrior_users = {
"jdoe" = {
email = "john.doe@company.com"
labs = ["lab100"]
}
"asmith" = {
email = "alice.smith@company.com"
labs = ["lab100"]
}
"finstl1" = {
email = "lutz.finsterle@mahle.com"
labs = ["lab100","lab200"]
}
}

View File

@@ -0,0 +1,72 @@
lab_networks = {
lab100 = {
vni = 100
subnet = "10.100.0.0/24"
wireguard_net = "172.16.100.0/24"
dhcp_mode = "failover" # ISC DHCP with failover
road_warrior = true
gateways = [
{
hostname = "lab100-gw1"
type = "linux"
mgmt_ip = "192.168.1.11"
api_port = 22
lab_if = "eth1"
dhcp_role = "primary"
},
{
hostname = "lab100-rt1"
type = "mikrotik"
mgmt_ip = "192.168.1.12"
api_port = 8728
lab_if = "ether2"
dhcp_role = "secondary"
},
{
hostname = "lab100-openwrt1"
type = "openwrt"
mgmt_ip = "192.168.1.14"
api_port = 22
lab_if = "eth1"
dhcp_role = "backup"
}
]
}
lab200 = {
vni = 200
subnet = "10.200.0.0/24"
wireguard_net = "172.16.200.0/24"
dhcp_mode = "simple" # dnsmasq with split ranges
road_warrior = false
gateways = [
{
hostname = "lab200-openwrt1"
type = "openwrt"
mgmt_ip = "192.168.1.21"
api_port = 22
lab_if = "lan1"
dhcp_role = "primary"
},
{
hostname = "lab200-openwrt2"
type = "openwrt"
mgmt_ip = "192.168.1.22"
api_port = 22
lab_if = "lan1"
dhcp_role = "secondary"
}
]
}
}
road_warrior_users = {
"jdoe" = {
email = "john.doe@company.com"
labs = ["lab100"]
}
"asmith" = {
email = "alice.smith@company.com"
labs = ["lab100"]
}
}

27
terraform/variables.tf Normal file
View File

@@ -0,0 +1,27 @@
variable "lab_networks" {
description = "Map of lab networks with their configuration"
type = map(object({
vni = number
subnet = string
wireguard_net = string
dhcp_mode = string
road_warrior = bool
gateways = list(object({
hostname = string
type = string
mgmt_ip = string
api_port = number
lab_if = string
dhcp_role = string
}))
}))
}
variable "road_warrior_users" {
description = "Map of road warrior users and their lab access"
type = map(object({
email = string
labs = list(string)
}))
default = {}
}