Attempting Local-DNS on D-Link DIR-825 (Anweb)
This is my personal notes and findings on attempting to configure local DNS on D-Link DIR-825 (Anweb). This article is co-authored with Google Gemini 3.6 Flash.
tl;dr, I could not configure arbitrary local DNS records on D-Link DIR-825 (Anweb). It might still be possible if I could have the root shell access. I have documented the root cause analysis, what I have tried, what I have learned, and what could be done in this article.
Specifically, this exercise answers three key questions:
- Can we configure arbitrary local DNS records (e.g.
nas.home,server.home,pi.home) directly on the DIR-825 router? - How does the router's internal management API work under the hood?
- Can we programmatically inspect, automate, and control the router without using the Web UI?
This guide outlines everything I tried across SSH, Web, and JSON-RPC APIs, what worked, what failed and why, key concepts learned, future research horizons, community workarounds, and complete runnable Python tools.
1. Target Device & System Baseline
Hardware Specifications
- Device Model: D-Link DIR-825
- Hardware Revision: J2 (Model ID:
DIR_825J2_RT8197G_WW) - Firmware Version:
1.0.5a(Build Date: Mon Oct 7 09:08:14 MSK 2024) - Vendor Platform: D-Link Russia / Anweb Framework
- SoC / Processor: Realtek RTL8197FH-VG5 (MIPS 24Kc @ 1GHz, 128MB RAM)
- Root Filesystem: Read-Only SquashFS (
/dev/root) - Runtime Storage: Writable tmpfs (
/tmp) - Default IP / Gateway:
192.168.1.1
Shell Environment (SSH admin@192.168.1.1)
- Shell: BusyBox restricted shell
- User Accounts:
root: UID0, GID0(system processes)admin: UID1009, GID0(SSH access)
- Available Commands:
grep,vi,ps,top,hexdump,cat,ls,touch,killall - Missing Utilities:
find,head,which,id,uname,gdb,curl,wget
2. Architectural Overview & Communication Flow
Traditional consumer routers use server-side CGI scripts (.cgi, .asp, .php) or direct NVRAM mutations (nvram set).
The D-Link Russia Anweb architecture decoupled the frontend into a single-page AngularJS app that talks to backend root daemons (anweb, deuteron, dmsd) using a TR-069/TR-181 Data Model over JSON-RPC 2.0.
3. Comprehensive Failure Matrix (SSH, Web UI, APIs)
Understanding why certain approaches failed is as important as documenting what worked. Below is the complete diagnostic failure matrix:
| Vector | Action Attempted | Command / Request | Observed Output | Technical Root Cause Analysis |
|---|---|---|---|---|
| SSH | Direct file write to hosts | touch /tmp/dnsmasq/hosts26.conf | touch: Permission denied | File is owned by root:root (0644). SSH user admin has UID 1009 and GID 0. POSIX write checks require UID 0 for 0644 files. Group membership (GID 0) only grants read access. |
| SSH | Reload service | killall -HUP dnsmasq | Operation not permitted | dnsmasq PIDs are owned by root (UID 0). Linux kernel signal delivery (sys_kill) checks process owner UID vs caller UID (1009). Without CAP_KILL, operation fails. |
| SSH | Query local IPC socket | dmsc -r test | unknown method | dmsc is a wrapper for /var/run/dmsd.sock. It requires formal method signatures defined in dmsd rather than arbitrary string parameters. |
| Web UI | DHCP Hostname Resolution | Static DHCP Lease (Hostname: rpi) | nslookup rpi → NXDOMAIN | D-Link's DHCP integration writes dhcp-host=MAC,IP,"rpi" to dnsmasq config, but dnsmasq is configured with no-hosts and does not automatically expose static DHCP leases into DNS. |
| API | Unregistered RPC Method | POST /cpe (method: "GetView") | Method not supported (Code 9000) | GetView is not present in the backend /cpe handler dispatch table. |
| API | Querying Virtual UI Paths | GetConfig(["View.LAN"]) | Node not found (Code 9005) | View. paths are virtual templates processed by frontend controllers, not persistent nodes in the backend Device. object model. |
| API | Unauthenticated Request | POST /cpe without Token Header | Access denied (Code 10003) | The backend enforces mandatory verification of DMSD-Access-Token HTTP headers for all RPC methods. |
4. Step-by-Step Execution Journey
Phase 1: SPA Asset & Frontend Mining
Objective
Avoid brute-forcing or guessing API endpoints by analyzing the client-side JavaScript source code.
Execution
We fetched all core JavaScript bundles from the router:
curl -s "http://192.168.1.1/concat?type=js&path=admin/global_js_list" -o global_js.js
curl -s "http://192.168.1.1/concat?type=js&path=admin/js_list" -o js_list.js
curl -s "http://192.168.1.1/concat?type=js&path=admin/lib_js_list" -o lib_js.js
curl -s "http://192.168.1.1/apps/admin/config.js" -o config.js
We wrote analyze.py to extract URL endpoints using Python regular expressions:
import re
with open('js_list.js', 'r', encoding='utf-8', errors='ignore') as f:
text = f.read()
paths = set(re.findall(r'/[a-zA-Z0-9_\-]{3,}', text))
print("Endpoints found:", [p for p in sorted(paths) if not p.startswith('/admin')])
Discovered Endpoints
/login?- Authentication handler/cpe- Primary JSON-RPC 2.0 transport endpoint/cpex- Extended transport endpoint/cookies- Initial cookie bootstrapper/devinfo- System information endpoint
Phase 2: Reversing Authentication & Token Handshake
Objective
Programmatically authenticate and acquire session credentials.
Execution
We located authRequest inside js_list.js (line 398341):
function authRequest(params) {
return $http.post("/login?", {
login: encodeURIComponent(username),
password: password,
staysigned: staySignedIn
});
}
We wrote test_login.py to execute the login handshake:
import requests
session = requests.Session()
session.get("http://192.168.1.1/cookies")
payload = {"login": "admin", "password": "ROUTER_PASSWORD_HERE", "staysigned": False}
r = session.post("http://192.168.1.1/login?", json=payload)
data = r.json()
token = data["result"]["AccessToken"]
session.headers.update({"DMSD-Access-Token": token})
Response Structure
{
"id": 1,
"result": {
"Status": 0,
"AccessTimeout": 300,
"AccessToken": "e8699f85-4356-4899-907f-4304bf3d4330",
"RefreshToken": "fda918f3-d8f3-4897-b9d5-7de98ca16905"
}
}
Phase 3: Mining JSON-RPC 2.0 Methods
Objective
Enumerate all backend API capabilities exposed over POST /cpe.
Execution
Using find_rpc_methods.py, we extracted 27 registered RPC methods:
- AddObject - GetDatamodel - Reboot
- ApplyChanges - GetParameterAttributes - SaveConfig
- ApplyDifference - GetParameterNames - Select
- DeleteObject - GetParameterValues - SetParameterValues
- Download / Upload - GetRPCMethods - FileBrowser
- FactoryReset - Logout - Execute
- GetConfig - POST / GET - read / write
Phase 4: Datamodel Schema Introspection (GetDatamodel)
Objective
Dump the formal schema specification of the entire router firmware.
Execution
We called GetDatamodel for ["Device."] using dump_complete_datamodel.py:
payload = {
"jsonrpc": "2.0",
"method": "GetDatamodel",
"params": { "ParameterNames": ["Device."] },
"id": 1
}
response = session.post("http://192.168.1.1/cpe", json=payload)
Discovered Data Model Tree
The call returned the complete 82 KB formal datamodel schema (complete_datamodel_schema.json):
{
"Device": {
"#attr": { "type": "object", "access": "readOnly", "perms": "R-W-A-D-P" },
"DeviceInfo": {
"BuildTime": {
"#attr": { "type": "string", "default": "Mon Oct 7 09:08:14 MSK 2024", "maxLength": 64, "access": "readOnly" }
}
},
"Network": {
"IP": { ... },
"Connection": { ... }
},
"Switch": { ... },
"System": { ... },
"WiFi": { ... }
}
}
Phase 5: Local DNS Feasibility Evaluation
Final Determination
By inspecting complete_datamodel_schema.json and querying all sub-branches under Device.Network., Device.Services., and Device.System.:
- Schema Omission: The formal data model for Firmware
1.0.5acontains parameters for DHCP, IP addresses, Bridges, and VLANs, but noDevice.DNS.HostsorDevice.Services.LocDNSnode exists in the schema. - Permission Boundary: The SSH user
admin(UID 1009) cannot modify/tmp/dnsmasq/hosts26.confor signaldnsmasqdirectly. - Conclusion: Local static DNS entry management is omitted from the vendor's DMS data model.
5. In-Depth Breakdown of Every Attempted Test
Test 1: SSH File Modification (touch /tmp/dnsmasq/hosts26.conf)
- Goal: Append custom host entries (e.g.
192.168.1.50 camera.home) directly todnsmasqhosts file. - Command Executed:
touch /tmp/dnsmasq/hosts26.conf 2>&1 - Raw Output:
STDOUT: touch: /tmp/dnsmasq/hosts26.conf: Permission denied - Deep Analysis:
hosts26.confhas metadata-rw-r--r-- 1 root root.adminaccount has UID1009and GID0. Linux file permission logic:- If caller UID == file owner UID (1009 == 0 → False).
- Else if caller GID == file group GID (0 == 0 → True).
- Apply Group Permissions (
r--→ Read-Only). Therefore, GID 0 grants read access, but denies write access.
Test 2: Process Signaling via SSH (killall -HUP dnsmasq)
- Goal: Force
dnsmasqto re-read configuration without rebooting. - Command Executed:
killall -HUP dnsmasq 2>&1 - Raw Output:
STDOUT: killall: can't kill pid 3045: Operation not permitted - Deep Analysis:
In Linux, sending a signal via
kill()requireseuid == ruidof target process, orCAP_KILLcapability. Sinceadmin(UID 1009) lacksCAP_KILLand does not matchdnsmasq(UID 0), the kernel aborts the system call withEPERM.
Test 3: System IPC CLI Inspection (dmsc -r test)
- Goal: Use local router binary
dmscto query internal socket/var/run/dmsd.sock. - Command Executed:
dmsc -r test - Raw Output:
STDOUT: unknown method - Deep Analysis:
dmscis an IPC wrapper expecting registered TR-069 RPC method names (GetConfig,GetParameterNames) and formal parameters. Unregistered strings fail at the IPC dispatch layer.
6. Core Technical Concepts Mastered
- TR-069 / TR-181 Data Models (CWMP): Standardized hierarchical object structures (
Device.*) used across telecom and router equipment. - JSON-RPC 2.0 Transport Protocols: Uniform request/response structures using
jsonrpc: "2.0",method,params, andid. - SPA Frontend Asset Mining: Extracting complete backend API maps directly from JavaScript bundles.
- Schema Introspection: Programmatically querying a live server for its formal datamodel definition (
GetDatamodel). - POSIX Security Models: Understanding the difference between UID-based write permissions and GID-based group permissions in Linux.
7. Future Research Horizons & Experiments
If you want to continue exploring or extending your router's hardware, here are the logical next steps:
1. Firmware Binary Extraction (binwalk)
- Download official
.binfirmware from D-Link Russia support. - Extract SquashFS rootfs:
binwalk -e firmware.bin - Reverse engineer
/bin/deuteronand/bin/anwebusing Ghidra or IDA Pro.
2. Direct UNIX Domain Socket Access (/var/run/dmsd.sock)
- Connect directly to the internal socket over SSH:
bash
socat - UNIX-CONNECT:/var/run/dmsd.sock - Intercept raw IPC packets exchanged between
anweband system daemons.
8. Reddit & Community Workaround Catalog
Here are the top community-proven workarounds from r/HomeNetworking, r/OpenWrt, and r/SelfHosted:
Option 1: Pi-hole / AdGuard Home with DHCP Option 6 (Recommended)
- Run Pi-hole or AdGuard Home on a local server or Raspberry Pi (e.g.
192.168.1.50). - Define custom local DNS records (
camera.home→192.168.1.50). - In D-Link Web UI (LAN Settings / DHCP), set Primary DNS to your Pi-hole IP (
192.168.1.50).
Option 2: mDNS / Avahi (Zeroconf for .local domains)
- Install Avahi on your server:
sudo apt install avahi-daemon - Set hostname in
/etc/hostname(e.g.camera). - Devices on the network can access
camera.localautomatically.
Option 3: Config Backup File Patching
- Export configuration backup (
.bin/.xml) from Web UI. - Decrypt header using community tools (
dlink-config-decryptor). - Inject custom DNS XML tags, recalculate checksum, and restore backup.
9. Complete Reproducible Python Suite
Below is the complete, runnable Python automation tool used during this investigation:
import requests
import json
import re
ROUTER_IP = "192.168.1.1"
USERNAME = "admin"
PASSWORD = "YOUR_ROUTER_PASSWORD"
class DLinkRouterClient:
def __init__(self, ip, username, password):
self.ip = ip
self.username = username
self.password = password
self.session = requests.Session()
self.access_token = None
def authenticate(self):
# 1. Fetch cookie bootstrapper
self.session.get(f"http://{self.ip}/cookies")
# 2. Login
payload = {"login": self.username, "password": self.password, "staysigned": False}
r = self.session.post(f"http://{self.ip}/login?", json=payload)
res = r.json()
if "result" in res and res["result"].get("Status") == 0:
self.access_token = res["result"]["AccessToken"]
self.session.headers.update({"DMSD-Access-Token": self.access_token})
print(f"[+] Successfully authenticated. Token: {self.access_token}")
return True
else:
print(f"[-] Auth failed: {res}")
return False
def call_rpc(self, method, params=None):
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params or {},
"id": 1
}
r = self.session.post(f"http://{self.ip}/cpe", json=payload)
return r.json()
def dump_formal_schema(self, output_file="complete_datamodel_schema.json"):
res = self.call_rpc("GetDatamodel", {"ParameterNames": ["Device."]})
if "result" in res and "Datamodel" in res["result"]:
with open(output_file, "w") as f:
json.dump(res["result"]["Datamodel"], f, indent=2)
print(f"[+] Saved complete formal datamodel schema to {output_file}")
else:
print(f"[-] Error fetching datamodel: {res}")
if __name__ == "__main__":
client = DLinkRouterClient(ROUTER_IP, USERNAME, PASSWORD)
if client.authenticate():
client.dump_formal_schema()
Artifacts generated during this reverse engineering session (complete_datamodel_schema.json, router_config_dump.json, and automation scripts) are saved locally in the workspace repository.
Written by Vipin Joshi
Software Engineer from Delhi, IN. Building on the web & exploring the mountains.