Skip to content

rpcd-mod-rrdns: add neighbors cache implementation#8189

Closed
Konstantin-Glukhov wants to merge 1 commit intoopenwrt:masterfrom
Konstantin-Glukhov:rrdns
Closed

rpcd-mod-rrdns: add neighbors cache implementation#8189
Konstantin-Glukhov wants to merge 1 commit intoopenwrt:masterfrom
Konstantin-Glukhov:rrdns

Conversation

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor

Add a neighbors cache and integration for the rrdns RPC module so IP lookups can fall back to local neighbor/ethers names when no DNS PTR is returned.

Files changed:

  • .gitignore: Added .vscode
  • libs/rpcd-mod-rrdns/Makefile: Updated the package version
  • libs/rpcd-mod-rrdns/src/CMakeLists.txt: Linked new neighbor.c into build.
  • libs/rpcd-mod-rrdns/src/neighbor.h: New header for neighbor cache.
    • Declares rrdns_neighbors_cache_init, rrdns_neighbors_cache_clear, and neighbor_name, plus AVL-backed structs for IP/LLADDR caches.
  • libs/rpcd-mod-rrdns/src/neighbor.c: New implementation for neighbor cache.
    • Maintains two AVL caches (ipaddr and lladdr) using libubox/avl.
    • Loads static MAC→name mappings from /etc/ethers.
    • Queries kernel neighbors via NETLINK (RTM_GETNEIGH) to map IP→LLADDR and LLADDR→name.
    • Exposes neighbor_name(int family, const void *addr) returning a cached/display name (caller-owned string).
    • Provides cache init/clear functions and logs low-memory conditions.
  • libs/rpcd-mod-rrdns/src/rrdns.c: Integration with rrdns.
    • #include "neighbor.h".
    • If no PTR records are found, call neighbor_name() as a fallback.
    • Initialize neighbor cache in rpc_rrdns_lookup() and clear it in rdns_shutdown().
    • Minor reordering: avl_find() dedupe check moved earlier in rrdns_next_query().

Behavioral impact:

  • If no DNS PTR records are returned, rrdns will attempt to resolve a human-friendly name via local neighbor information (kernel neighbor table + /etc/ethers) and return that as a fallback.

  • Adds runtime caching to avoid repeated lookups; cache cleared on shutdown or when /etc/ethers mtime changes.

  • Emits syslog warnings on memory allocation failures.

  • This PR is not from my main or master branch 💩, but a separate branch ✅

  • Each commit has a valid ✒️ Signed-off-by: <my@email.address> row (via git commit --signoff)

  • Each commit and PR title has a valid 📝 <package name>: title first line subject for packages

  • Incremented 🆙 any PKG_VERSION in the Makefile

  • Tested on: (architecture: ARMv7, OpenWrt 25.12.0-rc1, browser: Chrome) ✅

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 4, 2026

So this is changing the behavior of an existing function? If so, did you check that all the existing clients of this function want this behavior? Any downsides?

@systemcrash
Copy link
Copy Markdown
Contributor

Not touching this until all tests pass. Otherwise it looks useful.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

Konstantin-Glukhov commented Jan 5, 2026

I don't know what's wrong with the signoff test. First it did not like my github email, I fixed it, then it does not like it that it does not match the original github email. What's up with that?

Signed-off-by: Konstantin Glukhov KGlukhov@Hotmail.com

[fail] Signed-off-by is missing or doesn't match author (should be Signed-off-by: Konstantin Glukhov <24302271+Konstantin-Glukhov@users.noreply.github.com>)

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

So this is changing the behavior of an existing function? If so, did you check that all the existing clients of this function want this behavior? Any downsides?

It does not change the behavior of the existing function, it only enhances it by extending the domain of names to be returned.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

Not touching this until all tests pass. Otherwise it looks useful.

Please help me to pass the sign off test, I don't understand how to fix it.

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 5, 2026

So this is changing the behavior of an existing function? If so, did you check that all the existing clients of this function want this behavior? Any downsides?

It does not change the behavior of the existing function, it only enhances it by extending the domain of names to be returned.

That is a change in behavior... what if the original client did not want it enhanced? Normal way to do it would be either verify that this is wanted behavior for all clients or define a new function/add a parameter to make sure you do not break something.

So are there any downsides?

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

So are there any downsides?

I don't see any downside. The purpose of this module is to provide a reverse name resolution. I don't even understand your point of how not providing a name resolution in some cases is a change of behavior. If a module provides name resolutions for the cases it could not provide before, how is it a change in behavior?

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 5, 2026

I don't even understand your point of how not providing a name resolution in some cases is a change of behavior.

So if the calling program e.g. only expects dns results but now gets results from other (unverified?) sources then that clearly is a change in behavior.

Downsides could be more memory usage which on certain devices may not be a good thing, could be speed of providing the answer, it could be that the answer it provides is now outdated etc. But I don't know, I did look into it, hence I was asking...

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

I don't even understand your point of how not providing a name resolution in some cases is a change of behavior.

So if the calling program e.g. only expects dns results but now gets results from other (unverified?) sources then that clearly is a change in behavior.

Downsides could be more memory usage which on certain devices may not be a good thing, could be speed of providing the answer, it could be that the answer it provides is now outdated etc. But I don't know, I did look into it, hence I was asking...

If /etc/ethers is empty (be default) there is no impact at all. If a user has an entry in /etc/ethers there will be an attempt to resolve the name by looking into the neighbors table. Memory wise, in storage the module requires 4K more (12K vs 8K).

@systemcrash
Copy link
Copy Markdown
Contributor

I don't know what's wrong with the signoff test. First it did not like my github email, I fixed it, then it does not like it that it does not match the original github email. What's up with that?

Signed-off-by: Konstantin Glukhov KGlukhov@Hotmail.com

[fail] Signed-off-by is missing or doesn't match author (should be Signed-off-by: Konstantin Glukhov <24302271+Konstantin-Glukhov@users.noreply.github.com>)

Once you've updated your github config - you'll need to amend your commit, so the author and committer match. Then force push.

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 5, 2026

I just did a quick search seem 3 apps/mods use network.rrdns via a ubus call (as present in the json file). Two of them (including one I made), use the hosthints rpcd to lookup already. So would that then not end up with partially double functionality?
Also would it not make more sense to change rpcd-mod-luci as there is already a cache present there?

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 5, 2026

Maybe to add: if your goal is to makes the items in /etc/ethers show up in luc-mod-status then maybe its "cheaper" (in terms of resources) to adjust that?

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

Konstantin-Glukhov commented Jan 6, 2026

Maybe to add: if your goal is to makes the items in /etc/ethers show up in luc-mod-status then maybe its "cheaper" (in terms of resources) to adjust that?

Are you thinking about something like this?

-- /usr/lib/lua/neighbors.lua
-- OpenWrt Lua API endpoint to get the neighbors cache
-- Function to load data from /etc/ethers
local function load_ethers()
    local ethers = {}
    local file = io.open("/etc/ethers", "r")
    if file then
        for line in file:lines() do
            local mac, ip = line:match("(%S+)%s+(%S+)")
            if mac and ip then
                ethers[ip] = mac
            end
        end
        file:close()
    end
    return ethers
end

-- Function to load data from the kernel neighbor table
local function load_neighbors()
    local neighbors = {}
    local handle = io.popen("ip neighbor show")  -- Shell command to get neighbors
    if handle then
        for line in handle:lines() do
            local ip, mac = line:match("(%S+)%s+([%S]+)%s+.*")
            if ip and mac then
                neighbors[ip] = mac
            end
        end
        handle:close()
    end
    return neighbors
end

-- Function to merge the neighbors and ethers data
local function merge_neighbors_cache()
    local neighbors_cache = {}

    -- Load both /etc/ethers and kernel neighbors
    local ethers = load_ethers()
    local neighbors = load_neighbors()

    -- Merge the two tables
    for ip, mac in pairs(ethers) do
        neighbors_cache[ip] = mac  -- /etc/ethers data
    end
    for ip, mac in pairs(neighbors) do
        if not neighbors_cache[ip] then
            neighbors_cache[ip] = mac  -- kernel neighbors data
        end
    end

    return neighbors_cache
end

-- Return the merged cache as a JSON string
local json = require("json")
local neighbors_cache = merge_neighbors_cache()
return json.stringify(neighbors_cache)  -- Return as JSON
-- /www/cgi-bin/neighbors_api.lua
#!/usr/bin/lua
-- Include the neighbors.lua file where the logic is implemented
dofile("/usr/lib/lua/neighbors.lua")

-- Set the HTTP header for JSON response
print("Content-Type: application/json\n\n")

-- Return the merged neighbors cache as JSON
local neighbors_cache = merge_neighbors_cache()
local json = require("json")
print(json.stringify(neighbors_cache))
// modules/luci-mod-status/htdocs/luci-static/resources/view/status/connections.js
let neighbors_cache = {};  // This will hold the merged data

// Fetch the neighbors cache data from OpenWrt Lua script
fetch('http://<OpenWrt_IP>/cgi-bin/neighbors_api.lua')
    .then(response => response.json())
    .then(data => {
        neighbors_cache = data;  // Populate the neighbors_cache with the merged data
    })
    .catch(error => console.error('Error loading neighbors cache:', error));

In my opinion it is way more complicated than a single c-program approach:

Aspect Single C Program Lua + JavaScript
Components to Modify 1: The C program itself 3+: Lua (backend), JavaScript (frontend), API Communication Layer
Performance Very efficient and fast More overhead due to multiple layers (Lua/JS)
Integration Complexity Simple, direct access to system data Higher due to multiple systems involved
Data Handling Handled in a single program Requires API communication, data fetching in JS
Maintenance Low maintenance once the program is working Higher maintenance due to frontend/backend dependencies

If there is a concern about rrdns returning names not from dns lookups, how about making it optional via /etc/config/rpcd?

# Enable or disable ethers-based name resolution
config rrdns 'rrdns'
    option enable_ethers_lookup '1'  # 1 to enable, 0 to disable ethers lookup

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@Konstantin-Glukhov Konstantin-Glukhov force-pushed the rrdns branch 2 times, most recently from f20b43c to a93b41d Compare January 6, 2026 16:15
@systemcrash
Copy link
Copy Markdown
Contributor

@Alphix - thoughts on this?

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 6, 2026

In my opinion it is way more complicated than a single c-program approach:

its already implemented in a c program: rpcd-mod-luci https://github.com/openwrt/luci/blob/master/libs/rpcd-mod-luci/src/luci.c

@github-actions

This comment has been minimized.

@systemcrash
Copy link
Copy Markdown
Contributor

  • Signed-off-by is missing or doesn't match author (should be Signed-off-by: Konstantin Glukhov <24302271+Konstantin-Glukhov@users.noreply.github.com>)

You need to fix your github email config to match.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

  • Signed-off-by is missing or doesn't match author (should be Signed-off-by: Konstantin Glukhov <24302271+Konstantin-Glukhov@users.noreply.github.com>)

You need to fix your github email config to match.

Yes, this is getting funny :-) I already changed my email in GitHub Settings and in .git/config, did a couple of amends after this, and it still has no effect.

I have a theory that my anonymous email was somehow saved in the commit with my original --signof.

I am going to do one more time:

GIT_COMMITTER_DATE="$(date)" git commit --amend --signoff
git push --force

and if this fails then I am at a total loss.

@github-actions

This comment has been minimized.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

  • Signed-off-by is missing or doesn't match author (should be Signed-off-by: Konstantin Glukhov <24302271+Konstantin-Glukhov@users.noreply.github.com>)

You need to fix your github email config to match.

I think I finally figured out. It was the author of the commit that had be updated, not the email config.

@github-actions

This comment has been minimized.

Add a neighbors cache and integration for the rrdns RPC module
so IP lookups can fall back to local neighbor/ethers names when
no DNS PTR is returned.

Files changed:
- .gitignore: Added .vscode
- libs/rpcd-mod-rrdns/Makefile: Updated the package version
- libs/rpcd-mod-rrdns/src/CMakeLists.txt: Linked new neighbor.c into build.
- libs/rpcd-mod-rrdns/src/neighbor.h: New header for neighbor cache.
  - Declares rrdns_neighbors_cache_init, rrdns_neighbors_cache_clear, and
    neighbor_name, plus AVL-backed structs for IP/LLADDR caches.
- libs/rpcd-mod-rrdns/src/neighbor.c: New code for neighbor cache.
  - Maintains two AVL caches (ipaddr and lladdr) using libubox/avl.
  - Loads static MAC→name mappings from /etc/ethers.
  - Queries kernel neighbors via NETLINK (RTM_GETNEIGH) to map IP→LLADDR
    and LLADDR→name.
  - Exposes neighbor_name(int family, const void *addr) returning a
    cached/display name (caller-owned string).
  - Provides cache init/clear functions and logs low-memory conditions.
- libs/rpcd-mod-rrdns/src/rrdns.c: Integration with rrdns.
  - #include "neighbor.h".
  - If no PTR records are found, call neighbor_name() as a fallback.
  - Initialize neighbor cache in rpc_rrdns_lookup() and clear it in
    rdns_shutdown().
  - Minor reordering: avl_find() dedupe check moved earlier in
    rrdns_next_query().

Behavioral impact:
- If no DNS PTR records are returned, rrdns will attempt to resolve
  a human-friendly name via local neighbor information (kernel
  neighbor table + /etc/ethers) and return that as a fallback.
- Adds runtime caching to avoid repeated lookups; cache cleared on
  shutdown or when /etc/ethers mtime changes.
- Emits syslog warnings on memory allocation failures.

Signed-off-by: Konstantin Glukhov <KGlukhov@Hotmail.com>
@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

In my opinion it is way more complicated than a single c-program approach:

its already implemented in a c program: rpcd-mod-luci https://github.com/openwrt/luci/blob/master/libs/rpcd-mod-luci/src/luci.c

I’m not sure what is meant by “it’s already implemented.”

After reviewing libs/rpcd-mod-luci/src/luci.c, it appears that luci-rpc.getHostHints only collects and aggregates host-related metadata, but does not actually implement IP→name resolution logic as used by the UI.

Specifically, luci-rpc.getHostHints performs the following steps:

  1. Builds an AVL tree keyed by MAC address, each entry containing IPv4 and IPv6 subtrees
  2. Extends this tree with DHCP lease information
  3. Augments MAC entries with hostnames from /etc/ethers
  4. Adds addresses assigned to local network interfaces
  5. Updates MAC entries with names obtained via network.rrdns.lookup

The resulting data structure is effectively:

        / IPv4 address AVL tree
MAC → hostname
        \ IPv6 address AVL tree

At this stage, the data is still organized primarily by MAC address, not by IP address. Resolving a hostname for a given IP therefore requires iterating over the MAC AVL tree and then over the associated IPv4/IPv6 subtrees. This traversal logic does not appear to be implemented in connections.js, which is why the functionality in question is not actually present there.

Note: There may be a minor precedence issue in step 5. Reverse DNS lookups are performed even when a hostname is already defined via /etc/ethers, allowing rrdns results to override statically configured names. If this behavior is intentional, it should probably be documented; otherwise, it may make more sense for /etc/ethers entries to take precedence over rrdns-derived names.

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 7, 2026

The thing is the ethers file is used to connect a Mac to an ip (or hostname) . It is not meant to resolve host names to ip or the other way around. Dns is for doing names to ip and the other way around. If you want to add static items you do that in the hosts file or dhcp lease file, not the ethers file. So i really do not think that you want to start mixing this. But if you really want to put it in the ethers file then there is already code to scan the ether file. Please don't put similar code in multiple locations. And even worse, don't make it such that those duplicated codes run, butt alas that is exactly what will happen for 2 out of 3 use cases.

Doing a lookup is a few lines of code and if you are worried about performance then note that it is running client side, and you could even invert the gathered table and cache it. So all of this will not impact the AP or routers performance or memory usage. You then only need to modify 1 component, maintenance is easy, using existing interfaces, performance is excellent as it runs client side etc

@Ramon00
Copy link
Copy Markdown
Contributor

Ramon00 commented Jan 7, 2026

(got an error and then posted the comment twice)

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

(got an error and then posted the comment twice)

Comments can be edited.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

Konstantin-Glukhov commented Jan 7, 2026

The thing is the ethers file is used to connect a Mac to an ip (or hostname) . It is not meant to resolve host names to ip or the other way around. Dns is for doing names to ip and the other way around. If you want to add static items you do that in the hosts file or dhcp lease file, not the ethers file. So i really do not think that you want to start mixing this. But if you really want to put it in the ethers file then there is already code to scan the ether file. Please don't put similar code in multiple locations. And even worse, don't make it such that those duplicated codes run, butt alas that is exactly what will happen for 2 out of 3 use cases.

Doing a lookup is a few lines of code and if you are worried about performance then note that it is running client side, and you could even invert the gathered table and cache it. So all of this will not impact the AP or routers performance or memory usage. You then only need to modify 1 component, maintenance is easy, using existing interfaces, performance is excellent as it runs client side etc

There are no formal restrictions or written rules that limit how /etc/ethers can be used. Given that SLAAC IPv6 addresses are not registered in DNS , it is a logical option to use /etc/ethers to map node names via MAC address.

I managed to implement this mapping in connections.js - it was not that complicated. I will submit a separate pull request for connections.js and will close this one.

Thank you for your feedback and guidance.

@Konstantin-Glukhov
Copy link
Copy Markdown
Contributor Author

Extending rrdns with /etc/ethers was rejected based on conflicting functionality in luci-rpc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants