October 31, 2026
I Finally Hacked My ISP Router and Turned It Into a Mini PC — Part 2
psst… You didn’t hear this from me, but here’s the secret door 👉 <button>Trust me, bro, no virus here…<button/>

By hiruthicSha
9 min read
In Part 1, I managed to get my own Linux environment running on the ISP router and, after quite a bit of wrestling, brought its LAN port to life.
That was the turning point. I finally had a working environment to experiment with, and naturally, I started asking a rather dangerous question:
What else can I make this little box do?
As it turns out, quite a bit. Some experiments worked better than expected. Others sent me straight back to the serial console.
Let's get into it.
7. The router became my web server
Once SSH worked, I wanted to do something more interesting than repeatedly checking uname and free.
But before I go to the part where I tried to imeplemtn the per device throtling as I expected in my previous article, I wanted to take it slow and observe what it can do and cant.
I had a static portfolio website. It was mostly HTML, CSS, JavaScript, images, and PDFs.
The router had a 625 MHz CPU and roughly 98 MB of usable RAM.
Why not serve the website from it? I checked httpd but it was not found.
My existing BusyBox build didn't include the HTTP server applet. I rebuilt BusyBox with CONFIG_HTTPD=y. That added roughly 250 KB to the binary.
I uploaded it over SSH, verified the MD5, and mounted a larger tmpfs for the website:
mount -t tmpfs -o size=40m tmpfs /tmp/wwwmount -t tmpfs -o size=40m tmpfs /tmp/wwwThen I transferred the site:
tar -cf - index.html resume.html robots.txt sitemap.xml static |
ssh root@192.168.31.173 \
"cd /tmp/www && tar -xf -"tar -cf - index.html resume.html robots.txt sitemap.xml static |
ssh root@192.168.31.173 \
"cd /tmp/www && tar -xf -"The transfer took about 14 seconds. The site occupied approximately 14 MB on the router.
I started BusyBox's HTTP server:
/tmp/bin/busybox httpd \
-p 80 \
-h /tmp/www \
-c /tmp/httpd.conf/tmp/bin/busybox httpd \
-p 80 \
-h /tmp/www \
-c /tmp/httpd.confThe first browser test worked.
My portfolio was now being served by the ISP router.
The browser was connecting directly to the router's LAN IP. I checked the site from the laptop with curl. The homepage returned HTTP 200. The CSS, JavaScript, and images loaded.
Then I did a more thorough check.
I fetched all 122 files through the router and compared them against the originals.
I also tested eight parallel downloads of the largest image, which was approximately 3.1 MB.
Downloads: 8
HTTP status: 200 for all
Total time: 2.4 seconds
Aggregate throughput: approximately 10 MB/sDownloads: 8
HTTP status: 200 for all
Total time: 2.4 seconds
Aggregate throughput: approximately 10 MB/sFrom, "Not able to hack into this" to running a server on the router was a pretty surprising result.
8. A live dashboard on a 625 MHz router
Once the website was running, I started building a monitoring dashboard.
I didn't want to install Grafana or another heavyweight service. I wanted something that could run with the tools already available.
I wanted to do this since I was expecting to do per-device throttling and I believed understating performance should be a priority since that would be a problem when managing multiple devices.
The dashboard used static JSON files generated by a shell script.
Every two seconds, the script read:
/proc/uptime
/proc/loadavg
/proc/meminfo
/proc/net/dev
/proc/net/arp/proc/uptime
/proc/loadavg
/proc/meminfo
/proc/net/dev
/proc/net/arpIt also sent a one-packet ping to the gateway.
The script wrote the results to JSON files in the website's directory. The browser fetched those files every two seconds and updated the page.
The page displayed:
- Uptime and load average.
- Memory usage.
- Gateway ping latency.
- Network traffic counters.
- Devices visible in the ARP table.
- A live ping chart.
- A free-memory chart.
Obviously, I couldn't set the time on the device, so the time was showing as 5 AM even tho it was around 3 PM. Other than that, the counters were working fine, which gave me hope on achieving the per-device throttling.
9. Adding ad-blocking DNS
The next experiment was a DNS server.
The stock firmware already included dnsmasq, so I didn't need to build another DNS implementation.
I used the vendor's binary inside the stock root filesystem's chroot, and downloaded the base StevenBlack hosts list.
After deduplication, the list contained 71492 domains. I converted the entries into dnsmasq address rules:
address=/doubleclick.net/0.0.0.0
address=/googlesyndication.com/0.0.0.0address=/doubleclick.net/0.0.0.0
address=/googlesyndication.com/0.0.0.0The first queries were encouraging:
doubleclick.net
-> 0.0.0.0
googlesyndication.com
-> 0.0.0.0doubleclick.net
-> 0.0.0.0
googlesyndication.com
-> 0.0.0.0And the normal domains were still being resolved. The query log showed the difference between a blocked entry and a forwarded lookup:
config doubleclick.net is 0.0.0.0
reply google.com is 104.20.23.154config doubleclick.net is 0.0.0.0
reply google.com is 104.20.23.154The blocklist used about 10 MB of RAM. The router still had roughly 81 MB available.
I then extended the dashboard with DNS statistics:
- Total blocked queries.
- Total forwarded queries.
- Recently blocked domains.
The list was loaded once by the browser and filtered locally, rather than making the router search through it on every keystroke.
Again, everything lived in RAM. I hadn't changed the home router's DHCP or DNS configuration, so no other device was automatically using this DNS server.
As you can see, the recently blocked ones are not listing, I couldn't get it listed.
But nervertheless, it wasn't all happy road, since it started blocking a lot of requests and it was getting inconsistent. But the idea is that we are able to run whatever we can.
10. I went back to the original Wi-Fi idea
At this point, the project had become something quite different from what I originally intended.
But I hadn't forgotten the original access-point idea.
The plan was to use this router's Wi-Fi as a separate network. Internet would arrive through LAN1, and Wi-Fi clients would connect to the router's own access point.
I wanted a custom web panel that showed connected devices and allowed me to control their bandwidth.
The stock firmware already knew how to bring up the Wi-Fi hardware. I had previously configured the access point through the normal admin interface, so I didn't need to reverse-engineer the wireless driver from scratch.
I booted the stock kernel with init=/bin/sh, then started the stock startup scripts manually in the background. That gave me the complete vendor Wi-Fi stack while retaining access to the root shell.
The Wi-Fi interface came up:
wlan0
UP BROADCAST RUNNINGwlan0
UP BROADCAST RUNNINGThe UART log also showed a real WPA2 four-way handshake completing with a client.
So the access point was working.
I then separated LAN1 from the Linux bridge and configured it as the internet-facing interface. I applied the DHCP lease manually, enabled forwarding, and added NAT:
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING \
-o eth0.2 -j MASQUERADEecho 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING \
-o eth0.2 -j MASQUERADEI also fixed the DNS configuration for Wi-Fi clients.
At this point, the router could share internet from LAN1 to its Wi-Fi network.
But there was a problem.
The home network and the access-point network were not actually isolated.
I had removed LAN1 from the Linux bridge, but Wi-Fi clients were still receiving DHCP addresses from the main home router.
That shouldn't have happened if Linux bridging were responsible for all the forwarding.
It wasn't.
11. The Linux bridge was not the whole network
The router used a Realtek hardware switch fabric.
That meant traffic could be forwarded by the switch ASIC without passing through the Linux bridge in the way I expected.
I tested this by removing interfaces from the bridge and forcing the laptop to release and renew its DHCP lease.
It still received an address from the home router. I then examined the vendor's hardware VLAN and port-isolation commands.
The diag utility had a command tree for querying and changing VLAN membership. Thankfully, it had the ? syntax that shoed the next valid argument so I didnt ahve to guess.
The hardware state showed that several physical ports shared VLAN 4005, and the Wi-Fi membership was represented through separate WLAN mask fields.
I tried changing the Wi-Fi membership mask. The command appeared to succeed, but reading the value back showed that it hadn't changed.
I then found a port-isolation command that genuinely worked.
I tested it on an unused port first:
rg add port-isolation port 7 portmask 0x1rg add port-isolation port 7 portmask 0x1Reading it back confirmed the new mask.
I tried applying a mask to port 0, which was the LAN1 port carrying my SSH connection.
That was a mistake.
The SSH session went silent, and the serial console started printing repeated kernel traces involving sys_setsockopt.
I recovered through the serial console by restoring the original port mask:
rg add port-isolation port 0 portmask 0x3ffrg add port-isolation port 0 portmask 0x3ffThe router recovered, but I wasn't going to keep experimenting with unknown isolation masks on the only port I needed to access the device.
I also tried the vendor's own admin UI port-binding mechanism. It didn't fix the DHCP behaviour either.
After three different approaches, I stopped trying to force the router's internal switch to do something I didn't fully understand.
The practical alternative was to isolate the router using an external managed switch, if the switch supported VLANs or port isolation.
That was a useful lesson: changing Linux bridge membership does not necessarily change what a hardware switch is forwarding.
12. Building the device-control panel
I still wanted the connected-device panel. I combined the stock bridge's MAC table with the ARP table to identify connected devices and their IP addresses, then added per-device speed controls using Linux traffic control (tc).
The configuration looked correct. I created HTB classes, added u32 filters matching destination MAC addresses, and fixed a filter-handle formatting bug. A test class even showed the expected 1 Mbps limit.
But an actual download still reached 58 Mbps.
The culprit was Realtek's hardware fast-bridge path. Traffic was being forwarded by the switch hardware, bypassing Linux's normal networking path and its tc rules. Disabling fast-bridge didn't solve it, and the interface counters barely moved during a 5 MB download.
I then tried the switch's egress bandwidth controls and per-flow share meters. The commands accepted the settings, but I couldn't reliably bind a specific Wi-Fi client to a meter.
13. Reverse-engineering the Wi-Fi driver's bandwidth control
I found a file exposed by the wireless driver: /proc/wlan0/mib_gbwc .
It contained fields for bandwidth control, transmit and receive thresholds, and an address list.
This looked promising.
I couldn't find a matching configuration page or command in the vendor firmware, so I investigated the implementation directly.
There was no source code available. I used /proc/kallsyms to locate function names, read instruction bytes from live kernel memory through /dev/mem, and disassembled the resulting data with the MIPS cross-objdump.
The functions included:
rtl8192cd_tx_gbwc
GBWC_forward_check
rtl8192cd_proc_mib_gbwc
rtl8192cd_GBWC_timerrtl8192cd_tx_gbwc
GBWC_forward_check
rtl8192cd_proc_mib_gbwc
rtl8192cd_GBWC_timerI also found the exact MIB key names by searching strings in the live kernel memory:
gbwcmode
gbwcthrd_tx
gbwcthrd_rx
gbwcnum
gbwcaddrgbwcmode
gbwcthrd_tx
gbwcthrd_rx
gbwcnum
gbwcaddrThe disassembly revealed the mode semantics:
- Mode 1: blacklist — listed addresses are shaped.
- Mode 2: whitelist — listed addresses are exempt, everyone else is shaped.
- Modes 3 and 5: shape everyone, without checking the address list.
I also found that changing the mode wasn't enough. The interface had to be cycled for the GBWC timer to become active.
I configured the values, checked them through /proc/wlan0/mib_gbwc, and confirmed that the timer was alive.
Then I tested throughput again.
Nothing changed.
Even with mode 3, which should have shaped all traffic unconditionally, the download remained around 55–66 Mbps.
That was the important result.
The feature was almost certainly Group Bandwidth Control, not per-client unicast bandwidth control. It appeared to be intended for multicast or broadcast-group traffic rather than ordinary downloads.
I had found and configured the feature correctly. It simply wasn't designed to solve the problem I was trying to solve.
Bummer. I spent a lot of time over this after the motivation I got from deploying my site…🥲.
14. What actually works, and what doesn't
By this point, I had tested several different layers of the networking stack.
The results were fairly conclusive.
Before calling the project complete, adding the final notes to my project tracker, and putting the router back into its shell, I had one more idea.
What if I could write C code in a browser, compile it for the router's CPU, execute the resulting binary on the router itself, and send the output back to the browser?
So I built a small website to do exactly that.
The workflow was simple: write some C code, submit it, compile it for the router's MIPS-based CPU, run the resulting executable on the actual RTL9602C, and display the execution result on the webpage.
And it worked.
Was there a practical reason to build this? Not really. I could already cross-compile programs on my development machine and transfer them over SSH. Building a web interface around that process solved a problem I didn't actually have.
But watching code written in a browser turn into a native executable on an ISP router, run on hardware that was never intended to support this kind of experimentation, and return its output to the same browser was oddly satisfying.
After weeks of serial consoles, NAND dumps, MIPS instruction probes, custom root filesystems, driver disassembly, and networking experiments, this was a fitting final experiment.
Completely unnecessary? Yes.
Worth doing? Absolutely.
Now I could finally put the router back in its shell. 😅
Stay Curious. Adios 👋