Network Mapper v1.0 instructions Almquist Shell Script written for POSIX systems like OpenWRT, Linux and WSL2/WSLg --------------------------------------- Dependencies --------------------------------------- This was written in the Almquist Shell (ASH) scripting language for OpenWRT devices, but has been tested to run on Ubuntu on WSLg (which runs Bourne Again Shell, or Bash). Further information on the Almquist Shell can be found at: https://en.wikipedia.org/wiki/Almquist_shell https://www.in-ulm.de/~mascheck/various/ash/ The graphical front-end that you see in yuor browser is rendered by the D3 Javascript Visualizer by Mike Bostock and Observable, Inc., which you can grab here: https://d3js.org/d3.v7.js or in minified form here: https://d3js.org/d3.v7.min.js Well, okay, it also relies on the fact that that your device (computer, router, whatever) has its TCP/IP stack installed, obviously. But aside from needing TCP/IP, the Almquist Shell and D3... yeah, this is all self-contained! --------------------------------------- Installation --------------------------------------- On OpenWRT, place networkmap.sh, update_all.sh, get_ipv6.sh and get_dhcp.sh in /www/cgi-bin/ and your networkmap.js, networkmap.json and networkmap.html in /www/. Keep this readme file safe. In the archive: ├── readme.txt (this very file) ├── d3.v7.js ├── d3.v7.min.js ├── networkmap.js ├── networkmap.json ├── networkmap.html └── /cgi-bin/ ├── networkmap.sh ├── update_all.sh ├── get_dhcp.sh └── get_ipv6.sh Folder Structure of involved files: user-provided/generated, with their target locations: /etc/ └── config └── dhcp /tmp/ ├── dhcp.leases └── hosts └── odhcpd Provided by this package, with their target locations: /www/ ├── d3.v7.js ├── d3.v7.min.js ├── networkmap.js ├── networkmap.json ├── networkmap.html └── /cgi-bin/ ├── networkmap.sh ├── update_all.sh ├── get_dhcp.sh └── get_ipv6.sh I've also included two tools, get_dhcp.sh and get_ipv6.sh to help with determining addresses. Unless you made it yourself, networkmap.json won't exist yet, as it has to be compiled from your network information. Don't panic if networkmap.json isn't initially compiled, either, as the section below will detail how to generate or make your own! --------------------------------------- How to run this prorgam --------------------------------------- Step 1: ensure you have a copy of your router's /tmp/dhcp.leases file at YOUR computer's /tmp/dhcp.leases You may also need: /tmp/hosts/odhcpd /tmp/ipv6list /tmp/mac_ipv4_map /tmp/mac_ipv6_map as well as the output of: ip neigh show ip -6 neigh show and for wifi clients/repeater stations and information: iw dev phy0-ap0 station dump iw dev phy1-ap0 station dump NOTE: Your router's wifi interface may be named differently depending on your hardware and software (OpenWRT version, drivers, and so on), such as wlan0, or radio0. These instrucitons also assume you have iw and wpad available instead of hostapd_cli. I'm sure hostapd_cli works just fine but I have not tested it with that, as i have wpad and iw instead. This program's shell script (networkmap.sh) should be run in /www/cgi-bin/ on OpenWRT, with chmod of +x (executable) from terminal/ssh. It can also run elsewhere on linux machines, just ensure that /etc/config/dhcp and /tmp/dhcp.leases exist. Step 2: run this program: sh +x networkmapper.sh > /www/networkmap.json Step 3: if it complains about no networkmap.json file (or generates an empty one), don't panic! just make a networkmap.json file with these contents as an example: ``` { "links": [ { "source": "YourRouter", "target": "YourRepeaterRouter1" }, { "source": "YourRepeaterRouter1", "target": "Device0OnRepeater1" }, { "source": "YourRepeaterRouter1", "target": "Device1OnRepeater1" }, { "source": "YourRouter", "target": "YourRepeaterRouter2" }, { "source": "YourRepeaterRouter2", "target": "Device0OnRepeater2" }, { "source": "YourRepeaterRouter2", "target": "Device1OnRepeater2" }, { "source": "YourRouter", "target": "YourLaptop" }, { "source": "YourRouter", "target": "YourPC" }, { "source": "YourRouter", "target": "YourTablet" }, { "source": "YourRouter", "target": "YourTV" }, { "source": "YourRouter", "target": "YourGameConsole" }, { "source": "YourRouter", "target": "YourLegacyConsole" }, { "source": "YourRouter", "target": "YourPhone" }, { "source": "YourRouter", "target": "YourNetPrinter" }, { "source": "YourRouter", "target": "YourNetCam" }, { "source": "YourRouter", "target": "YourRaspberryPi" }, { "source": "YourRouter", "target": "YourVM" }, { "source": "YourRouter", "target": "YourModem" } ], "nodes": [ { "id": "YourRouter", "ip": "192.168.1.1", "type": "router", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:55", "ipv6": "fe80:abcd:1234" }, { "id": "YourRepeaterRouter1", "ip": "192.168.1.2", "type": "router", "connection": "wifi", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:56", "ipv6": ["2001:1970::4
fe80::feed:face:dead:beef::4
fd38:i5ee:cafe::4"] }, { "id": "YourRepeaterRouter2", "ip": "192.168.1.3", "type": "switch", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:57", "ipv6": ["2001:1970::5
fe80::feed:face:dead:beef::5
fd38:i5ee:cafe::5"] }, { "id": "Device0OnRepeater1", "ip": "192.168.1.100", "type": "pc", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:58", "ipv6": ["2001:1970::6
fe80::feed:face:dead:beef::6
fd38:i5ee:cafe::6"] }, { "id": "Device1OnRepeater1", "ip": "192.168.1.101", "type": "pc", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:59", "ipv6": ["2001:1970::7
fe80::feed:face:dead:beef::7
fd38:i5ee:cafe::7"] }, { "id": "Device0OnRepeater2", "ip": "192.168.1.200", "type": "pc", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:45:55", "ipv6": ["2001:1970::8
fe80::feed:face:dead:beef::8
fd38:i5ee:cafe::8"] }, { "id": "Device1OnRepeater2", "ip": "192.168.1.201", "type": "pc", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:46:55", "ipv6": ["2001:1970::9
fe80::feed:face:dead:beef::9
fd38:i5ee:cafe::9"] }, { "id": "YourPC", "ip": "192.168.1.11", "type": "pc", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:47:55", "ipv6": ["2001:1970::3
fe80::feed:face:dead:beef::3
fd38:i5ee:cafe::3"] }, { "id": "YourTablet", "ip": "192.168.1.12", "type": "tablet", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:48:55", "ipv6": ["2001:1970::10
fe80::feed:face:dead:beef::10
fd38:i5ee:cafe::10"] }, { "id": "YourTV", "ip": "192.168.1.13", "type": "tv", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:49:55", "ipv6": ["2001:1970::11
fe80::feed:face:dead:beef::11
fd38:i5ee:cafe::11"] }, { "id": "YourGameConsole", "ip": "192.168.1.14", "type": "console", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:41:55", "ipv6": ["2001:1970::12
fe80::feed:face:dead:beef::12
fd38:i5ee:cafe::12"] }, { "id": "YourPhone", "ip": "192.168.1.15", "type": "phone", "connection": "wifi", "online": true, "lastSeen": 0, "mac": "00:11:22:33:42:55", "ipv6": ["2001:1970::13
fe80::feed:face:dead:beef::13
fd38:i5ee:cafe::13"] }, { "id": "YourNetCam", "ip": "192.168.1.16", "type": "camera", "connection": "wifi", "online": true, "lastSeen": 0, "mac": "00:11:22:33:43:55", "ipv6": ["2001:1970::14
fe80::feed:face:dead:beef::14
fd38:i5ee:cafe::14"] }, { "id": "YourNetPrinter", "ip": "192.168.1.17", "type": "printer", "connection": "wifi", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:55", "ipv6": "" }, { "id": "YourRaspberryPi", "ip": "192.168.1.18", "type": "iot", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "AA:BB:CC:DD:EE:FF", "ipv6": ["2001:1970::2
fe80::feed:face:dead:beef::2
fd38:i5ee:cafe::2"] }, { "id": "YourLaptop", "ip": "192.168.1.19", "type": "pc", "connection": "wifi", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:51", "ipv6": ["2001:1970::15
fe80::feed:face:dead:beef::15
fd38:i5ee:cafe::15"] }, { "id": "YourLegacyConsole", "ip": "192.168.1.20", "type": "console", "connection": "wifi-b", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:52", "ipv6": ["2001:1970::16
fe80::feed:face:dead:beef::16
fd38:i5ee:cafe::16"] }, { "id": "YourVM", "ip": "192.168.1.99", "type": "vm", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:53", "ipv6": ["2001:1970::17
fe80::feed:face:dead:beef::17
fd38:i5ee:cafe::17"] }, { "id": "YourModem", "ip": "192.168.100.1", "type": "modem", "connection": "ethernet", "online": true, "lastSeen": 0, "mac": "00:11:22:33:44:54", "ipv6": ["2001:1970::1
fe80::feed:face:dead:beef::1
fd38:i5ee:cafe::1"] } ] } ``` the IPv6 addresses must be seperated via
, otherwise the addresses will be on a single line. instead of this: "ipv6":["2001:1970:aaaa:bbbb:54eb:cccc:dddd:1234","fe80::1234:5678:fe08:90ab","fd38:1234:5678:0:90ab:cdef:abcd:ab12","2001:1970:dead:beef:cafe:1234:5678:abcd"] write them like THIS: "ipv6": ["2001:1970::1"
fe80::feed:face:dead:beef
fd38:i5ee:cafe::1"] NOTE: YOU really should change these values (names, IP addresses, and so on) to match whatever items are in your router's DHCP configuration. OpenWRT stores these in /etc/config/dhcp in all the cases i've seen. You can daisy-chain things along like Router -> Repeater -> Device by telling them their relationship in the links section. This version does not really implement auto-detection of MAC and IPv6 addresses yet. Step 4: Inspect networkmap.html to ensure both networkmap.js and d3.v7.js (or d3.v7.min.js) are present in the header, such as via ``` Network Map of your Local Area Network ``` If you find the dashed line animation for wifi connections too distracting, you can disable that by commenting-out the in-line CSS animation code: via after After that, you will have a visualization of your network. You can update it via runnign networkmap.sh again, to check for new DHCP entries and auto-add them to networkmap.json (or you can edit networkmap.json manually). If everything works out just fine, proceed to the final step. Step 5: Enjoy the network mapper program! Step 6: If you wish to have this script run automatically, you can make a cron task via the command: crontab -e then typing: ``` * * * * * /www/cgi-bin/update_all.sh ``` or if installed into /root/, replace /www/cgi-bin/ with /root/. That will run it every 60 seconds / 1 minute. Cron can't do sub-minute intervals (for more rapid queries, a systemd timer or service would be appropriate), but we don't need to continually ping every single device on the network, as that would introduce some congestion and slow things down slightly. --------------------------------------- Physical Network: Ethernet vs Wifi --------------------------------------- Your two primary choices (for now, anyway) are either ethernet or wifi, for connections. If you are paying attention while browsing networkmap.js, you WILL spot wifi-b. That is becuase nearly all 2.4 GHz wifi networks these days operate as 802.11n for higher bandwidth and speed. HOWEVER, some older devices like the developer's PlayStation Portable really don't like 802.11n becuase they can't understand it, and thus fall back to the older 802.11b wifi standard. For those that do manage to run an 802.11b network for legacy devices such as these, wifi-b is an option to differentiate it from regular 802.11n wifi on 2.4 GHz, or current wifi modes on 5 Ghz and beyond. --------------------------------------- Device Categories --------------------------------------- The different categories, as stated within networkmap.js are: router: Router and repeater devices console: Video game consoles with internet connectivity phone: Cell phones with network capabilities, be they Android, iOS or whatever else pc: Any type of personal computer, such as a Windows, Linux, Mac, BSD or other machine, whether desktop or laptop tablet: Tablet Devices, again be they Android, iOS or whatever else printer: Network-attached printers camera: for security camera systems, network cameras and the like iot: internet-of-things devices, like raspberry pi, arduino, and other smart or network-connected devices vm: virtual machines with network connectivity tv: smart TVs with networking (ethernet, wifi or both) capabilities streaming: streaming devices like the Roku Box or Amazon Fire Stick or Chromecast Stick switch: dumb ethernet switches that have their own browser UI, *not* the Nintendo video game console! modem: cable modems, DSL modems, 3g/4g/5g modems, and so on.. basically the thing your router plugs in to for internet. --------------------------------------- Troubleshooting --------------------------------------- Help! I have a node that's flying off to upper-left infinity! This is a known issue that happens sometimes, usually when you have a duplicate name entry (or one that's similar-enough to confuse it, like Daves-Galaxy-A21 and Dave-s-Galaxy-A21... one phone, two wifi chipsets, one each for 2.4 and 5 Ghz). Don't panic, this is an EASY fix! Just run this in your browser's console (usually by pressing [CTRL]+[SHIFT]+[I] for Microsoft Edge, Google Chrome and other Chromium-based browsers and just [Q] in Firefox and related browsers) to detect any duplicate names: // Detect duplicate node IDs const seen = new Set(); allNodes.forEach(n => { if (seen.has(n.id)) { console.warn("Duplicate node ID detected:", n.id, n); } seen.add(n.id); }); if it's due to a duplicate LINK, try this one in browser console: // Detect links pointing to missing nodes const nodeIds = new Set(allNodes.map(n => n.id)); allLinks.forEach(l => { const s = l.source.id || l.source; const t = l.target.id || l.target; if (!nodeIds.has(s)) { console.warn("Link source missing:", s, l); } if (!nodeIds.has(t)) { console.warn("Link target missing:", t, l); } }); If the node has an empty or invalid name/ID the script won't know what to do with it, as confirmed by this command in browser console: // Detect empty or invalid IDs allNodes.forEach(n => { if (!n.id || typeof n.id !== "string" || n.id.trim() === "") { console.warn("Invalid node ID:", n); } }); or, it could be a node that just has missing fields that need filling, as as determined by this browser console command: // Detect malformed nodes allNodes.forEach(n => { if (!n.ip || !n.mac) { console.warn("Node missing fields:", n); } }); To find the misbehaving node in question, run this in browser console: nodeElements.attr("stroke", d => { return (!d.id || d.id.trim() === "") ? "red" : null; }); Any further comments, questions or suggestions can be forwarded to the developer at slycooper1986@yahoo.ca