feat: add homelab config skills (VLAN segmentation, Pi-hole DNS, WireGuard VPN) (#1838)

* feat: add homelab config skills (VLAN, Pi-hole, WireGuard)

Adds three homelab configuration skills, extracted from the stale PR #1413
with the same safety treatment applied to the previously accepted batch:

- homelab-vlan-segmentation: IoT/guest/trusted/server VLAN design for UniFi,
  pfSense/OPNsense, and MikroTik. All firewall rules add isolation, not remove
  protections. Added change-window guidance and AP trunk port clarification.

- homelab-pihole-dns: Pi-hole install, blocklists, DNS-over-HTTPS, local DNS
  records, troubleshooting. Docker is now the lead install method; bare-metal
  uses inspect-first pattern before running the installer script.

- homelab-wireguard-vpn: WireGuard server, peer config, split tunnel, DDNS.
  Replaced broad iptables FORWARD ACCEPT with scoped directional rules
  (wg0→eth0 forward + established return only). Credentials moved to env
  files with explicit notes against inline secrets and version control.

Continues the contribution from PR #1413; the eight skills/agents from
that PR are already in main via #1729 and #1731.

* docs: harden homelab skill pack

---------

Co-authored-by: Affaan Mustafa <affaan@dcube.ai>
This commit is contained in:
Arsal Sajjad 2026-05-12 18:20:53 -07:00 committed by GitHub
parent 7f3dfde6d7
commit 71ed7c58d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 903 additions and 13 deletions

View File

@ -11,7 +11,7 @@
{
"name": "ecc",
"source": "./",
"description": "The most comprehensive Claude Code plugin — 60 agents, 225 skills, 75 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
"description": "The most comprehensive Claude Code plugin — 60 agents, 228 skills, 75 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
"version": "2.0.0-rc.1",
"author": {
"name": "Affaan Mustafa",

View File

@ -1,7 +1,7 @@
{
"name": "ecc",
"version": "2.0.0-rc.1",
"description": "Battle-tested Claude Code plugin for engineering teams — 60 agents, 225 skills, 75 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
"description": "Battle-tested Claude Code plugin for engineering teams — 60 agents, 228 skills, 75 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
"author": {
"name": "Affaan Mustafa",
"url": "https://x.com/affaanmustafa"

View File

@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
This is a **production-ready AI coding plugin** providing 60 specialized agents, 225 skills, 75 commands, and automated hook workflows for software development.
This is a **production-ready AI coding plugin** providing 60 specialized agents, 228 skills, 75 commands, and automated hook workflows for software development.
**Version:** 2.0.0-rc.1
@ -150,7 +150,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 60 specialized subagents
skills/ — 225 workflow skills and domain knowledge
skills/ — 228 workflow skills and domain knowledge
commands/ — 75 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)

View File

@ -358,7 +358,7 @@ If you stacked methods, clean up in this order:
/plugin list ecc@ecc
```
**That's it!** You now have access to 60 agents, 225 skills, and 75 legacy command shims.
**That's it!** You now have access to 60 agents, 228 skills, and 75 legacy command shims.
### Dashboard GUI
@ -1362,7 +1362,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|---------|-------------|----------|--------|
| Agents | PASS: 60 agents | PASS: 12 agents | **Claude Code leads** |
| Commands | PASS: 75 commands | PASS: 35 commands | **Claude Code leads** |
| Skills | PASS: 225 skills | PASS: 37 skills | **Claude Code leads** |
| Skills | PASS: 228 skills | PASS: 37 skills | **Claude Code leads** |
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
@ -1467,7 +1467,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|---------|------------|------------|-----------|----------|
| **Agents** | 60 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
| **Commands** | 75 | Shared | Instruction-based | 35 |
| **Skills** | 225 | Shared | 10 (native format) | 37 |
| **Skills** | 228 | Shared | 10 (native format) | 37 |
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |

View File

@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
**完成!** 你现在可以使用 60 个代理、225 个技能和 75 个命令。
**完成!** 你现在可以使用 60 个代理、228 个技能和 75 个命令。
### multi-* 命令需要额外配置

View File

@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、225 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、228 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 2.0.0-rc.1
@ -147,7 +147,7 @@
```
agents/ — 60 个专业子代理
skills/ — 225 个工作流技能和领域知识
skills/ — 228 个工作流技能和领域知识
commands/ — 75 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)

View File

@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
**搞定!** 你现在可以使用 60 个智能体、225 项技能和 75 个命令了。
**搞定!** 你现在可以使用 60 个智能体、228 项技能和 75 个命令了。
***
@ -1138,7 +1138,7 @@ opencode
|---------|-------------|----------|--------|
| 智能体 | PASS: 60 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 75 个 | PASS: 35 个 | **Claude Code 领先** |
| 技能 | PASS: 225 项 | PASS: 37 项 | **Claude Code 领先** |
| 技能 | PASS: 228 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
@ -1246,7 +1246,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|---------|------------|------------|-----------|----------|
| **智能体** | 60 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 75 | 共享 | 基于指令 | 35 |
| **技能** | 225 | 共享 | 10 (原生格式) | 37 |
| **技能** | 228 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |

View File

@ -0,0 +1,274 @@
---
name: homelab-pihole-dns
description: Pi-hole installation, blocklist management, DNS-over-HTTPS setup, DHCP integration, local DNS records, and troubleshooting broken DNS resolution on a home network.
origin: community
---
# Homelab Pi-hole DNS
Pi-hole is a network-wide DNS ad blocker that runs on a Raspberry Pi or any Linux host.
Every device on your network gets ad and malware domain blocking automatically — no browser
extension needed.
## When to Use
- Installing Pi-hole on a Raspberry Pi or Linux host
- Configuring Pi-hole as the DNS server for a home network
- Adding or managing blocklists
- Setting up DNS-over-HTTPS (DoH) upstream resolvers
- Creating local DNS records (e.g. `nas.home.lan`, `pi.home.lan`)
- Troubleshooting devices that lose internet access after Pi-hole is installed
- Running Pi-hole alongside or instead of DHCP
## How Pi-hole Works
```
Normal flow (without Pi-hole):
Device → requests ads.tracker.com → ISP DNS → real IP → ads load
With Pi-hole:
Device → requests ads.tracker.com → Pi-hole DNS → blocked (returns 0.0.0.0) → no ad
All DNS queries go through Pi-hole first.
Pi-hole checks against blocklists.
Blocked domains return a null response — the ad/tracker never loads.
Allowed domains get forwarded to your upstream resolver (Cloudflare, Google, etc.).
```
## Installation
### Docker (Recommended)
Docker is the easiest way to install Pi-hole and makes updates and backups
straightforward.
```yaml
# docker-compose.yml
services:
pihole:
image: pihole/pihole:<pinned-release-tag>
container_name: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp" # Web admin
environment:
TZ: "America/New_York"
WEBPASSWORD: "${PIHOLE_WEBPASSWORD}" # set via .env file or secret
PIHOLE_DNS_: "1.1.1.1;1.0.0.1"
DNSMASQ_LISTENING: "all"
volumes:
- "./etc-pihole:/etc/pihole"
- "./etc-dnsmasq.d:/etc/dnsmasq.d"
restart: unless-stopped
cap_add:
- NET_ADMIN # only needed if Pi-hole will serve DHCP
```
Replace `<pinned-release-tag>` with a current Pi-hole release tag before deploying.
Avoid `latest` for long-lived DNS infrastructure so upgrades are deliberate and
reviewable.
Set `PIHOLE_WEBPASSWORD` in a `.env` file next to `docker-compose.yml`, chmod it to
`600`, and keep it out of git — do not put the password directly in the compose file.
Access web admin at: `http://<pi-ip>/admin`
### Bare-Metal Install (Raspberry Pi OS / Debian / Ubuntu)
Pi-hole requires a static IP before installing.
```bash
# Step 1: Assign a static IP (edit /etc/dhcpcd.conf on Pi OS)
sudo nano /etc/dhcpcd.conf
# Add at the bottom:
interface eth0
static ip_address=192.168.3.2/24
static routers=192.168.3.1
static domain_name_servers=192.168.3.1
# Step 2: Download and inspect the installer before running it.
# Prefer the package or installer path documented by Pi-hole for your OS/version.
curl -sSL https://install.pi-hole.net -o pi-hole-install.sh
less pi-hole-install.sh # review before proceeding
# Step 3: Run
bash pi-hole-install.sh
# Follow the interactive installer:
# 1. Select network interface (eth0 for wired — recommended)
# 2. Select upstream DNS (Cloudflare or leave default — can change later)
# 3. Confirm static IP
# 4. Install the web admin interface (recommended)
# 5. Note the admin password shown at the end
```
## Pointing Your Network at Pi-hole
```
# Method 1: Change DNS in your router DHCP settings (recommended)
Router admin UI → DHCP Settings → DNS Server
Primary DNS: 192.168.3.2 (Pi-hole IP)
Secondary DNS: leave blank for strict blocking, or use a second Pi-hole.
A public fallback such as 1.1.1.1 improves availability during
rollout but can bypass blocking because clients may query it.
All devices get Pi-hole as DNS automatically on next DHCP renewal.
Force renewal: reconnect Wi-Fi or run 'sudo dhclient -r && sudo dhclient' on Linux
# Method 2: Per-device DNS (useful for testing before network-wide rollout)
Windows: Control Panel → Network Adapter → IPv4 Properties → set DNS manually
macOS: System Settings → Network → Details → DNS → set manually
Linux: /etc/resolv.conf or NetworkManager
# Method 3: Pi-hole as DHCP server (replaces router DHCP)
Pi-hole admin → Settings → DHCP → Enable
Disable DHCP on your router first — two DHCP servers on the same network cause conflicts
Advantage: hostname resolution works automatically (devices register their names)
```
## Blocklist Management
```
# Pi-hole admin → Adlists → Add new adlist
# Recommended blocklists:
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
# default — 200k+ domains
https://blocklistproject.github.io/Lists/malware.txt
# malware domains
https://blocklistproject.github.io/Lists/tracking.txt
# tracking/telemetry
# After adding a list:
Tools → Update Gravity (downloads and compiles all blocklists)
# If a site is blocked that should not be (false positive):
Pi-hole admin → Whitelist → Add domain
Example: api.my-legitimate-service.com
# Check what is being blocked in real time:
Dashboard → Query Log (live DNS query stream with block/allow status)
```
## DNS-over-HTTPS Upstream
DNS-over-HTTPS encrypts your DNS queries so your ISP cannot see what sites you resolve.
```bash
# Install cloudflared (Cloudflare's DoH proxy).
# Prefer Cloudflare's package repository for automatic signed package verification.
# If you download a binary directly, pin a release version and verify its checksum.
CLOUDFLARED_VERSION="<pinned-version>"
curl -LO "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64"
# Verify the checksum/signature from Cloudflare's release notes before installing.
sudo mv cloudflared-linux-arm64 /usr/local/bin/cloudflared
sudo chmod +x /usr/local/bin/cloudflared
# Create cloudflared config
sudo mkdir -p /etc/cloudflared
sudo tee /etc/cloudflared/config.yml << EOF
proxy-dns: true
proxy-dns-port: 5053
proxy-dns-upstream:
- https://1.1.1.1/dns-query
- https://1.0.0.1/dns-query
EOF
# Create systemd service
sudo cloudflared service install
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
# Now point Pi-hole at the local DoH proxy:
# Pi-hole admin → Settings → DNS → Custom upstream DNS
# Set to: 127.0.0.1#5053
# Uncheck all other upstream resolvers
```
## Local DNS Records
Make your services reachable by name (e.g. `nas.home.lan`, `grafana.home.lan`).
> **Domain name note:** `.home.lan` is widely used in homelabs and works in practice.
> The IETF-reserved suffix for local use is `.home.arpa` (RFC 8375) — use that to
> follow the standard. Avoid `.local` for Pi-hole DNS records as it conflicts with
> mDNS/Bonjour.
```
# Pi-hole admin → Local DNS → DNS Records
Domain IP
nas.home.lan 192.168.30.10
pi.home.lan 192.168.30.2
grafana.home.lan 192.168.30.3
proxmox.home.lan 192.168.30.4
# From any device on your network:
ping nas.home.lan → 192.168.30.10
http://grafana.home.lan → your Grafana dashboard
# For subdomains, add a CNAME:
Pi-hole admin → Local DNS → CNAME Records
Domain: portainer.home.lan → Target: pi.home.lan
```
## Troubleshooting
```bash
# Pi-hole blocking something it should not
pihole -q example.com # Check if domain is blocked and which list
pihole -w example.com # Whitelist immediately
# DNS not resolving at all
pihole status # Check if pihole-FTL is running
dig @192.168.3.2 google.com # Test DNS directly against Pi-hole
# Restart Pi-hole DNS
pihole restartdns
# Check query logs for a specific device
pihole -t # Live tail of all queries
# Or filter by client in the web admin Query Log
# Pi-hole gravity update (refresh blocklists)
pihole -g
```
## Anti-Patterns
```
# BAD: Depending on one Pi-hole without a recovery path
# If Pi-hole crashes or the Pi loses power, DNS can stop working
# GOOD: Keep a documented router fallback for rollback during setup
# BETTER: Run two Pi-hole instances for redundancy; avoid public fallback DNS for strict blocking
# BAD: Installing Pi-hole without a static IP
# If the Pi gets a new DHCP IP, all devices lose DNS
# GOOD: Set static IP first, then install Pi-hole
# BAD: Enabling Pi-hole DHCP without disabling the router's DHCP first
# Two DHCP servers on the same network hand out conflicting IPs
# GOOD: Disable router DHCP, then enable Pi-hole DHCP
# BAD: Never updating gravity (blocklists)
# New ad and malware domains accumulate — stale lists miss them
# GOOD: Schedule weekly gravity update: pihole -g (or enable in Settings → API)
```
## Best Practices
- Give the Pi a static IP or DHCP reservation before installing Pi-hole
- Use Pi-hole as primary DNS; for redundancy, add a second Pi-hole instead of a
public resolver if you need strict blocking
- Enable DoH (DNS-over-HTTPS) with cloudflared for encrypted upstream queries
- Set `home.lan` as your local domain and create DNS records for all your services
- Review the Query Log occasionally — blocked queries show you what devices are doing
## Related Skills
- homelab-network-setup
- homelab-vlan-segmentation
- homelab-wireguard-vpn

View File

@ -0,0 +1,311 @@
---
name: homelab-vlan-segmentation
description: Segmenting home networks into VLANs for IoT, guest, trusted, and server traffic using UniFi, pfSense/OPNsense, and MikroTik — including switch trunk config, firewall rules, and wireless SSID mapping.
origin: community
---
# Homelab VLAN Segmentation
How to split a home network into isolated VLANs so IoT devices, guests, and your main
PCs cannot talk to each other. The most impactful security upgrade for a home network.
All firewall rules shown here add isolation between segments — they do not remove
existing protections. Apply changes in a maintenance window and verify connectivity
between segments after each step before moving on.
## When to Use
- Setting up VLANs on a home network for the first time
- Isolating IoT devices (smart bulbs, cameras, TVs) from trusted devices
- Creating a guest Wi-Fi network that cannot reach home devices
- Explaining how VLANs work to someone unfamiliar with the concept
- Configuring trunk ports, access ports, and SSID-to-VLAN mapping
- Troubleshooting inter-VLAN routing or firewall rule issues on pfSense/OPNsense/UniFi
## How It Works
```
Without VLANs — flat network:
All devices on 192.168.1.0/24
Smart TV (potential malware) → can reach your NAS, PCs, everything
With VLANs:
VLAN 10 — Trusted 192.168.10.0/24 (PCs, phones, laptops)
VLAN 20 — IoT 192.168.20.0/24 (smart TV, bulbs, cameras)
VLAN 30 — Servers 192.168.30.0/24 (NAS, Pi, VMs)
VLAN 40 — Guest 192.168.40.0/24 (visitor Wi-Fi)
VLAN 99 — Management 192.168.99.0/24 (switch/AP web UIs)
Smart TV → blocked from reaching 192.168.10.0/24 and 192.168.30.0/24
Guests → internet only, cannot see any home devices
```
## VLAN Design Template
```
VLAN Name Subnet Gateway Purpose
10 trusted 192.168.10.0/24 192.168.10.1 PCs, phones, laptops
20 iot 192.168.20.0/24 192.168.20.1 Smart home devices
30 servers 192.168.30.0/24 192.168.30.1 NAS, Pi, self-hosted
40 guest 192.168.40.0/24 192.168.40.1 Visitor Wi-Fi
99 management 192.168.99.0/24 192.168.99.1 Network gear web UIs
```
## Examples
**Typical homelab with UniFi AP and managed switch:**
```
Scenario: 3-bedroom house, UniFi Dream Machine + UniFi 8-port switch + 2 APs
VLAN 10 — Trusted 192.168.10.0/24 MacBook, iPhones, iPad
VLAN 20 — IoT 192.168.20.0/24 Nest thermostat, Philips Hue, Ring doorbell, smart TVs
VLAN 30 — Servers 192.168.30.0/24 Synology NAS (192.168.30.10), Pi-hole (192.168.30.2)
VLAN 40 — Guest 192.168.40.0/24 Visitor Wi-Fi — internet only
SSID → VLAN mapping:
"Home" → VLAN 10 (WPA2, strong password, trusted devices only)
"IoT" → VLAN 20 (WPA2, separate password, printed on router for setup)
"Guest" → VLAN 40 (WPA2, simple password you can share freely)
Switch port behavior:
Port 1 → trunk to router (tagged VLANs 10,20,30,40,99)
Port 2 → trunk to APs (tagged VLANs 10,20,40; AP handles per-SSID tagging)
Port 3 → access VLAN 30 (NAS — untagged, no VLAN awareness needed)
Port 4 → access VLAN 30 (Pi-hole — untagged)
Port 58 → access VLAN 10 (wired workstations)
Firewall rules applied (all rules add isolation, none remove existing protections):
IoT → Trusted: BLOCK
IoT → Servers: BLOCK except 192.168.30.2:53 (Pi-hole DNS allowed)
IoT → Internet: ALLOW
Guest → Local networks: BLOCK
Guest → Internet: ALLOW
Trusted → everywhere: ALLOW
```
## UniFi Configuration
### Create Networks in UniFi Controller
```
Settings → Networks → Create New Network
For each VLAN:
Name: IoT
Purpose: Corporate (gives DHCP + routing)
VLAN ID: 20
Network: 192.168.20.0/24
Gateway IP: 192.168.20.1
DHCP: Enable
DHCP Range: 192.168.20.100 192.168.20.254
```
### Map SSIDs to VLANs (UniFi)
```
Settings → WiFi → Create New WiFi
Name: IoT-Network
Password: <separate password>
Network: IoT ← select your VLAN here
# All devices connecting to this SSID land in VLAN 20
Name: Guest
Password: <guest password>
Network: Guest
Guest Policy: Enable ← isolates guests from each other too
```
### UniFi Firewall Rules (Traffic Rules)
```
Settings → Traffic & Security → Traffic Rules
# Block IoT from reaching Trusted VLAN
Action: Block
Category: Local Network
Source: IoT (192.168.20.0/24)
Destination: Trusted (192.168.10.0/24)
# Allow IoT to reach internet only
Action: Allow
Source: IoT
Destination: Internet
# Block Guest from all local networks
Action: Block
Source: Guest
Destination: Local Networks
```
## pfSense / OPNsense Configuration
### Create VLANs
```
Interfaces → Assignments → VLANs → Add
Parent Interface: em1 (your LAN NIC)
VLAN Tag: 20
Description: IoT
# Repeat for each VLAN, then assign each VLAN to an interface:
Interfaces → Assignments → Add
Select the VLAN you created → click Add
Enable the interface, set IP to gateway address (192.168.20.1/24)
```
### DHCP for Each VLAN
```
Services → DHCP Server → Select your VLAN interface
Enable DHCP
Range: 192.168.20.100 to 192.168.20.254
DNS Servers: 192.168.30.2 ← Pi-hole IP if you have one
```
### Firewall Rules (pfSense/OPNsense)
```
# Rules are processed top-to-bottom, first match wins.
# On the IoT interface (VLAN 20):
Rule 1: Allow IoT → Pi-hole DNS ← MUST come before the RFC1918 block rule
Protocol: UDP/TCP
Source: IoT net
Destination: 192.168.30.2 port 53
Action: Allow
Rule 2: Block IoT → RFC1918 (all private IP ranges)
Protocol: any
Source: IoT net
Destination: RFC1918 (192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12)
Action: Block
Rule 3: Allow IoT → internet
Protocol: any
Source: IoT net
Destination: any
Action: Allow
# On the Trusted interface (VLAN 10):
Allow all (trusted devices can reach everything)
Source: Trusted net
Destination: any
Action: Allow
# Additional exceptions for IoT devices that need specific local services:
Insert before Rule 2 (the RFC1918 block):
Protocol: TCP
Source: IoT net
Destination: 192.168.30.x port 8123 ← Home Assistant
Action: Allow
```
## MikroTik Configuration
```
# Step 1: Create a bridge with VLAN filtering enabled
/interface bridge
add name=bridge vlan-filtering=yes
# Step 2: Add physical ports to the bridge
# Trunk port to router/uplink (tagged for all VLANs)
/interface bridge port
add bridge=bridge interface=ether1 frame-types=admit-only-vlan-tagged
# Access port for trusted devices (untagged VLAN 10)
/interface bridge port
add bridge=bridge interface=ether2 pvid=10 frame-types=admit-only-untagged-and-priority-tagged
# Access port for IoT devices (untagged VLAN 20)
/interface bridge port
add bridge=bridge interface=ether3 pvid=20 frame-types=admit-only-untagged-and-priority-tagged
# Step 3: Define which VLANs are allowed on which ports
/interface bridge vlan
add bridge=bridge tagged=ether1 untagged=ether2 vlan-ids=10
add bridge=bridge tagged=ether1 untagged=ether3 vlan-ids=20
# Step 4: Create VLAN interfaces on the bridge (gateway IPs)
/interface vlan
add interface=bridge name=vlan10 vlan-id=10
add interface=bridge name=vlan20 vlan-id=20
# Step 5: Assign gateway IPs
/ip address
add interface=vlan10 address=192.168.10.1/24
add interface=vlan20 address=192.168.20.1/24
# Step 6: DHCP pools and servers
/ip pool
add name=pool-trusted ranges=192.168.10.100-192.168.10.254
add name=pool-iot ranges=192.168.20.100-192.168.20.254
/ip dhcp-server
add interface=vlan10 address-pool=pool-trusted name=dhcp-trusted
add interface=vlan20 address-pool=pool-iot name=dhcp-iot
/ip dhcp-server network
add address=192.168.10.0/24 gateway=192.168.10.1
add address=192.168.20.0/24 gateway=192.168.20.1
# Step 7: Firewall — block IoT from reaching trusted VLAN
/ip firewall filter
add chain=forward src-address=192.168.20.0/24 dst-address=192.168.10.0/24 \
action=drop comment="Block IoT to Trusted"
```
## Switch Trunk vs Access Ports
```
# Trunk port: carries multiple VLANs (tagged) — connects switch-to-switch, switch-to-router, switch-to-AP
# Access port: carries one VLAN (untagged) — connects to end devices (PC, camera, NAS)
# A managed switch port connected to your router should be a trunk:
Allowed VLANs: 10, 20, 30, 40, 99
# A port connecting to a PC should be an access port:
VLAN: 10 (trusted)
No tagging — the PC does not know or care about VLANs
# A port connecting to an AP must be a trunk:
The AP tags traffic from each SSID with the right VLAN ID
Allowed VLANs: 10, 20, 40 (whichever SSIDs the AP serves)
```
## Anti-Patterns
```
# BAD: Creating VLANs without adding firewall rules
# VLANs without firewall rules do not provide security — inter-VLAN routing is open by default
# GOOD: Add explicit block rules immediately after creating VLANs
# BAD: Putting the Pi-hole in the IoT VLAN
# IoT devices can reach it but trusted devices cannot (without extra rules)
# GOOD: Pi-hole in the Servers VLAN with a rule allowing all VLANs to reach port 53
# BAD: Native VLAN equals management VLAN
# Untagged traffic landing in your management VLAN enables VLAN hopping attacks
# GOOD: Use a dedicated unused VLAN as native (e.g. VLAN 999), keep management traffic tagged
# BAD: Same Wi-Fi password for IoT SSID and trusted SSID
# Anyone who learns the password can connect IoT devices to the wrong segment
```
## Best Practices
- Start with 4 VLANs: Trusted, IoT, Servers, Guest — add more as needed
- Put Pi-hole in the Servers VLAN (192.168.30.x)
- Add a firewall rule allowing DNS (port 53) from all VLANs to the Pi-hole IP — before any RFC1918 block rule
- Test isolation after every rule change: from the IoT VLAN, try to ping a trusted device — it should fail
- Use a management VLAN for switch and AP web UIs and restrict access to the Trusted VLAN only
- Document your VLAN design in a table (VLAN ID, name, subnet, purpose)
## Related Skills
- homelab-network-setup
- homelab-pihole-dns
- homelab-wireguard-vpn

View File

@ -0,0 +1,305 @@
---
name: homelab-wireguard-vpn
description: WireGuard VPN server setup, peer configuration, key generation, split tunneling vs full tunnel routing, and remote access to a home network from mobile and laptop clients.
origin: community
---
# Homelab WireGuard VPN
WireGuard is a fast, modern VPN protocol. It is the right choice for remote access to a
home network — simpler to configure than OpenVPN and faster than most alternatives.
All configuration examples show common setups. Review each command — especially the
iptables forwarding rules and key file permissions — before applying them to your
system, and make changes in a maintenance window.
## When to Use
- Setting up WireGuard server on a Raspberry Pi, Linux host, pfSense, or router
- Generating WireGuard keypairs and writing peer config files
- Configuring remote access from a phone or laptop to a home network
- Explaining split tunneling (route only home traffic) vs full tunnel (route all traffic)
- Troubleshooting WireGuard connections that will not come up
- Automating peer configuration generation for multiple clients
## How WireGuard Works
```
Your phone (WireGuard client)
│ Encrypted UDP tunnel (port 51820)
Your home router (WireGuard server — needs a public IP or DDNS)
Your home network (192.168.1.0/24, NAS, Pi, etc.)
Every device has a keypair (public + private key).
The server knows each client's public key.
The client knows the server's public key + endpoint (IP:port).
Traffic is encrypted end-to-end with no central server or certificate authority.
```
## Server Setup (Linux)
```bash
# Install WireGuard
sudo apt update && sudo apt install wireguard -y
# Generate server keypair — create files with private permissions from the start
sudo mkdir -p /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server_private.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key'
# Write server config — substitute the actual private key value
# Do not store private keys in version control or share them
sudo tee /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.8.0.1/24 # VPN subnet — server gets .1
ListenPort = 51820
PrivateKey = <paste_server_private_key_here>
# Scoped forwarding rules: allow VPN traffic in/out, not a blanket FORWARD ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT
PostDown = iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# Phone — replace with the actual phone public key
PublicKey = <phone_public_key>
AllowedIPs = 10.8.0.2/32
[Peer]
# Laptop — replace with the actual laptop public key
PublicKey = <laptop_public_key>
AllowedIPs = 10.8.0.3/32
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
# Replace eth0 with your actual outbound interface name
# Check with: ip route show default
# Enable IP forwarding (required for routing traffic through the server)
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
# Start WireGuard and enable on boot
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
```
## Client Configuration
```bash
# Generate a unique keypair for each client device
# Run on the client, or on the server and transfer the private key securely — never in plaintext
umask 077
wg genkey | tee phone_private.key | wg pubkey > phone_public.key
# Client config file (phone_wg0.conf):
[Interface]
PrivateKey = <phone_private_key>
Address = 10.8.0.2/32
DNS = 192.168.1.2 # Optional: use Pi-hole for DNS over the tunnel
[Peer]
PublicKey = <server_public_key>
Endpoint = your-home-ip.ddns.net:51820 # Your public IP or DDNS hostname
AllowedIPs = 192.168.1.0/24 # Split tunnel: only home network traffic
# AllowedIPs = 0.0.0.0/0, ::/0 # Full tunnel: all traffic through VPN
PersistentKeepalive = 25 # Keep NAT hole open (required for mobile clients)
```
## Split Tunnel vs Full Tunnel
```
# Split tunnel: AllowedIPs = 192.168.1.0/24
Only traffic destined for your home network goes through the VPN.
Internet traffic (YouTube, Spotify) goes directly — better performance on mobile.
Best for: "I just want to reach my NAS and Pi from anywhere."
# Full tunnel: AllowedIPs = 0.0.0.0/0, ::/0
ALL traffic goes through your home internet connection.
Useful for: piggybacking home DNS/Pi-hole ad blocking.
Downside: home upload speed becomes your bottleneck everywhere.
# Multi-subnet split tunnel (most common homelab use case):
AllowedIPs = 192.168.10.0/24, 192.168.20.0/24, 192.168.30.0/24, 10.8.0.0/24
Routes all your VLANs through the tunnel; internet stays direct.
```
## Key Generation and Peer Management
```python
import subprocess
def generate_keypair() -> tuple[str, str]:
"""Generate a WireGuard keypair. Returns (private_key, public_key)."""
private = subprocess.check_output(["wg", "genkey"]).decode().strip()
public = subprocess.run(
["wg", "pubkey"], input=private.encode(), capture_output=True
).stdout.decode().strip()
return private, public
def generate_preshared_key() -> str:
return subprocess.check_output(["wg", "genpsk"]).decode().strip()
def build_client_config(
client_private_key: str,
client_vpn_ip: str, # e.g. "10.8.0.3"
server_public_key: str,
server_endpoint: str, # e.g. "home.example.com:51820"
allowed_ips: str = "192.168.1.0/24",
dns: str = "",
) -> str:
dns_line = f"DNS = {dns}\n" if dns else ""
return f"""[Interface]
PrivateKey = {client_private_key}
Address = {client_vpn_ip}/32
{dns_line}
[Peer]
PublicKey = {server_public_key}
Endpoint = {server_endpoint}
AllowedIPs = {allowed_ips}
PersistentKeepalive = 25
"""
def build_server_peer_block(
client_public_key: str,
client_vpn_ip: str,
comment: str = "",
) -> str:
comment_line = f"# {comment}\n" if comment else ""
return f"""
{comment_line}[Peer]
PublicKey = {client_public_key}
AllowedIPs = {client_vpn_ip}/32
"""
```
Keep private keys out of source control. If you use this script, write key material
to files with mode 600 and never log or print it.
## pfSense / OPNsense WireGuard
```
# pfSense: VPN → WireGuard → Add Tunnel
Interface Keys: Generate (creates keypair automatically)
Listen Port: 51820
Interface Address: 10.8.0.1/24
# Add Peer (one per client):
Public Key: <client public key>
Allowed IPs: 10.8.0.2/32
# Assign the WireGuard interface:
Interfaces → Assignments → Add (select wg0)
Enable interface, no IP needed (it is set in the tunnel config)
# Firewall rules:
WAN → Allow UDP port 51820 inbound (so clients can reach the server)
WireGuard interface → Allow traffic to LAN networks you want reachable
```
## DDNS (Dynamic DNS) for Home Servers
Most home internet connections have a dynamic IP. Use DDNS so your VPN endpoint
stays reachable after an IP change.
```bash
# Option 1: Cloudflare DDNS — store credentials in a secrets file, not inline
# docker-compose entry using an env file:
ddns-updater:
image: qmcgaw/ddns-updater
env_file: ./ddns.env # store zone_id and token here, not in compose
restart: unless-stopped
# ddns.env (chmod 600, not committed to git):
# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id
# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
# Option 2: DuckDNS (free, simple)
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org)
Store token in /etc/ddns.env (mode 600), then use a small root-owned script:
# /usr/local/bin/update-duckdns
#!/bin/sh
set -eu
. /etc/ddns.env
curl --fail --silent --show-error --max-time 10 \
--get "https://www.duckdns.org/update" \
--data-urlencode "domains=myhome" \
--data-urlencode "token=${DUCKDNS_TOKEN}" \
--data-urlencode "ip="
# Cron job:
*/5 * * * * /usr/local/bin/update-duckdns >/dev/null 2>&1
```
## Troubleshooting
```bash
# Check WireGuard status and last handshake
sudo wg show
# If "latest handshake" is never or very old, the tunnel is not connected.
# Check:
# 1. Is UDP port 51820 open on the router/firewall?
sudo ufw status # or check pfSense/UniFi firewall rules
# 2. Is the server public key in the client config correct?
sudo wg show wg0 public-key # Compare to what is in the client config
# 3. Is IP forwarding enabled on the server?
cat /proc/sys/net/ipv4/ip_forward # Should be 1
# 4. Does the client AllowedIPs cover the IP you are trying to reach?
# If AllowedIPs = 192.168.1.0/24 and you are trying to reach 192.168.3.5, it will not route.
# Check kernel logs for WireGuard errors
dmesg | grep wireguard
# Restart WireGuard
sudo wg-quick down wg0 && sudo wg-quick up wg0
```
## Anti-Patterns
```
# BAD: Storing private keys in version control or sharing them
# Private keys are equivalent to passwords — never commit them to git
# BAD: Using AllowedIPs = 0.0.0.0/0 on mobile without considering the impact
# Full tunnel routes all mobile traffic through your home upload — usually slow
# BAD: Not setting PersistentKeepalive on mobile clients
# Mobile clients behind NAT drop idle tunnels without it
# BAD: Opening port 51820 in the firewall but forgetting IP forwarding on the server
# Tunnel connects but no traffic routes — confusing to debug
# BAD: Sharing a keypair across multiple client devices
# Each device must have its own unique keypair — shared keys break the security model
# BAD: Using a broad "FORWARD ACCEPT" iptables rule
# Scope forwarding rules to the wg0 interface and direction only
```
## Best Practices
- Generate a unique keypair per client device — never reuse keys
- Use split tunneling (`AllowedIPs = <home subnets>`) for mobile
- Set `PersistentKeepalive = 25` on all mobile clients
- Use DDNS if your ISP assigns a dynamic IP; store credentials in env files, not inline
- Use scoped iptables forwarding rules (inbound on wg0 only) rather than a blanket FORWARD ACCEPT
- Add Pi-hole's IP as `DNS =` in client configs to get ad blocking over the VPN
- Rotate the server keypair periodically and update all client configs
## Related Skills
- homelab-network-setup
- homelab-vlan-segmentation
- homelab-pihole-dns