vocuzi
Back to Home

Attempting Local-DNS on D-Link DIR-825 (Anweb)

2026-08-033 min read
By Vipin Joshi

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:

  1. Can we configure arbitrary local DNS records (e.g. nas.home, server.home, pi.home) directly on the DIR-825 router?
  2. How does the router's internal management API work under the hood?
  3. 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: UID 0, GID 0 (system processes)
    • admin: UID 1009, GID 0 (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.

Architecture Diagram
Rendering diagram...

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:

VectorAction AttemptedCommand / RequestObserved OutputTechnical Root Cause Analysis
SSHDirect file write to hoststouch /tmp/dnsmasq/hosts26.conftouch: Permission deniedFile 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.
SSHReload servicekillall -HUP dnsmasqOperation not permitteddnsmasq 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.
SSHQuery local IPC socketdmsc -r testunknown methoddmsc is a wrapper for /var/run/dmsd.sock. It requires formal method signatures defined in dmsd rather than arbitrary string parameters.
Web UIDHCP Hostname ResolutionStatic DHCP Lease (Hostname: rpi)nslookup rpiNXDOMAIND-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.
APIUnregistered RPC MethodPOST /cpe (method: "GetView")Method not supported (Code 9000)GetView is not present in the backend /cpe handler dispatch table.
APIQuerying Virtual UI PathsGetConfig(["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.
APIUnauthenticated RequestPOST /cpe without Token HeaderAccess 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:

bash
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:

python
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):

javascript
function authRequest(params) {
  return $http.post("/login?", {
    login: encodeURIComponent(username),
    password: password,
    staysigned: staySignedIn
  });
}

We wrote test_login.py to execute the login handshake:

python
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

json
{
  "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:

text
- 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:

python
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):

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.:

  1. Schema Omission: The formal data model for Firmware 1.0.5a contains parameters for DHCP, IP addresses, Bridges, and VLANs, but no Device.DNS.Hosts or Device.Services.LocDNS node exists in the schema.
  2. Permission Boundary: The SSH user admin (UID 1009) cannot modify /tmp/dnsmasq/hosts26.conf or signal dnsmasq directly.
  3. 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 to dnsmasq hosts file.
  • Command Executed: touch /tmp/dnsmasq/hosts26.conf 2>&1
  • Raw Output: STDOUT: touch: /tmp/dnsmasq/hosts26.conf: Permission denied
  • Deep Analysis: hosts26.conf has metadata -rw-r--r-- 1 root root. admin account has UID 1009 and GID 0. Linux file permission logic:
    1. If caller UID == file owner UID (1009 == 0 → False).
    2. Else if caller GID == file group GID (0 == 0 → True).
    3. 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 dnsmasq to 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() requires euid == ruid of target process, or CAP_KILL capability. Since admin (UID 1009) lacks CAP_KILL and does not match dnsmasq (UID 0), the kernel aborts the system call with EPERM.

Test 3: System IPC CLI Inspection (dmsc -r test)

  • Goal: Use local router binary dmsc to query internal socket /var/run/dmsd.sock.
  • Command Executed: dmsc -r test
  • Raw Output: STDOUT: unknown method
  • Deep Analysis: dmsc is 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

  1. TR-069 / TR-181 Data Models (CWMP): Standardized hierarchical object structures (Device.*) used across telecom and router equipment.
  2. JSON-RPC 2.0 Transport Protocols: Uniform request/response structures using jsonrpc: "2.0", method, params, and id.
  3. SPA Frontend Asset Mining: Extracting complete backend API maps directly from JavaScript bundles.
  4. Schema Introspection: Programmatically querying a live server for its formal datamodel definition (GetDatamodel).
  5. 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:

Architecture Diagram
Rendering diagram...

1. Firmware Binary Extraction (binwalk)

  • Download official .bin firmware from D-Link Russia support.
  • Extract SquashFS rootfs: binwalk -e firmware.bin
  • Reverse engineer /bin/deuteron and /bin/anweb using 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 anweb and system daemons.

8. Reddit & Community Workaround Catalog

Here are the top community-proven workarounds from r/HomeNetworking, r/OpenWrt, and r/SelfHosted:

Architecture Diagram
Rendering diagram...

Option 1: Pi-hole / AdGuard Home with DHCP Option 6 (Recommended)

  1. Run Pi-hole or AdGuard Home on a local server or Raspberry Pi (e.g. 192.168.1.50).
  2. Define custom local DNS records (camera.home192.168.1.50).
  3. 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)

  1. Install Avahi on your server: sudo apt install avahi-daemon
  2. Set hostname in /etc/hostname (e.g. camera).
  3. Devices on the network can access camera.local automatically.

Option 3: Config Backup File Patching

  1. Export configuration backup (.bin/.xml) from Web UI.
  2. Decrypt header using community tools (dlink-config-decryptor).
  3. 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:

python
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.

Categorized under networkingView Category →

Written by Vipin Joshi

Software Engineer from Delhi, IN. Building on the web & exploring the mountains.