Flash OpenWrt on
TP-Link ER605 v2
Complete guide: from stock firmware to a fully configured OpenWrt router with encrypted DNS, network-wide ad blocking, and secure NTP.
i Overview
This guide covers replacing the stock TP-Link firmware on the ER605 v2 with OpenWrt. It applies to all hardware revisions (V2.0, V2.20, V2.26, V2.60) — the limiting factor is the firmware version, not the hardware revision.
Based on chill1Penguin's method. The password generator, backup scripts, and initramfs image come from that project.
Do NOT connect to the internet
Keep the WAN cable disconnected during the entire flashing process. If the router detects internet access, TP-Link requires an online-signed token and the locally generated password stops working.
No factory recovery after flashing
After flashing OpenWrt, the ER605's Emergency Recovery Mode is disabled. The only way back to stock firmware is via ubiformat from OpenWrt's console using your MTD backup.
Bricking risk
If power is lost during flashing, the router may be permanently bricked. Use a stable power source and don't disconnect anything during the process.
Firmware Compatibility
The hardware revision (V2.0, V2.20, etc.) does NOT matter. Only the firmware version matters.
| Firmware | Status | Root Access Method |
|---|---|---|
| ≤ 2.0.1 | ✅ Works | Direct root login with "root password" |
| 2.1.1 – 2.2.5 | ✅ Works | GUI login + enable + debug + CLI debug password |
| 2.2.6 | ❌ Blocked | Local password generator patched out |
| 2.3.0 – 2.3.3 | ❌ Blocked | RSA-signed tokens + anti-rollback. Downgrade impossible. |
Firmware 2.2.6+ is confirmed blocked. Tested live in March 2026: downgrade from 2.3.3 to 2.2.5 was impossible using Emergency Recovery, GUI rollback, SSH rollback, and factory reset. If your router already has 2.2.6+, your options are: contact TP-Link support, buy another ER605 with 2.2.5, or wait for a community bypass.
Password Generator
Generates root and debug passwords locally in your browser. Nothing is sent to any server. Only works with firmware ≤ 2.2.5.
Found on the label on the bottom of your ER605 or in System Status. Formats: AA:BB:CC:DD:EE:FF, AA-BB-CC-DD-EE-FF, or AABBCCDDEEFF.
Invalid MAC address. Must contain exactly 12 hex characters.
The username you use to log in to the web GUI.
Flash Guide
Estimated time: 30–45 minutes. Requirements: a Linux PC, an ethernet cable, ssh, curl, python3, md5sum.
Phase 1 — Preparation (with internet)
Download everything
Your PC needs internet for this step. Download all files before touching the router.
1a. Generate the debug password using the generator above.
Save both CLI debug passwords to a text file — you'll need the right one depending on your firmware version.
1b. Download flashing files:
mkdir -p ~/er605_flash
cd ~/er605_flash
curl -o er605v2_write_initramfs.sh https://raw.githubusercontent.com/chill1Penguin/er605v2_openwrt_install/main/er605v2_write_initramfs.sh
curl -o openwrt-initramfs-compact.bin https://raw.githubusercontent.com/chill1Penguin/er605v2_openwrt_install/main/openwrt-initramfs-compact.bin
1c. Download sysupgrade image:
# Sysupgrade 25.12.2 (latest stable):
curl -o openwrt-25.12.2-sysupgrade.bin https://downloads.openwrt.org/releases/25.12.2/targets/ramips/mt7621/openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin
1d. Verify you have everything:
ls -la ~/er605_flash/
You should see: er605v2_write_initramfs.sh, openwrt-initramfs-compact.bin, and the sysupgrade .bin file.
Disconnect internet & verify firmware
- Disconnect the WAN cable from the router
- Connect your PC to a LAN port via ethernet
- Go to
http://192.168.0.1and check the Firmware Version in System Status
If firmware is 2.2.5 or earlier → continue. If 2.2.6+ → you're blocked.
Enable SSH on the router
In the web GUI: System Tools → Diagnostics → Remote Assistance → enable it.
Phase 2 — SSH Access
Connect via SSH
The ER605's SSH server uses legacy algorithms that modern distros block by default. Pass them directly in the command:
ssh -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa -c aes128-ctr [email protected]
Use your GUI username (usually admin). Password is the same as the GUI.
Enter debug mode
Once inside SSH, run:
enable
debug
Paste the CLI debug mode password from Step 1. If it works, you'll have a root shell.
Phase 3 — MTD Backup (strongly recommended)
Back up all MTD partitions
Option A: Network backup with netcat (recommended)
No USB needed. Two scripts handle everything: one runs on your PC and receives the data, the other runs on the router and sends it. Partitions are streamed directly without storing anything in the router's RAM.
Step 1 — Find your PC's IP:
ip -4 addr show | grep "192.168.0"
Note the IP (e.g. 192.168.0.109). You'll need it below.
Method 1: Simple scripts (one partition per connection)
receive_backup.sh — run on your PC:
#!/bin/bash
# receive_backup.sh — Run on your PC
# Receives all MTD partitions from the ER605 via netcat
DIR=~/er605_backup
mkdir -p "$DIR"
PARTITIONS="0:Bootloader 1:Config 2:Factory 3:firmware 4:panic-oops 5:partition-table 6:support-list 7:device-info 8:device-info.b 9:tddp 10:tddp.b 11:bootloader 12:kernel 13:rootfs 14:firmware-info 15:extra-para 16:log 17:rootfs_data 18:bootloader.b 19:kernel.b 20:rootfs.b 21:firmware-info.b 22:extra-para.b 23:log.b 24:rootfs_data.b 25:log_recovery 26:database"
for PART in $PARTITIONS; do
NUM="${PART%%:*}"
NAME="${PART##*:}"
FILE="$DIR/mtd${NUM}_${NAME}.bin"
echo ""
echo "=== Waiting for mtd${NUM} (${NAME})... ==="
nc -l -p 9090 > "$FILE"
SIZE=$(stat -c%s "$FILE" 2>/dev/null || echo 0)
echo " Received: $FILE ($SIZE bytes)"
done
echo ""
echo "=== Backup complete ==="
ls -lh "$DIR"
nc -l -p 9090 gives an error (common on Debian/Ubuntu with OpenBSD netcat), change it to nc -l 9090 without the -p flag.send_backup.sh — run on the router:
#!/bin/ash
# send_backup.sh — Run on the ER605 router (ash/BusyBox)
# Usage: ash send_backup.sh YOUR_PC_IP
PC_IP="$1"
if [ -z "$PC_IP" ]; then
echo "Usage: ash send_backup.sh YOUR_PC_IP"
exit 1
fi
i=0
while [ -e "/dev/mtd${i}ro" ]; do
NAME=$(grep "mtd${i}:" /proc/mtd | cut -d'"' -f2)
echo ""
echo "=== Sending mtd${i} (${NAME})... ==="
dd if="/dev/mtd${i}ro" | nc "$PC_IP" 9090
echo " mtd${i} sent."
sleep 2
i=$((i + 1))
done
echo ""
echo "=== All partitions sent ==="
How to use:
- Transfer
send_backup.shto the router via the HTTP server:curl -o /tmp/send_backup.sh http://YOUR_PC_IP:8080/send_backup.sh - Run
./receive_backup.shon your PC (it waits for each partition) - Run
ash /tmp/send_backup.sh YOUR_PC_IPon the router
Method 2: chill1Penguin's scripts (with auto md5sum verification)
backup.sh — paste and run on the router:
Edit BACKUP_HOST to match your PC's IP, then paste this entire block into the router's shell:
cat << "EOF" > /tmp/backup.sh
#!/bin/sh
BACKUP_HOST="192.168.0.100" # Your PC's IP address
BACKUP_PORT="9999" # Port to use for transfer
echo "Backup host ${BACKUP_HOST}"
echo -n > /tmp/md5sums
cat /proc/mtd | tail -n+2 | while read; do
MTD_DEV=$(echo ${REPLY} | cut -f1 -d:)
MTD_NAME=$(echo ${REPLY} | cut -f2 -d\")
FILENAME="${MTD_DEV}_${MTD_NAME}.backup"
echo "Backing up ${MTD_DEV} (${MTD_NAME})"
# First send filename
echo "$FILENAME" | busybox nc ${BACKUP_HOST} ${BACKUP_PORT}
sleep 1
# Then send the data
dd if=/dev/${MTD_DEV}ro | busybox nc ${BACKUP_HOST} ${BACKUP_PORT}
SUM=$(dd if=/dev/${MTD_DEV}ro | md5sum | cut -f1 -d" ")
echo -e "$SUM\t$FILENAME" >> /tmp/md5sums
sleep 1
done
echo md5sums | busybox nc ${BACKUP_HOST} ${BACKUP_PORT}
dd if=/tmp/md5sums | busybox nc ${BACKUP_HOST} ${BACKUP_PORT}
EOF
chmod +x /tmp/backup.sh
receive.sh — save and run on your PC:
#!/bin/bash
# receive.sh — Run on your PC
# Receives MTD backups sent by backup.sh from the router
PORT=9999
DIR=~/er605_backup
mkdir -p "$DIR"
cd "$DIR"
while true; do
# Receive filename
FILENAME=$(nc -l -p $PORT)
FILENAME=$(echo "$FILENAME" | tr -d '\r\n')
if [ -z "$FILENAME" ]; then continue; fi
echo "Receiving ${FILENAME}..."
# Receive file data
nc -l -p $PORT > "$FILENAME"
done
nc -l -p gives an error, change to nc -l without -p.How to use:
- Run
./receive.shon your PC - Run
/tmp/backup.shon the router - Backup takes ~5 minutes. Don't interrupt it.
- When done, press Ctrl+C on the PC to stop receive.sh
- Verify:
md5sum -c md5sums
Tip: mtd3 (firmware) is the largest (~130MB) and takes ~2 minutes. The rest are small and take seconds. Store these backups somewhere safe — they're your only way back to stock firmware without a serial console.
Original scripts from chill1Penguin/MTD_backup.
Option B: USB backup
Format a 256MB+ USB drive as FAT32, plug it into the router, mount it, and dump all partitions.
i=0; while [ -e "/dev/mtd${i}ro" ]; do
NAME=$(grep "mtd${i}:" /proc/mtd | cut -d'"' -f2)
echo "Backing up mtd${i} (${NAME})..."
dd if="/dev/mtd${i}ro" of="/mnt/usb/mtd${i}_${NAME}.bin"
i=$((i + 1))
done
cp /proc/mtd /mnt/usb/proc_mtd.txt
sync && umount /mnt/usb
Phase 4 — Transfer files to the router
Start a temporary HTTP server on your PC
cd ~/er605_flash
python3 -m http.server 8080
sudo firewall-cmd --add-port=8080/tcp. Ubuntu: sudo ufw allow 8080/tcp.Download files from the router
cd /tmp
curl -o er605v2_write_initramfs.sh http://YOUR_PC_IP:8080/er605v2_write_initramfs.sh
curl -o openwrt-initramfs-compact.bin http://YOUR_PC_IP:8080/openwrt-initramfs-compact.bin
chmod +x er605v2_write_initramfs.sh
Verify checksum
md5sum openwrt-initramfs-compact.bin
The output must match:
e06dd6da68b739b2544931a0203292db openwrt-initramfs-compact.bin
Source: chill1Penguin/md5sums. If they don't match, do NOT continue.
Phase 5 — Flashing (point of no return)
Flash the initramfs image
./er605v2_write_initramfs.sh openwrt-initramfs-compact.bin
Reboot
reboot
Wait 2–3 minutes. All LEDs will turn off, then blink, then stabilize. This is normal.
# Monitor from your PC:
while ! ping -c1 -W1 192.168.1.1 &>/dev/null; do echo "Waiting..."; sleep 2; done; echo "Router is up!"
Phase 6 — OpenWrt Setup
Access the installer
Open http://192.168.1.1 — note the IP changed from 192.168.0.1 to 192.168.1.1. You should see the "ER605 v2 Installer" page.
sudo ip addr add 192.168.1.100/24 dev YOUR_INTERFACE.Adjust UBI layout & flash sysupgrade 25.12.2
- Click "Adjust UBI Layout" if it says "NOT ADJUSTED"
- Upload the 25.12.2 sysupgrade image, uncheck "Keep settings", and click Flash
- Wait several minutes — do NOT power off the router
- After reboot, access
http://192.168.1.1→ you should see LuCI. Login: root, password: empty.
Post-Install Configuration
uci set system.@system[0].timezone='YOUR_TZ_STRING'
uci set system.@system[0].zonename='Your/Timezone'
uci commit system
Change the values to match your timezone. See the OpenWrt timezone list for valid values.
ssh [email protected]
apk update
apk add luci-app-sqm # Smart QoS
apk add luci-proto-wireguard # Fast VPN
apk add luci-app-statistics collectd-mod-cpu collectd-mod-memory # Stats
Encrypted DNS (dnscrypt-proxy2)
All DNS queries from your network will be encrypted via DNSCrypt/DoH before reaching the internet. dnsmasq stays as local DNS, forwarding to dnscrypt-proxy on port 5353.
Install
apk update
apk add dnscrypt-proxy2
Configure
Edit /etc/dnscrypt-proxy2/dnscrypt-proxy.toml — key changes:
# Listen on port 5353 (not 53, that's dnsmasq)
listen_addresses = ['127.0.0.1:5353']
# Quad9 DoH with DNSSEC + malware filter + ECS
server_names = ['quad9-doh-ip4-port443-filter-ecs-pri']
# Don't require nofilter — we WANT Quad9's malware filtering
require_nofilter = false
# Allow expired certs on startup (no battery-backed clock on ER605)
cert_ignore_timestamp = true
# Cache (recommended for 128MB RAM)
cache = true
cache_size = 1024
cache_min_ttl = 600
cache_max_ttl = 86400
# Block IPv6 if not used
block_ipv6 = true
listen_addresses line. Check with: grep -n '^listen_addresses' /etc/dnscrypt-proxy2/dnscrypt-proxy.tomlRedirect dnsmasq to dnscrypt-proxy
uci delete dhcp.@dnsmasq[0].server 2>/dev/null
uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].logqueries='0'
uci commit dhcp
service dnsmasq restart
noresolv='1'. Without it, dnsmasq still uses the ISP's DNS servers received via DHCP on the WAN interface — your queries will leak to the ISP even though dnscrypt-proxy is running. Verify with: uci get dhcp.@dnsmasq[0].noresolv (must return 1).Force the router itself to use encrypted DNS
The steps above only cover dnsmasq (which serves LAN clients). The router's own DNS resolution uses /etc/resolv.conf, which is populated by the ISP's DNS servers received via DHCP on the WAN interface. Without this step, the router itself still sends unencrypted DNS queries to your ISP.
uci set network.wan.peerdns='0'
uci set network.wan.dns='127.0.0.1'
uci commit network
service network restart
noresolv='1' only affects dnsmasq (LAN clients). The router's own system processes — curl (blocklist downloads), chrony (NTP resolution), apk (package updates) — use /etc/resolv.conf, which still contains your ISP's raw DNS servers unless you disable peerdns. Verify with: cat /etc/resolv.conf (should show 127.0.0.1, not your ISP's IPs).Enable and verify
service dnscrypt-proxy start
service dnscrypt-proxy enable
netstat -tlnup | grep 5353 # Should show dnscrypt-proxy
apk add bind-dig
dig @127.0.0.1 -p 5353 example.com # Should resolve
From a device on your network, visit dnsleaktest.com → Extended test. You should see Quad9 servers, not your ISP.
Network-wide Ad Blocking
Direct dnsmasq-based blocking using the Hagezi Pro++ blocklist (~240,000 domains). No extra packages needed — dnsmasq loads the blocklist from a config directory in /tmp (RAM) because the blocklist is too large for the router's limited flash storage (~7MB free). Since /tmp is cleared on every reboot, an init script recreates the directory and a hotplug script re-downloads the list when WAN comes up. Blocks ads, tracking, malware, and phishing for every device on the network.
Set up dnsmasq config directory
mkdir -p /tmp/dnsmasq.d
# delete first to avoid duplicates if you run this more than once
uci delete dhcp.@dnsmasq[0].confdir 2>/dev/null
uci add_list dhcp.@dnsmasq[0].confdir='/tmp/dnsmasq.d'
uci commit dhcp
Since /tmp is cleared on every reboot, create an init script to recreate the directory before dnsmasq starts (priority 18, dnsmasq is 19):
cat > /etc/init.d/mkdir-dnsmasq-confdir << 'EOF'
#!/bin/sh /etc/rc.common
START=18
start() {
mkdir -p /tmp/dnsmasq.d
}
EOF
chmod +x /etc/init.d/mkdir-dnsmasq-confdir
/etc/init.d/mkdir-dnsmasq-confdir enable
Create the update script
cat > /usr/sbin/update-blocklist.sh << 'EOF'
#!/bin/sh
curl -s -o /tmp/dnsmasq.d/blocklist.conf \
https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/dnsmasq/pro.plus.txt && \
/etc/init.d/dnsmasq restart
EOF
chmod +x /usr/sbin/update-blocklist.sh
Download blocklist and verify
/usr/sbin/update-blocklist.sh
wc -l /tmp/dnsmasq.d/blocklist.conf # Should show ~240k lines
nslookup ads.google.com # Should return NXDOMAIN
nslookup google.com # Should resolve normally
Switch blocklist
You can switch to a different Hagezi blocklist at any time. Edit the URL in the update script. Available options (dnsmasq format):
| List | Domains | LIST_NAME | Description |
|---|---|---|---|
| Light | ~95k | light | Ads + tracking only. Minimal breakage |
| Multi | ~130k | multi | Ads, tracking, and some malware |
| Pro | ~180k | pro | Ads, tracking, malware, phishing |
| Pro++ | ~240k | pro.plus | Pro + extra aggressive blocking ← used in this guide |
| Ultimate | ~350k | ultimate | Maximum blocking. May break some sites |
To switch, edit the script and replace pro.plus with the LIST_NAME from the table, then re-run:
sed -i 's|/dnsmasq/.*\.txt|/dnsmasq/LIST_NAME.txt|' /usr/sbin/update-blocklist.sh
/usr/sbin/update-blocklist.sh
wc -l /tmp/dnsmasq.d/blocklist.conf # Verify the new domain count
Auto-update lists daily
echo '0 4 * * * /usr/sbin/update-blocklist.sh' >> /etc/crontabs/root
/etc/init.d/cron restart
Auto-load blocklist on boot
The init script above creates the directory, but the blocklist file itself is also lost on reboot. This hotplug script re-downloads it automatically when the WAN interface gets a public IP:
cat > /etc/hotplug.d/iface/99-blocklist << 'EOF'
#!/bin/sh
[ "$ACTION" = ifup ] && [ "$INTERFACE" = wan ] && {
sleep 5
/usr/sbin/update-blocklist.sh
}
EOF
chmod +x /etc/hotplug.d/iface/99-blocklist
Allowlist a domain
Remove the matching line from the blocklist and restart dnsmasq:
sed -i '/example\.com/d' /tmp/dnsmasq.d/blocklist.conf
/etc/init.d/dnsmasq restart
Note: the domain will be re-blocked on the next daily update. For a permanent allowlist, create a separate file:
# Add to a persistent allowlist that survives updates
mkdir -p /etc/dnsmasq.d
echo 'server=/example.com/#' > /etc/dnsmasq.d/allowlist.conf
uci add_list dhcp.@dnsmasq[0].confdir='/etc/dnsmasq.d'
uci commit dhcp
/etc/init.d/dnsmasq restart
NTP + NTS (Chrony)
Secure time synchronization with Cloudflare via Network Time Security (NTS). Prevents time-based attacks and serves NTP to all LAN devices.
Install
apk update
apk del chrony
apk add chrony-nts ca-certificates
rm -f /etc/config/chrony-opkg
Configure Cloudflare NTS
# Remove default pool and server entries (pool overrides NTS)
while uci -q get chrony.@pool[0]; do uci delete chrony.@pool[0]; done
while uci -q get chrony.@server[0]; do uci delete chrony.@server[0]; done
# Add Cloudflare NTS server
uci add chrony server
uci set chrony.@server[-1].hostname='time.cloudflare.com'
uci set chrony.@server[-1].iburst='yes'
uci set chrony.@server[-1].nts='yes'
uci commit chrony
/etc/init.d/chronyd restart
Serve NTP to your LAN
By default chrony only syncs its own clock. To make the router act as a local NTP server for all LAN devices, you need to allow your subnet and disable the built-in sysntpd so it doesn't conflict.
# Disable the built-in NTP server (busybox sysntpd) — chrony replaces it
/etc/init.d/sysntpd stop
/etc/init.d/sysntpd disable
# Enable NTP server on port 123 and allow LAN clients
# We use conf.d because chrony defaults to port 0 (NTP disabled)
# and the UCI 'allow' option ignores the 'subnet' field.
mkdir -p /etc/chrony/conf.d
cat > /etc/chrony/conf.d/ntp-server.conf << 'EOF'
port 123
allow 192.168.1.0/24
EOF
/etc/init.d/chronyd restart
Verify NTP server + NTS
# Check NTS sync with Cloudflare
chronyc sources -a # Should show time.cloudflare.com with ^*
chronyc -N authdata # Mode should be NTS, Cook > 0
chronyc tracking # Leap status should be Normal
# Check the router is serving NTP to the LAN
chronyc serverstats # NTP packets received > 0 means clients are connecting
netstat -ulnp | grep 123 # Should show chronyd on 0.0.0.0:123 (all interfaces)
netstat -ulnp | grep 123 shows 0.0.0.0:123 (all interfaces) — if it shows 127.0.0.1:123, chrony is only serving localhost.Linux (chrony) — point chrony to router
Many Linux distros (Fedora, RHEL, Arch, etc.) ship with chrony by default. Replace the default NTP pool/server entries with your router:
# Find your chrony config (varies by distro)
# Fedora/RHEL/Arch: /etc/chrony.conf
# Debian/Ubuntu: /etc/chrony/chrony.conf
CONF=/etc/chrony.conf
[ -f /etc/chrony/chrony.conf ] && CONF=/etc/chrony/chrony.conf
# Comment out existing pool/server lines (skip if already correct)
sudo sed -i '/^server 192\.168\.1\.1 iburst$/!{ s/^pool .*/#&/; s/^server .*/#&/; }' "$CONF"
# Add the router line only if not already present (idempotent)
grep -qx 'server 192.168.1.1 iburst' "$CONF" || \
echo -e '\nserver 192.168.1.1 iburst' | sudo tee -a "$CONF"
# Restart chrony and verify
sudo systemctl restart chronyd
chronyc sources -a # Should show 192.168.1.1 with ^*
chronyc tracking # Ref ID should point to 192.168.1.1
chronyc -h 192.168.1.1 tracking will fail with "506 Cannot talk to daemon" — this is normal. That command uses chrony's admin protocol (port 323), which is restricted to localhost for security. NTP clients use port 123, which is what matters. Use chronyc sources -a on the client to verify sync.Linux (systemd-timesyncd) — point timesyncd to router
Some distros (Ubuntu, Debian, Linux Mint) use systemd-timesyncd instead of chrony. Check which one you have and configure accordingly:
# Check which NTP client is active
timedatectl timesync-status 2>/dev/null && echo "Using systemd-timesyncd" || echo "Not using timesyncd"
# Configure timesyncd to use the router
sudo mkdir -p /etc/systemd/timesyncd.conf.d
printf '[Time]\nNTP=192.168.1.1\nFallbackNTP=\n' | sudo tee /etc/systemd/timesyncd.conf.d/router.conf
# Restart and verify
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status # Server should show 192.168.1.1
Firewall Hardening
OpenWrt's default config is already solid. These two tweaks improve security and performance with no risk.
Drop invalid packets
Network → Firewall → General Settings → check "Drop invalid packets". Drops malformed or out-of-sequence packets that are often part of port scans or fingerprinting attacks.
Software flow offloading
Network → Firewall → Routing/NAT Offloading → "Software flow offloading". Established connections bypass nftables rules for much better throughput on the MT7621.
Performance
Tweaks to improve throughput, DNS response times, and reduce resource usage on the ER605's dual-core MT7621 with 128MB of RAM. All of these are reversible.
Enable Packet Steering
By default, all network interrupts land on a single CPU core. Packet Steering distributes packet processing across both cores of the MT7621, reducing bottlenecks under heavy traffic.
uci set network.globals.packet_steering='1'
uci commit network
service network restart
Enable Software Flow Offloading
Allows established connections to bypass the full firewall/NAT processing path, significantly improving throughput. Already recommended in the Post-Install section via LuCI — here's the UCI equivalent.
uci set firewall.@defaults[0].flow_offloading='1'
uci commit firewall
service firewall restart
Increase dnsmasq DNS cache
The default cache holds only 150 entries. Increasing it to 1000 means repeated DNS queries resolve instantly from local cache instead of hitting dnscrypt-proxy again. The memory cost is negligible (~100KB).
uci set dhcp.@dnsmasq[0].cachesize='1000'
uci commit dhcp
service dnsmasq restart
Disable IPv6 if your ISP doesn't provide it
odhcpd handles DHCPv6 and Router Advertisements for IPv6. If your ISP doesn't give you IPv6 (most residential connections in Latin America don't), it's running for nothing — using RAM and CPU cycles with no benefit.
First, check if you actually have IPv6:
ip -6 addr show scope global
If it returns nothing (no global IPv6 address), you can safely disable odhcpd:
service odhcpd stop
service odhcpd disable
service odhcpd enable && service odhcpd start. This is completely reversible.Reduce system log buffer
By default, logd keeps a 64KB log buffer in RAM. Reducing it to 32KB saves memory while still keeping enough log history for basic debugging:
uci set system.@system[0].log_size='32'
uci commit system
/etc/init.d/log restart
Check current RAM usage
Monitor available memory at any time:
free -m
With everything running (dnsmasq + blocklist + dnscrypt-proxy + chrony), expect around 50–60MB free. That's plenty for the ER605.
Quick Reference
| Command | What it does |
|---|---|
| ssh [email protected] | Connect to router |
| service <name> start/stop/restart | Control a service |
| service <name> enable/disable | Enable/disable on boot |
| logread -e <service> | View logs for a service |
| netstat -tlnup | Show listening ports |
| free -m | RAM usage |
| df -h | Disk / overlay space |
| apk list --installed | Installed packages |
| uci show <component> | View UCI config |
| Active service | Function |
|---|---|
| update-blocklist.sh | DNS blocklist via dnsmasq (Hagezi) |
| chronyd | Time sync (NTP + NTS) |
| dnscrypt-proxy | Encrypted DNS (DNSCrypt/DoH) |
| dnsmasq | Local DNS + DHCP |
| dropbear | SSH server |
| firewall | nftables (fw4) |
Post-Reboot Verification
All configuration set via uci commit persists across reboots (stored in /etc/config/ on flash). Services enabled with service <name> enable start automatically on boot. Run these commands after a reboot to confirm everything is working:
# dnsmasq → dnscrypt-proxy forwarding
uci get dhcp.@dnsmasq[0].server # → 127.0.0.1#5353
uci get dhcp.@dnsmasq[0].noresolv # → 1
uci get dhcp.@dnsmasq[0].logqueries # → 0
# Router's own DNS (not leaking to ISP)
uci get network.wan.peerdns # → 0
uci get network.wan.dns # → 127.0.0.1
cat /etc/resolv.conf # → search lan + nameserver 127.0.0.1 (no ISP IPs)
# dnscrypt-proxy listening
netstat -tlnup | grep 5353 # → dnscrypt-proxy on TCP+UDP
# Ad blocking active (blocklist loaded)
wc -l /tmp/dnsmasq.d/blocklist.conf # → ~240k lines
# NTP with NTS + LAN server
chronyc sources -a # → time.cloudflare.com with ^*
chronyc -N authdata # → NTS mode, cookies > 0
chronyc serverstats # → NTP packets received > 0
# DNS resolves correctly
dig @127.0.0.1 -p 5353 example.com # → NOERROR + IP address
nslookup ads.google.com # → NXDOMAIN (blocked)
# Performance tweaks
uci get network.globals.packet_steering # → 1
uci get firewall.@defaults[0].flow_offloading # → 1
uci get dhcp.@dnsmasq[0].cachesize # → 1000
| Check | Expected result |
|---|---|
| uci get dhcp.@dnsmasq[0].server | 127.0.0.1#5353 |
| uci get dhcp.@dnsmasq[0].noresolv | 1 |
| uci get dhcp.@dnsmasq[0].logqueries | 0 |
| uci get network.wan.peerdns | 0 |
| uci get network.wan.dns | 127.0.0.1 |
| cat /etc/resolv.conf | 127.0.0.1 only (no ISP IPs) |
| netstat -tlnup | grep 5353 | dnscrypt-proxy on TCP + UDP |
| wc -l /tmp/dnsmasq.d/blocklist.conf | ~240k lines (blocklist loaded) |
| chronyc sources -a | time.cloudflare.com with ^* |
| chronyc -N authdata | NTS mode, cookies > 0 |
| chronyc serverstats | NTP packets received > 0 (LAN server) |
| nslookup ads.google.com | NXDOMAIN |
| uci get network.globals.packet_steering | 1 |
| uci get firewall.@defaults[0].flow_offloading | 1 |
| uci get dhcp.@dnsmasq[0].cachesize | 1000 |
Troubleshooting
SSH: Connection refused
SSH: "REMOTE HOST IDENTIFICATION HAS CHANGED"
Expected after flashing — the SSH server changed. Remove the old key:
ssh-keygen -R 192.168.0.1
ssh-keygen -R 192.168.1.1
SSH: "no matching host key / cipher / kex"
Use the full command with legacy algorithm flags:
ssh -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa -c aes128-ctr [email protected]
Can't access 192.168.1.1 after reboot
sudo nmcli con mod "Wired connection 1" ipv4.method manual ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1, then reconnect. SSH in and check: logread | grep dnsmasq. Common causes: (1) /tmp/dnsmasq.d doesn't exist — verify the init script is enabled: /etc/init.d/mkdir-dnsmasq-confdir enable. (2) Duplicate confdir entries — fix with: uci delete dhcp.@dnsmasq[0].confdir; uci add_list dhcp.@dnsmasq[0].confdir='/tmp/dnsmasq.d'; uci commit dhcp. Remember to switch your PC back to DHCP after.dnscrypt-proxy: port 53 already in use
listen_addresses lines in the .toml, or the active one points to port 53 (used by dnsmasq). Ensure only one line exists pointing to 127.0.0.1:5353.Blocklist is empty or not loaded
/usr/sbin/update-blocklist.sh. Check that curl can reach the CDN: curl -sI https://cdn.jsdelivr.net. Verify the file exists: ls -la /tmp/dnsmasq.d/blocklist.conf.DNS doesn't resolve anything
rm /tmp/dnsmasq.d/blocklist.conf && /etc/init.d/dnsmasq restart. Check dnsmasq: service dnsmasq status. Test dnscrypt-proxy directly: dig @127.0.0.1 -p 5353 example.com. If that works but the network doesn't resolve, restart dnsmasq.Credentials don't work after downgrade/rollback
Revert to Stock Firmware
Rollback DNS changes
uci delete dhcp.@dnsmasq[0].server
uci set dhcp.@dnsmasq[0].noresolv='0'
uci delete dhcp.@dnsmasq[0].logqueries 2>/dev/null
uci commit dhcp
uci set network.wan.peerdns='1'
uci delete network.wan.dns 2>/dev/null
uci commit network
service dnsmasq restart
service network restart
Rollback NTP/NTS changes
apk del chrony-nts
apk add chrony
rm -f /etc/chrony/conf.d/ntp-server.conf
uci delete chrony.@server[0]
uci add chrony pool
uci set chrony.@pool[-1].hostname='2.openwrt.pool.ntp.org'
uci set chrony.@pool[-1].maxpoll='12'
uci set chrony.@pool[-1].iburst='yes'
uci commit chrony
/etc/init.d/chronyd restart
/etc/init.d/sysntpd enable
/etc/init.d/sysntpd start
Usage Guide
Common day-to-day tasks for managing your OpenWrt-powered ER605 v2 via the command line (SSH).
Assign a Static IP (DHCP Static Lease)
Each uci add dhcp host creates a new "host" entry inside the DHCP configuration. Each host entry is a static lease — it binds a device's MAC address to a fixed IP so it always gets the same address.
Add a single static lease
# Replace the values with your device's info
uci add dhcp host
uci set dhcp.@host[-1].name='MyDevice'
uci set dhcp.@host[-1].mac='AA:BB:CC:DD:EE:FF'
uci set dhcp.@host[-1].ip='192.168.1.100'
uci commit dhcp
/etc/init.d/dnsmasq restart
Example: multiple devices
# Access Point 1
uci add dhcp host
uci set dhcp.@host[-1].name='AP-Living-Room'
uci set dhcp.@host[-1].mac='2C:3B:70:AA:11:22'
uci set dhcp.@host[-1].ip='192.168.1.2'
# Access Point 2
uci add dhcp host
uci set dhcp.@host[-1].name='AP-Office'
uci set dhcp.@host[-1].mac='2C:3B:70:BB:33:44'
uci set dhcp.@host[-1].ip='192.168.1.3'
# NAS
uci add dhcp host
uci set dhcp.@host[-1].name='NAS'
uci set dhcp.@host[-1].mac='00:11:32:CC:55:66'
uci set dhcp.@host[-1].ip='192.168.1.10'
# Apply all at once
uci commit dhcp
/etc/init.d/dnsmasq restart
List all static leases
uci show dhcp | grep host
Remove a static lease
# First find the index of the host you want to remove
uci show dhcp | grep host
# Then delete it (replace [0] with the correct index)
uci delete dhcp.@host[0]
uci commit dhcp
/etc/init.d/dnsmasq restart
View Connected Devices
Active DHCP leases
cat /tmp/dhcp.leases
ARP table (all devices seen on the network)
ip neigh
Port Forwarding
# Example: forward external port 8080 to internal 192.168.1.100:80
uci add firewall redirect
uci set firewall.@redirect[-1].name='Web-Server'
uci set firewall.@redirect[-1].src='wan'
uci set firewall.@redirect[-1].src_dport='8080'
uci set firewall.@redirect[-1].dest='lan'
uci set firewall.@redirect[-1].dest_ip='192.168.1.100'
uci set firewall.@redirect[-1].dest_port='80'
uci set firewall.@redirect[-1].proto='tcp'
uci set firewall.@redirect[-1].target='DNAT'
uci commit firewall
/etc/init.d/firewall restart
Change LAN IP / Subnet
uci set network.lan.ipaddr='192.168.10.1'
uci set network.lan.netmask='255.255.255.0'
uci commit network
/etc/init.d/network restart
System Info & Reboot
# Check uptime
uptime
# Check OpenWrt version
cat /etc/openwrt_release
# Reboot the router
reboot
Useful UCI Tips
| Command | Description |
|---|---|
uci show <config> |
Show all settings for a config (dhcp, network, firewall…) |
uci changes |
Show uncommitted changes |
uci commit |
Apply all pending changes |
uci revert <config> |
Discard uncommitted changes for a config |
logread -f |
Follow the system log in real time |
df -h |
Check disk/flash storage usage |
Sysupgrade Guide
How to upgrade OpenWrt to a newer release on your already-flashed ER605 v2. This example covers upgrading to 25.12.2 (latest stable as of April 2026).
Post-upgrade system info
Download the sysupgrade image
Download the sysupgrade binary from the official OpenWrt downloads.
cd /tmp
curl -o openwrt-25.12.2-sysupgrade.bin \
https://downloads.openwrt.org/releases/25.12.2/targets/ramips/mt7621/openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin
| File | openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin |
| Size | 6.32 MiB |
| MD5 | c67df3d3afd4358edc566dbcc5d66dc7 |
| SHA256 | 20e99c590c5824033f8d34c6acff3738aca6ec42bf159c16530ae1680a8f08c0 |
Verify the checksum
Always verify the SHA256 hash before flashing to ensure file integrity.
sha256sum openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin
Expected: 20e99c59...a8f08c0
Backup your current configuration
Using -n (or unchecking "Keep settings" in LuCI) wipes all settings. If you have a custom config (firewall rules, DNS, static leases, etc.), download a backup first.
In LuCI: System → Backup / Flash Firmware → Generate archive
Flash via LuCI (web interface)
- Go to System → Backup / Flash Firmware → Flash new firmware image
- Select
openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.binand click Upload - Verify the checksums shown match the values above (MD5 & SHA256)
- Uncheck "Keep settings and retain the current configuration"
- Click Continue and wait several minutes — do NOT power off the router
Alternative: flash via SSH
If you prefer the command line, transfer the file to the router and run sysupgrade:
# From your PC — transfer the image to the router:
scp openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin [email protected]:/tmp/
# On the router via SSH:
cd /tmp
sysupgrade -n -v openwrt-25.12.2-ramips-mt7621-tplink_er605-v2-squashfs-sysupgrade.bin
The -n flag means "do not keep settings" (clean install). -v enables verbose output.
Post-upgrade: reconfigure
After reboot, the router is at 192.168.1.1 with default settings (root, no password). You need to reconfigure everything: root password, firewall, DNS, DHCP, etc.
Disable automatic firmware upgrade check
By default, LuCI checks for firmware upgrades on login. You can disable this to avoid unnecessary external requests:
uci set attendedsysupgrade.client.login_check_for_upgrades='0'
uci commit attendedsysupgrade
This prevents the attended sysupgrade client from calling home every time you open LuCI.