From 71ed7c58d45954fe9bbca7f686aec68d85251aa2 Mon Sep 17 00:00:00 2001 From: Arsal Sajjad Date: Tue, 12 May 2026 18:20:53 -0700 Subject: [PATCH] feat: add homelab config skills (VLAN segmentation, Pi-hole DNS, WireGuard VPN) (#1838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- AGENTS.md | 4 +- README.md | 6 +- README.zh-CN.md | 2 +- docs/zh-CN/AGENTS.md | 4 +- docs/zh-CN/README.md | 6 +- skills/homelab-pihole-dns/SKILL.md | 274 +++++++++++++++++++ skills/homelab-vlan-segmentation/SKILL.md | 311 ++++++++++++++++++++++ skills/homelab-wireguard-vpn/SKILL.md | 305 +++++++++++++++++++++ 10 files changed, 903 insertions(+), 13 deletions(-) create mode 100644 skills/homelab-pihole-dns/SKILL.md create mode 100644 skills/homelab-vlan-segmentation/SKILL.md create mode 100644 skills/homelab-wireguard-vpn/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 89c87c7c..71047464 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index aabcb2fa..e98e81ab 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" diff --git a/AGENTS.md b/AGENTS.md index 5d5229ca..e34717bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) diff --git a/README.md b/README.md index 6bfa448d..ec2461fc 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index c1775b4e..19dfb2cf 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 60 个代理、225 个技能和 75 个命令。 +**完成!** 你现在可以使用 60 个代理、228 个技能和 75 个命令。 ### multi-* 命令需要额外配置 diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 738abc7a..7ab620ba 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -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/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 15dfbf20..4794bef1 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -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 条指令 | diff --git a/skills/homelab-pihole-dns/SKILL.md b/skills/homelab-pihole-dns/SKILL.md new file mode 100644 index 00000000..0048749c --- /dev/null +++ b/skills/homelab-pihole-dns/SKILL.md @@ -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: + 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 `` 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:///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="" +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 diff --git a/skills/homelab-vlan-segmentation/SKILL.md b/skills/homelab-vlan-segmentation/SKILL.md new file mode 100644 index 00000000..8741801d --- /dev/null +++ b/skills/homelab-vlan-segmentation/SKILL.md @@ -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 5–8 → 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: + Network: IoT ← select your VLAN here + # All devices connecting to this SSID land in VLAN 20 + + Name: 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 diff --git a/skills/homelab-wireguard-vpn/SKILL.md b/skills/homelab-wireguard-vpn/SKILL.md new file mode 100644 index 00000000..65b58fb7 --- /dev/null +++ b/skills/homelab-wireguard-vpn/SKILL.md @@ -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 = + +# 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 = +AllowedIPs = 10.8.0.2/32 + +[Peer] +# Laptop — replace with the actual laptop public key +PublicKey = +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 = +Address = 10.8.0.2/32 +DNS = 192.168.1.2 # Optional: use Pi-hole for DNS over the tunnel + +[Peer] +PublicKey = +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: + 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 = `) 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