mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +08:00
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:
parent
7f3dfde6d7
commit
71ed7c58d4
@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"source": "./",
|
"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",
|
"version": "2.0.0-rc.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc",
|
"name": "ecc",
|
||||||
"version": "2.0.0-rc.1",
|
"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": {
|
"author": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
"url": "https://x.com/affaanmustafa"
|
"url": "https://x.com/affaanmustafa"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — Agent Instructions
|
# 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
|
**Version:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
|||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 60 specialized subagents
|
agents/ — 60 specialized subagents
|
||||||
skills/ — 225 workflow skills and domain knowledge
|
skills/ — 228 workflow skills and domain knowledge
|
||||||
commands/ — 75 slash commands
|
commands/ — 75 slash commands
|
||||||
hooks/ — Trigger-based automations
|
hooks/ — Trigger-based automations
|
||||||
rules/ — Always-follow guidelines (common + per-language)
|
rules/ — Always-follow guidelines (common + per-language)
|
||||||
|
|||||||
@ -358,7 +358,7 @@ If you stacked methods, clean up in this order:
|
|||||||
/plugin list ecc@ecc
|
/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
|
### 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** |
|
| Agents | PASS: 60 agents | PASS: 12 agents | **Claude Code leads** |
|
||||||
| Commands | PASS: 75 commands | PASS: 35 commands | **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!** |
|
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
| 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 |
|
| **Agents** | 60 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||||
| **Commands** | 75 | Shared | Instruction-based | 35 |
|
| **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 Events** | 8 types | 15 types | None yet | 11 types |
|
||||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||||
|
|||||||
@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 你现在可以使用 60 个代理、225 个技能和 75 个命令。
|
**完成!** 你现在可以使用 60 个代理、228 个技能和 75 个命令。
|
||||||
|
|
||||||
### multi-* 命令需要额外配置
|
### multi-* 命令需要额外配置
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Everything Claude Code (ECC) — 智能体指令
|
# Everything Claude Code (ECC) — 智能体指令
|
||||||
|
|
||||||
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、225 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
|
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、228 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
|
||||||
|
|
||||||
**版本:** 2.0.0-rc.1
|
**版本:** 2.0.0-rc.1
|
||||||
|
|
||||||
@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
agents/ — 60 个专业子代理
|
agents/ — 60 个专业子代理
|
||||||
skills/ — 225 个工作流技能和领域知识
|
skills/ — 228 个工作流技能和领域知识
|
||||||
commands/ — 75 个斜杠命令
|
commands/ — 75 个斜杠命令
|
||||||
hooks/ — 基于触发的自动化
|
hooks/ — 基于触发的自动化
|
||||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||||
|
|||||||
@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
|||||||
/plugin list ecc@ecc
|
/plugin list ecc@ecc
|
||||||
```
|
```
|
||||||
|
|
||||||
**搞定!** 你现在可以使用 60 个智能体、225 项技能和 75 个命令了。
|
**搞定!** 你现在可以使用 60 个智能体、228 项技能和 75 个命令了。
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@ -1138,7 +1138,7 @@ opencode
|
|||||||
|---------|-------------|----------|--------|
|
|---------|-------------|----------|--------|
|
||||||
| 智能体 | PASS: 60 个 | PASS: 12 个 | **Claude Code 领先** |
|
| 智能体 | PASS: 60 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||||
| 命令 | PASS: 75 个 | PASS: 35 个 | **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: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||||
@ -1246,7 +1246,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
|||||||
|---------|------------|------------|-----------|----------|
|
|---------|------------|------------|-----------|----------|
|
||||||
| **智能体** | 60 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
| **智能体** | 60 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||||
| **命令** | 75 | 共享 | 基于指令 | 35 |
|
| **命令** | 75 | 共享 | 基于指令 | 35 |
|
||||||
| **技能** | 225 | 共享 | 10 (原生格式) | 37 |
|
| **技能** | 228 | 共享 | 10 (原生格式) | 37 |
|
||||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||||
|
|||||||
274
skills/homelab-pihole-dns/SKILL.md
Normal file
274
skills/homelab-pihole-dns/SKILL.md
Normal 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
|
||||||
311
skills/homelab-vlan-segmentation/SKILL.md
Normal file
311
skills/homelab-vlan-segmentation/SKILL.md
Normal 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 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: <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
|
||||||
305
skills/homelab-wireguard-vpn/SKILL.md
Normal file
305
skills/homelab-wireguard-vpn/SKILL.md
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user