discover · compare · collections · requirements · contribute · feed · watching · stats · help with your tokens
npapatheodorou/cudy-openwrt-tutorial · source
Snapshot 0 · 2026-11-04T12:47:28Z · TypeScript · MIT
Content fingerprint
bc332682169ed4ed9ea0eb879d17a0be2519e57e4908a17b536777b6352b7476GitHub description at capture
Tutorial repo on upgrading Cudy WR3000 routers with OpenWrt. Covers flashing the firmware, adding Adblock for cleaner browsing, and configuring WireGuard VPN for secure connections. Step-by-step instructions to unlock your router’s full potential.
README at capture
<p align="center">
<img src="./assets/icons/icon.svg" width="112" alt="Cudy + OpenWrt Tutorial icon"/>
</p>
# 🚀 Cudy + OpenWrt Tutorial
<p>
<a href="https://cudy-openwrt-tutorial.vercel.app/"><img alt="Website" src="https://img.shields.io/badge/website-open%20the%20guide-0d9488?style=flat-square"></a>
<a href="./firmware/"><img alt="Firmware packages" src="https://img.shields.io/badge/firmware-18%20packages-2563eb?style=flat-square"></a>
<a href="./LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-64748b?style=flat-square"></a>
</p>
This guide walks you through upgrading your **Cudy router** to
OpenWrt, enabling LuCI Web UI, blocking ads, setting up WireGuard VPN
(general steps and provider-specific instructions), and managing IPv6.
> 🌐 **Prefer a website?** The same guide is available at **[cudy-openwrt-tutorial.vercel.app](https://cudy-openwrt-tutorial.vercel.app/)**, with copy buttons, progress tracking, screenshot zoom and fill-in helpers for your Wi-Fi and WireGuard values.
# 🛒 Buying Suggestion
Buying Suggestion: Prefer the Cudy WR3000E over other WR3000 variants if you plan to install OpenWrt — it offers significantly more memory, supports Wi-Fi 6, and overall provides excellent value for money (VFM)
------------------------------------------------------------------------
## 📥 Step 1 --- Download Required Files
1. **OpenWrt Firmware (without recovery TFTP):**
Select the appropriate firmware according to your router’s model. [Download here](./firmware/)
<details>
<summary>📦 Firmware packages (18)</summary>
| Model | Download | Size | Includes |
| ----- | -------- | ---- | -------- |
| **WR3000E V1** ⭐ Recommended | [WR3000E-V1.zip](./firmware/WR3000E-V1.zip) | 10.1 MB | Sysupgrade image |
| WR3000 V1 | [WR3000-V1.zip](./firmware/WR3000-V1.zip) | 9.5 MB | Sysupgrade image · no TFTP recovery |
| WR3000H V1 | [WR3000H-V1.zip](./firmware/WR3000H-V1.zip) | 10.1 MB | Sysupgrade image |
| WR3000S V1 | [WR3000S-V1.zip](./firmware/WR3000S-V1.zip) | 10.1 MB | Sysupgrade image |
| WR2100 V1 | [WR2100-V1.zip](./firmware/WR2100-V1.zip) | 17.4 MB | Flash image · TFTP recovery kit · developer files |
| WR1300 V1 | [WR1300-V1.zip](./firmware/WR1300-V1.zip) | 21.6 MB | Flash image (OpenWrt 22.03.2) · TFTP recovery kit · developer files |
| WR1300 V2 / V3 | [WR1300-V2-V3.zip](./firmware/WR1300-V2-V3.zip) | 10.2 MB | Flash image (OpenWrt 22.03.0) |
| WR1300 V3.0 (update 2025-06-09) | [WR1300-V3.0-update-20250609.zip](./firmware/WR1300-V3.0-update-20250609.zip) | 7.0 MB | Flash image (OpenWrt 23.05.5) |
| X6 V1 | [X6-V1.zip](./firmware/X6-V1.zip) | 19.3 MB | Flash image · TFTP recovery kit · developer files |
| X6 V2 | [X6-V2.zip](./firmware/X6-V2.zip) | 21.4 MB | Flash image (OpenWrt 22.03) · TFTP recovery kit · developer files |
| M1200 V1 | [M1200-V1.zip](./firmware/M1200-V1.zip) | 6.4 MB | Flash image · LAN/WAN ports may swap after flashing, so try both |
| M1300 V2 | [M1300-V2.zip](./firmware/M1300-V2.zip) | 7.0 MB | Flash image |
| M1800 V2 | [M1800-V2.zip](./firmware/M1800-V2.zip) | 21.6 MB | Flash image (OpenWrt 22.03.2) · TFTP recovery kit · developer files |
| TR1200 V1 | [TR1200-V1.zip](./firmware/TR1200-V1.zip) | 6.0 MB | Flash image |
| TR3000 V1 | [TR3000-V1.zip](./firmware/TR3000-V1.zip) | 10.1 MB | Sysupgrade image |
| AP3000 Outdoor V1 | [AP3000-Outdoor-V1.zip](./firmware/AP3000-Outdoor-V1.zip) | 10.1 MB | Sysupgrade image |
| RE3000 V1 | [RE3000-V1.zip](./firmware/RE3000-V1.zip) | 23.5 MB | Sysupgrade image · TFTP recovery kit |
| R700 V1 | [R700-V1.zip](./firmware/R700-V1.zip) | 6.0 MB | Flash image |
Verify your download against [SHA256SUMS.txt](./firmware/SHA256SUMS.txt).
</details>
2. **OpenWrt Firmware Finder (SNAPSHOT builds):**
Select the firmware that matches your router’s model; ensure the version chosen is `SNAPSHOT`.
<details>
<summary><a href="https://firmware-selector.openwrt.org/">Download Here</a></summary>
<p align="center">
<img src="./assets/images/s1_2.png" width="800"/>
</p>
</details>
3. **PuTTY (SSH client):**
[Download PuTTY](https://www.putty.org/)
------------------------------------------------------------------------
## ⚡ Step 2 --- Flash OpenWrt
1. Access router at `192.168.10.1` (Cudy's portal).
2. <details>
<summary>Upgrade the firmware using the OpenWrt image downloaded in Step 1.1.</summary>
<p align="center">
<img src="./assets/images/s2_1.png" width="800"/>
</p>
</details>
3. After upgrade → visit
`192.168.1.1 → System → Backup / Flash Firmware`.
4. <details>
<summary>Upload new file and start upgrade using the image download in Step 1.2.</summary>
<p align="center">
<img src="./assets/images/s2_4.png" width="800"/>
</p>
</details>
5. Since the snapshot has no UI:
- Open **PuTTY** and connect to `192.168.1.1` via SSH.
- Login as `root` (no password first time).
------------------------------------------------------------------------
> If an error occurs at any stage of the process, you can simply SSH into the system and run `firstboot && reboot`
## 🖥️ Step 3 --- Install LuCI Web Interface
<!-- snippet: luci-install -->
```sh
apk update
apk add luci
apk add luci-ssl
/etc/init.d/uhttpd restart
uci set uhttpd.main.redirect_https=1
uci commit uhttpd
/etc/init.d/uhttpd restart
```
------------------------------------------------------------------------
<details>
<summary><span style="font-size:1.5em;font-weight:600">🐣 Beginners (Setup using Interface)</span></summary>
👉 After restart, log in at `192.168.1.1` via LuCI.
- Set a new password for root.
- Go to **Network → Interfaces → LAN**
- <details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s3_2.png" width="800"/>
</p>
</details>
- <details>
<summary>Change IPv4 address → `192.168.2.1/24`</summary>
<p align="center">
<img src="./assets/images/bg3_2.png" width="800"/>
</p>
</details>
- **Save & Apply**
### 🛡️ Step 4 --- Adblock (network-wide ad/tracker blocking)
1. Go to **System → Software**
- Update Lists
- Install: `adblock`, `luci-app-adblock`
- <details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s4_1.png" width="800"/>
</p>
</details>
2. Configure via **Services → Adblock**:
- **General Settings**
- ✔ TLD Compression
- ✔ DNS Report
- <details>
<summary>Images Here</summary>
<p align="center">
<img src="./assets/images/s4_2_1.png" width="800"/>
</p>
</details>
- **Firewall Settings**
- Forced Zones → `@lan`, `@wan`, `@wan6`
- Forced Ports → `853`, `53`, `5353`
- <details>
<summary>Images Here</summary>
<p align="center">
<img src="./assets/images/s4_2_2.png" width="800"/>
</p>
</details>
- **Advanced DNS Settings**
- DNS Backend → `dnsmasq`
- <details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s4_2_3.png" width="800"/>
</p>
</details>
- **Feed Selection**
- Blocklist Feed → `adguard (L, general)`, `adguard_tracking (L, tracking)`, `android_tracking (S, tracking)`, `anti_ad (L, compilation)`, `certpl (L, phishing)`
- <details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s4_2_4.png" width="800"/>
</p>
</details>
3. Restart **dnsmasq** from **System → Startup**.
<details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s4_3_1.png" width="800"/>
</p>
</details>
------------------------------------------------------------------------
### 🔐 Step 5 --- WireGuard VPN client (route LAN through a VPN provider)
1. Go to **System → Software**
- Update Lists
- Install: `luci-proto-wireguard`
- Reboot
2. Go to **Network → Interfaces**
- Add new interface
- Name → `wg0`
- Protocol → **WireGuard VPN**
- Create interface
3. Firewall Setup:
- Go to **Network → Firewall**
- Add Zone:
- Name → `vpn`
- ✔ IPv4 Masquerading
- ✔ MSS Clamping
- Covered Networks → `wg0`
- Allow forward from source zones → `lan`
- **Lan Forwarding**:
- Edit `lan` → Allow forward to destination zones → `wg0`
4. Go again to **Network → Interfaces**
- Edit interface with Name → `wg0`
- Go to `Firewall Settings`
- Create / Assign firewall-zone → `vpn` (see step 2)
- **Save & Apply**
5. Kill Switch Rule (Optional):
- Go to **Traffic Rules**
- Add Rule:
- Name → `vpn-killswitch`
- Source Zone → `lan`
- Destination Zone → `wan`
- Action → **drop**
------------------------------------------------------------------------
### 🌍 Step 5.2 --- VPN Provider Configuration
<details>
<summary><strong>🛰️ NordVPN</strong></summary>
1. **Generate Credentials**
- Visit [Nord Account Access Tokens](https://my.nordaccount.com/dashboard/nordvpn/access-tokens/)
→ `Generate a token`.
- **Copy it**.
- <details>
<summary>Image Here</summary>
<p align="center">
<img src="./assets/images/s5_2_1.png" width="800"/>
</p>
</details>
2. **Find Your Recommended Server and Download Configuration**
- Visit [nordconf-wg](https://nordconf-wg.netlify.app/#private_key). The tool will automatically recommend the nearest server based on your current location.
- Paste your access token into the **NordVPN access token** field
- Generate and download the ``.conf`` configuration file.
3. **Configure WireGuard**
- Go to **Network → Interfaces → wg0**
- Import your configuration file → **Load configuration...**
- In Peers → Edit → ✔ Route Allowed IPs
- **Save**
> If you'd rather get your NordLynx private key yourself, try the [NordVPN Private Key Extractor](#-extra-tools).
</details>
------------------------------------------------------------------------
</details>
<details>
<summary><span style="font-size:1.5em;font-weight:600">⚡ Advanced (Setup using SSH Connection)</span></summary>
👉 After restart, open **PuTTY** and connect to `192.168.1.1` via SSH
<!-- snippet: advanced-setup -->
```sh
# Set a new root password
passwd root
# Change IPv4 address to 192.168.2.1/24
uci set network.lan.ipaddr="192.168.2.1"
uci set network.lan.netmask='255.255.255.0'
# Set WiFi
uci set wireless.radio0.cell_density='0'
uci set wireless.default_radio0.ssid='<WIFI_NAME>'
uci set wireless.default_radio0.encryption='sae-mixed'
uci set wireless.default_radio0.key='<WIFI_PASS>'
uci set wireless.default_radio0.ocv='0'
uci delete wireless.default_radio0.disabled
uci set wireless.radio1.cell_density='0'
uci set wireless.default_radio1.ssid='<WIFI_NAME>'
uci set wireless.default_radio1.encryption='sae-mixed'
uci set wireless.default_radio1.key='<WIFI_PASS>'
uci set wireless.default_radio1.ocv='0'
uci delete wireless.default_radio1.disabled
uci commit wireless
uci commit network
wifi reload
/etc/init.d/network restart
```
<details>
<summary><span style="font-size:1.2em;font-weight:600">🛡️ Step 4 --- Adblock (network-wide ad/tracker blocking)</span></summary>
<!-- snippet: advanced-adblock -->
```sh
# 1. Install packages
apk add adblock luci-app-adblock banip luci-app-banip
# 2 & 3. Configure adblock
uci set adblock.global.adb_enabled='1'
uci set adblock.global.adb_report='1'
uci set adblock.global.adb_nftforce='1'
uci set adblock.global.adb_dns='dnsmasq'
uci set adblock.global.adb_fetchcmd='uclient-fetch'
uci set adblock.global.adb_nftdevforce='@lan' '@wan' '@wan6'
uci set adblock.global.adb_nftportforce='853' '53' '5353'
uci set adblock.global.adb_feed='adguard' 'adguard_tracking' 'android_tracking' 'anti_ad' 'certpl'
uci set adblock.global.adb_forcedns='1'
uci set adblock.global.adb_zonelist='wan lan'
uci set adblock.global.adb_portlist='53 853 5353'
uci set adblock.global.adb_tld='1'
uci set adblock.global.adb_fetchutil='uclient-fetch'
uci commit adblock
# 4. Make dnsmasq ignore /etc/resolv.conf
uci delete dhcp.@dnsmasq[0].server
uci set dhcp.@dnsmasq[0].noresolv='1'
uci add_list dhcp.@dnsmasq[0].server='9.9.9.9'
uci add_list dhcp.@dnsmasq[0].server='149.112.112.112'
uci commit dhcp
/etc/init.d/dnsmasq restart
/etc/init.d/adblock enable
/etc/init.d/adblock start
# 5. Block DoH bypass
uci set banip.global.ban_enabled='1'
uci set banip.global.ban_autodetect='1'
uci set banip.global.ban_protov4='1'
uci set banip.global.ban_protov6='1'
uci set banip.global.ban_logprerouting='0'
uci set banip.global.ban_logforwardwan='0'
uci set banip.global.ban_logforwardlan='0'
uci -q delete banip.global.ban_feed
uci set banip.doh=feed
uci set banip.doh.ban_fetchchain='outbound'
uci commit banip
service banip restart
```
</details>
------------------------------------------------------------------------
<details>
<summary><span style="font-size:1.2em;font-weight:600">🔐 Step 5 --- WireGuard VPN client (route LAN through a VPN provider)</span></summary>
1. Install the packages
<!-- snippet: wg-install -->
```sh
apk update
apk add wireguard-tools kmod-wireguard luci-proto-wireguard luci-app-wireguard
reboot
```
2. Assumption: you already have a WireGuard `.conf` file in hand. Standard format:
<!-- snippet: wg-conf-example -->
```ini
[Interface]
PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
Address = 10.64.0.2/32
DNS = 10.64.0.1
[Peer]
PublicKey = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=
Endpoint = se-mma-wg-001.mullvad.net:51820
AllowedIPs = 0.0.0.0/0, ::/0
```
You'll copy five values out of that file:
| Placeholder in the snippet | Source in your `.conf` |
| -------------------------- | ----------------------------- |
| `<PRIVATE_KEY>` | `[Interface] PrivateKey` |
| `<ADDRESS>` | `[Interface] Address` |
| `<DNS>` | `[Interface] DNS` |
| `<PEER_PUBLIC_KEY>` | `[Peer] PublicKey` |
| `<ENDPOINT_HOST>` | host part of `[Peer] Endpoint` (everything before `:51820`)|
> 💡 On the [website](https://cudy-openwrt-tutorial.vercel.app/#advanced-step-5) you can paste your `.conf` and these placeholders fill in automatically (parsed locally in your browser).
<!-- snippet: wg-interface -->
```sh
# Interface
uci set network.wg0=interface
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key='<PRIVATE_KEY>'
uci -q delete network.wg0.addresses
uci add_list network.wg0.addresses='<ADDRESS>'
uci -q delete network.wg0.dns
uci add_list network.wg0.dns='<DNS>'
uci set network.peer0=wireguard_wg0
uci set network.peer0.description='VPN provider'
uci set network.peer0.public_key='<PEER_PUBLIC_KEY>'
uci set network.peer0.endpoint_host='<ENDPOINT_HOST>'
uci set network.peer0.endpoint_port='51820'
uci set network.peer0.route_allowed_ips='1'
uci set network.peer0.persistent_keepalive='25'
uci -q delete network.peer0.allowed_ips
uci add_list network.peer0.allowed_ips='0.0.0.0/0'
uci add_list network.peer0.allowed_ips='::/0'
uci commit network
/etc/init.d/network reload
```
3. Firewall (kill-switch style)
One `vpn` zone for `wg0`, with masquerading. We also remove the default `lan → wan` forwarding so traffic *only* leaves through the VPN — if the tunnel drops, nothing leaks to your ISP.
<!-- snippet: wg-firewall -->
```sh
# Named VPN zone covering wg0
uci set firewall.vpn=zone
uci set firewall.vpn.name='vpn'
uci set firewall.vpn.input='REJECT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.forward='REJECT'
uci set firewall.vpn.masq='1'
uci set firewall.vpn.mtu_fix='1'
uci -q delete firewall.vpn.network
uci add_list firewall.vpn.network='wg0'
# Named forwarding lan -> vpn
uci set firewall.lan_to_vpn=forwarding
uci set firewall.lan_to_vpn.src='lan'
uci set firewall.lan_to_vpn.dest='vpn'
# KILL SWITCH: remove the default lan -> wan forwarding
for fwd in $(uci -q show firewall | sed -n "s/^firewall\.\([^.]*\)=forwarding$/\1/p"); do
src=$(uci -q get firewall."$fwd".src)
dest=$(uci -q get firewall."$fwd".dest)
if [ "$src" = "lan" ] && [ "$dest" = "wan" ]; then
uci delete firewall."$fwd"
break
fi
done
uci commit firewall
service firewall restart
```
> **Don't want a kill switch?** Skip the loop that deletes `lan → wan`. With it left in place, if the VPN drops, traffic falls back to your regular ISP (privacy-leaking but no outages).
4. **Fail-safe: a `killswitch` toggle script**
The kill switch is great until your VPN config expires (looking at you, NordVPN), your provider has an outage, or you simply mistype an `Endpoint` and the LAN has no internet. You can always SSH into the router from LAN even when the kill switch is on — the router's own input policy isn't affected — so you'll never be fully locked out. But you want a one-line escape hatch.
This script lets you flip the kill switch on and off without remembering UCI syntax in a panic:
<!-- snippet: killswitch-script -->
```sh
cat > /usr/bin/killswitch <<'EOF'
#!/bin/sh
# Toggle the VPN kill switch (lan -> wan forwarding) on or off.
# Usage: killswitch {on|off|status}
set -e
find_fwd_lan_wan() {
# Print the section id of the firewall.forwarding entry where src=lan, dest=wan
for f in $(uci -q show firewall | sed -n "s/^firewall\.\([^.]*\)=forwarding$/\1/p"); do
[ "$(uci -q get firewall."$f".src)" = "lan" ] || continue
[ "$(uci -q get firewall."$f".dest)" = "wan" ] || continue
echo "$f"; return 0
done
return 1
}
case "$1" in
off)
# Restore lan -> wan so LAN reaches the internet directly again
if find_fwd_lan_wan >/dev/null; then
echo "Kill switch already OFF (lan -> wan forwarding exists)"
exit 0
fi
uci set firewall.lan_to_wan=forwarding
uci set firewall.lan_to_wan.src='lan'
uci set firewall.lan_to_wan.dest='wan'
uci commit firewall
service firewall reload
echo "Kill switch OFF — LAN now has direct internet (no VPN)."
echo "Optional: 'ifdown wg0' to stop the tunnel entirely."
;;
on)
# Remove lan -> wan so LAN traffic only leaves via VPN
f=$(find_fwd_lan_wan) || { echo "Kill switch already ON (no lan -> wan forwarding)"; exit 0; }
uci delete firewall."$f"
uci commit firewall
service firewall reload
echo "Kill switch ON — LAN traffic only exits through wg0."
echo "Make sure 'ifup wg0' is up or LAN will have no internet."
;;
status)
if find_fwd_lan_wan >/dev/null; then
echo "Kill switch: OFF (lan -> wan forwarding present)"
else
echo "Kill switch: ON (no lan -> wan forwarding)"
fi
echo
echo "wg0:"
wg show wg0 2>/dev/null || echo " (interface down or not configured)"
;;
*)
echo "Usage: killswitch {on|off|status}"
exit 1
;;
esac
EOF
chmod +x /usr/bin/killswitch
```
Use it like this:
<!-- snippet: killswitch-usage -->
```sh
killswitch status # see what state you're in
killswitch off # disable kill switch — LAN gets direct internet again
killswitch on # re-enable kill switch — LAN routes through wg0 only
```
> Even with the kill switch ON and `wg0` down, the router itself can still reach the internet (the kill switch only governs *forwarded* traffic from LAN). That means you can `ssh root@192.168.1.1` from any LAN device, run `killswitch off`, fix your config, and re-enable. You're never truly locked out.
**A more aggressive fail-safe (optional):** drop a copy of `killswitch off` as `/root/PANIC.sh` so you can run `sh /root/PANIC.sh` blindly:
<!-- snippet: panic-script -->
```sh
cat > /root/PANIC.sh <<'EOF'
#!/bin/sh
# Emergency: disable kill switch and bring wg0 down.
/usr/bin/killswitch off
ifdown wg0 2>/dev/null
echo "Done. You should have normal internet now."
EOF
chmod +x /root/PANIC.sh
```
4. Verify
Bring up the tunnel and check:
<!-- snippet: wg-verify-handshake -->
```sh
ifup wg0
wg show
# Look for `latest handshake: X seconds ago` — should appear within ~30s
```
Confirm your external IP changed (from a LAN client, or from the router itself):
<!-- snippet: wg-verify-ip -->
```sh
curl ifconfig.co
curl ifconfig.co/country
```
Check for DNS leaks: <https://dnsleaktest.com> from a LAN client.
> **NordVPN gotcha:** if the handshake never completes, the config may have expired (Nord rotates keys on manually-extracted configs) or the server may have stopped accepting manual WireGuard. Regenerate the config and try a different server hostname.
</details>
------------------------------------------------------------------------
<details>
<summary><span style="font-size:1.2em;font-weight:600">📄🔓 Step 5.1 --- VPN Whitelisting (Enable Policy-Based Routing)</span></summary>
1. **Install**
<!-- snippet: pbr-install -->
```sh
opkg update
opkg install pbr luci-app-pbr
service rpcd restart
```
2. **Enable PBR globally**
<!-- snippet: pbr-enable -->
```sh
uci set pbr.config.enabled='1'
uci set pbr.config.strict_enforcement='1' # drop traffic if a policy's interface is down (no silent fallback)
uci set pbr.config.resolver_set='nft' # use nftables (correct for fw4 / OpenWrt 22.03+)
uci set pbr.config.verbosity='1' # quieter logs once you trust the setup
uci commit pbr
service pbr enable
service pbr restart
```
> **About `strict_enforcement='1'`** — if a policy says "route through `wg0`" and `wg0` is down, the traffic is **dropped** instead of falling back to `wan`. This prevents accidental leaks when the VPN flaps. Set to `'0'` if you'd rather have fallback.
3. **Add a bypass policy (one per device or group)**
3.1 — Bypass VPN by MAC address
For a smart TV (`AA:BB:CC:DD:EE:FF`) that should use the regular ISP connection:
<!-- snippet: pbr-bypass-mac -->
```sh
uci set pbr.smart_tv=policy
uci set pbr.smart_tv.name='Smart TV'
uci set pbr.smart_tv.src_mac='AA:BB:CC:DD:EE:FF'
uci set pbr.smart_tv.interface='wan' # bypass VPN, exit via wan
uci set pbr.smart_tv.enabled='1'
uci commit pbr
service pbr reload
```
3.2 — Force a device ONLY through VPN
For a work laptop (`11:22:33:44:55:66`) that must always use VPN, even if other devices don't:
<!-- snippet: pbr-force-vpn -->
```sh
uci set pbr.work_laptop=policy
uci set pbr.work_laptop.name='Work Laptop'
uci set pbr.work_laptop.src_mac='11:22:33:44:55:66'
uci set pbr.work_laptop.interface='wg0' # force through VPN
uci set pbr.work_laptop.enabled='1'
uci commit pbr
service pbr reload
```
3.3 — Bypass VPN for specific destination domains/IPs
For banking sites that block known VPN exits — route them via `wan` for *all* LAN clients:
<!-- snippet: pbr-bypass-domains -->
```sh
uci set pbr.banking=policy
uci set pbr.banking.name='Banking sites'
uci set pbr.banking.dest_addr='chase.com paypal.com revolut.com'
uci set pbr.banking.interface='wan'
uci set pbr.banking.enabled='1'
uci commit pbr
service pbr reload
```
3.4 — Bypass by source IP range
For a VLAN, subnet, or specific static-IP device:
<!-- snippet: pbr-bypass-ip-range -->
```sh
uci set pbr.iot_subnet=policy
uci set pbr.iot_subnet.name='IoT subnet'
uci set pbr.iot_subnet.src_addr='192.168.1.100-192.168.1.150'
uci set pbr.iot_subnet.interface='wan'
uci set pbr.iot_subnet.enabled='1'
uci commit pbr
service pbr reload
```
> `src_addr` accepts single IPs, ranges (`a-b`), or CIDR (`192.168.1.0/28`).
3.5 — Bypass a single port (e.g. Steam, Plex)
<!-- snippet: pbr-bypass-port -->
```sh
uci set pbr.steam=policy
uci set pbr.steam.name='Steam game traffic'
uci set pbr.steam.proto='tcp udp'
uci set pbr.steam.dest_port='27015 27036'
uci set pbr.steam.interface='wan'
uci set pbr.steam.enabled='1'
uci commit pbr
service pbr reload
```
3.6 — Combine criteria
Fields can be combined within a single policy. For example, *"this MAC, only for these ports, via VPN"*:
<!-- snippet: pbr-combine -->
```sh
uci set pbr.laptop_torrents=policy
uci set pbr.laptop_torrents.name='Laptop torrents'
uci set pbr.laptop_torrents.src_mac='11:22:33:44:55:66'
uci set pbr.laptop_torrents.proto='tcp'
uci set pbr.laptop_torrents.dest_port='6881-6889'
uci set pbr.laptop_torrents.interface='wg0'
uci set pbr.laptop_torrents.enabled='1'
uci commit pbr
service pbr reload
```
4. **List, edit, disable, delete policies**
<!-- snippet: pbr-manage -->
```sh
# List all policies
uci show pbr | grep =policy
# Disable a policy temporarily (without deleting)
uci set pbr.smart_tv.enabled='0'
uci commit pbr
service pbr reload
# Re-enable
uci set pbr.smart_tv.enabled='1'
uci commit pbr
service pbr reload
# Delete a policy
uci delete pbr.smart_tv
uci commit pbr
service pbr reload
# Check what PBR is actually doing
service pbr status
# Live routing table per interface
ip rule show
ip route show table wan
ip route show table wg0
```
5. **Kill switch with MAC-based exemptions**
If you want a kill switch that **doesn't lock out the bypassed devices**, the cleanest approach is a firewall traffic rule using the `!` (NOT) operator on the source MAC. PBR routes the smart TV via `wan`; the kill switch then has to permit that.
> **Note:** UCI handles `!` natively — only the LuCI web UI strips that character, which is why most online guides tell you to edit `/etc/config/firewall` by hand. From the CLI we don't need that workaround.
5.1 — Create the rule
<!-- snippet: pbr-killswitch-rule -->
```sh
# Named traffic rule: REJECT lan -> wan, UNLESS source MAC is exempted
uci set firewall.vpn_killswitch=rule
uci set firewall.vpn_killswitch.name='vpn-killswitch'
uci set firewall.vpn_killswitch.src='lan'
uci set firewall.vpn_killswitch.dest='wan'
uci set firewall.vpn_killswitch.proto='all'
uci set firewall.vpn_killswitch.target='REJECT'
uci set firewall.vpn_killswitch.enabled='1'
# The exemption list — devices that bypass the kill switch
uci -q delete firewall.vpn_killswitch.src_mac
uci add_list firewall.vpn_killswitch.src_mac='!AA:BB:CC:DD:EE:FF' # smart TV
# Add more exemptions as needed:
# uci add_list firewall.vpn_killswitch.src_mac='!11:22:33:44:55:66'
uci commit firewall
service firewall restart
```
The resulting `/etc/config/firewall` entry:
<!-- snippet: pbr-killswitch-config -->
```
config rule 'vpn_killswitch'
option name 'vpn-killswitch'
option src 'lan'
option dest 'wan'
option proto 'all'
option target 'REJECT'
option enabled '1'
list src_mac '!AA:BB:CC:DD:EE:FF'
```
5.2 — Adding / removing exemptions later
<!-- snippet: pbr-killswitch-exemptions -->
```sh
# Add another bypass MAC
uci add_list firewall.vpn_killswitch.src_mac='!11:22:33:44:55:66'
uci commit firewall
service firewall restart
# Remove a specific exemption
uci -q del_list firewall.vpn_killswitch.src_mac='!11:22:33:44:55:66'
uci commit firewall
service firewall restart
# Disable kill switch temporarily (e.g. troubleshooting)
uci set firewall.vpn_killswitch.enabled='0'
uci commit firewall
service firewall reload
```
> 🧰 Lots of IPs to exempt? The [PBR Firewall Exceptions Generator](#-extra-tools) writes the policy blocks and `!IP` lines for you.
6. **Verification**
<!-- snippet: pbr-verify -->
```sh
# Status
service pbr status
ip rule show
ip route show table wan
ip route show table wg0
uci show pbr
uci show firewall.vpn_killswitch
# Reload after changes
service pbr reload
service firewall reload # or `restart` for bigger changes
# Disable PBR entirely (without uninstalling)
uci set pbr.config.enabled='0'
uci commit pbr
service pbr restart
```
</details>
------------------------------------------------------------------------
<details>
<summary><span style="font-size:1.2em;font-weight:600">🌐 Step 6 --- Manage IPv6</span></summary>
<details>
<summary><strong>Disable IPv6</strong></summary>
1. Remove WAN6 interface entirely
<!-- snippet: ipv6-disable-wan6 -->
``` bash
uci delete network.wan6
```
2. Disable IPv6 on LAN
<!-- snippet: ipv6-disable-lan -->
``` bash
uci set network.lan.ipv6='0'
uci set network.lan.delegate='0'
uci set dhcp.lan.dhcpv6='disabled'
uci set dhcp.lan.ra='disabled'
```
3. Commit changes
4. Disable odhcpd (not needed without IPv6)
<!-- snippet: ipv6-disable-odhcpd -->
``` bash
/etc/init.d/odhcpd disable
/etc/init.d/odhcpd stop
```
5. **Optional:** Kernel-Level Disable
<!-- snippet: ipv6-disable-kernel -->
``` bash
cat >> /etc/sysctl.conf << 'EOF'
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
EOF
sysctl -p
```
6. Restart network
<!-- snippet: ipv6-disable-restart -->
``` bash
/etc/init.d/network restart
```
</details>
<details>
<summary><strong>Enable IPv6</strong></summary>
1. Create WAN6 interface
<!-- snippet: ipv6-enable-wan6 -->
``` bash
uci set network.wan6=interface
uci set network.wan6.proto='dhcpv6'
uci set network.wan6.device='@wan'
```
2. Enable IPv6 on LAN
<!-- snippet: ipv6-enable-lan -->
``` bash
uci set network.lan.ipv6='1'
uci set network.lan.delegate='1'
uci set dhcp.lan.dhcpv6='server'
uci set dhcp.lan.ra='server'
```
3. Commit changes
<!-- snippet: ipv6-enable-commit -->
``` bash
uci commit
```
4. Enable odhcpd
<!-- snippet: ipv6-enable-odhcpd -->
``` bash
/etc/init.d/odhcpd enable
/etc/init.d/odhcpd start
```
5. **Optional:** Remove Kernel-Level Disable (if previously added)
<!-- snippet: ipv6-enable-kernel -->
``` bash
sed -i '/net.ipv6.conf.all.disable_ipv6/d' /etc/sysctl.conf
sed -i '/net.ipv6.conf.default.disable_ipv6/d' /etc/sysctl.conf
sysctl -w net.ipv6.conf.all.disable_ipv6=0
sysctl -w net.ipv6.conf.default.disable_ipv6=0
```
6. Restart network
<!-- snippet: ipv6-enable-restart -->
``` bash
/etc/init.d/network restart
```
</details>
</details>
</details>
## 🎉 Done!
You now have:
- ✅ OpenWrt installed
- ✅ LuCI Web UI
- ✅ Adblock filtering
- ✅ WireGuard VPN (general + NordVPN setup)
- ✅ IPv6 control
------------------------------------------------------------------------
## 🧰 Extra Tools
### 🔑 NordVPN Private Key Extractor
[`tools/nordvpn_priv_key_extractor.ps1`](./tools/nordvpn_priv_key_extractor.ps1) · [`.exe`](./tools/nordvpn_priv_key_extractor.exe)
Asks for your NordVPN access token (masked input) and queries NordVPN's credentials API to print your service username, password and NordLynx (WireGuard) private key.
<!-- snippet: tool-nordvpn-run -->
```powershell
powershell -ExecutionPolicy Bypass -File .\nordvpn_priv_key_extractor.ps1
```
> The `.exe` is unsigned, so Windows SmartScreen may warn you. Prefer the `.ps1` if you want to read the code before running it.
### 🧭 PBR Firewall Exceptions Generator
[`tools/pbr_firewall_exceptions_gen.py`](./tools/pbr_firewall_exceptions_gen.py)
Paste IPs (one per line, or separated by commas/spaces) and get `config policy` blocks that route each one via `wan`, plus `list dest_ip '!IP'` lines for your kill-switch rule (Step 5.1).
<!-- snippet: tool-pbr-run -->
```sh
python3 pbr_firewall_exceptions_gen.py
```
Paste the IPs into the terminal, then press Enter on an empty line. An in-browser version is available on the [website](https://cudy-openwrt-tutorial.vercel.app/#pbr-generator).
### 🛡️ Firmware checksums
[`firmware/SHA256SUMS.txt`](./firmware/SHA256SUMS.txt)
Before you flash, make sure the zip downloaded intact. Compute its SHA-256 and compare it with the value in `SHA256SUMS.txt`.
<!-- snippet: tool-checksums -->
```shell
# Windows (PowerShell)
Get-FileHash .\WR3000E-V1.zip -Algorithm SHA256
# Linux / macOS
shasum -a 256 WR3000E-V1.zip
```
------------------------------------------------------------------------
## 📁 Repository Layout
| Path | Contents |
| ---- | -------- |
| [`firmware/`](./firmware/) | Cudy intermediary OpenWrt firmware packages + `SHA256SUMS.txt` |
| [`tools/`](./tools/) | NordVPN key extractor and PBR exceptions generator |
| [`assets/images/`](./assets/images/) | Guide screenshots |
| [`assets/icons/`](./assets/icons/) | App icon, favicons and social preview |
| [`src/`](./src/), [`index.html`](./index.html) | Website (React + TypeScript + Vite), deployed on Vercel |
**Website development:** run `npm install`, then `npm run dev`. Every command block on the website is read straight from this README through the invisible `<!-- snippet: … -->` markers, so edit commands here and the site follows. `npm test` checks that the README, the website and the firmware checksums stay in sync.
------------------------------------------------------------------------
⚠️ **Disclaimer:** Use this at your own risk. Flashing firmware may void
warranty or brick your device.