Heuristic to find local ipv4 and ipv6 subnets

Hi all,

I'm trying to figure out a more-or-less reliable way to get a local and (for a router, if possible) public ip subnets from the CLI, to be used in a script.

The basic idea is a geoblocking project, so I want to obviously whitelist the local network when creating a whitelist in the firewall. Since the script may end up being used in a completely different environment than mine, I'm looking for a way for it to handle most common situations.

I don't really need the detection to be 100% reliable, as I'll probably ask the user to verify that the data gathered is correct (unless I can come up with some sort of a certain solution).

I am aware of the built-in utilities in OpenWRT (in particular the network.sh script) but I don't want to use them because the script will likely be used in other environments as well (or maybe even exclusively, since the idea of implementing a simple and dedicated geoblocking solution didn't get much positive feedback here so far). I'm asking here 'cause I think you guys know a lot about networking, and in parcticular, networking in a trimmed-down environment which I don't want to exclude.

Probably it makes sense to have a separate detection mechanism for something like a router and another one for a machine on a local network.

I don't mind writing a script that will cross-reference data gathered from several utilities. In fact, I'm looking into a way to find the public ip addresses just because I think that this might be useful in cross-referencing. I'm really only interested in the local network, so that might be one way to narrow down the list.

The scripts are written in Bash (4.0+) and assume Linux kernel, awk, some version of GNU'ish grep with ERE support, and probably GNU sed (non-trimmed-down version) are present in the system.

I've come up with some stuff like this so far:

for a local machine: find local network ipv4 subnets:

while read -r interface; do ip addr show "$interface" | grep -v 'scope link' | grep 'inet ' | awk '{print $2}' done < <(ip -4 route | grep -v -e 'unreachable' -e ' lo ' | grep '^default' | \ awk '{for(i=1; i<=NF; i++) if($i~/dev/) print $(i+1)}') | sort -u
[Edit: an apparently better solution at the end of the post]

^ the same command gives me a public ipv4 address on my router (but I'm not sure whether it's good enough for the general case)

With ipv6, I'm much less knowledgable as I've never really used ipv6 and just starting to get to understand the way it works. But this is what I've come up with so far:

while read -r interface; do
    ip addr show "$interface" | grep -v 'scope link' | grep 'inet6';
done < <(ip -6 route | grep -v -e "unreachable" -e ' lo ' | grep "^default" | \
awk '{for(i=1; i<=NF; i++) if($i~/dev/) print $(i+1)}')

This bascially outputs a few lines containing all ipv6 addresses associated with the interface that's marked as 'default'. From here, I haven't really figured out what to do next to filter out the relevant subnets. One confusing part is that there are multiple ipv6 subnets for the same interface.

A second issue is that on a router (at least on my router) the interface that's marked as 'default' is the WAN interface in fact. So that command doesn't really produce the local subnet, to the best of my understanding. If you know of a way to get the local subnet on a router, that would be really helpful.

Any ideas will be appreciated.

P.s. the above one-liners are just stubs that I've come up with quickly and probably there should be a way to shorten them, please don't get picky about it.

P.p.s. I really prefer not to rely on external (i.e. web-based) services for anything. Whatever can't be gathered locally, will just have to stay unknown. Edit: I'm Ok with something like traceroute google.com if that helps.

Edit: I've come up with a heuristic that seems to work regardless of whether the machine is a router or not. This is the command:

interfaces="$(ip -4 route show table local | grep broadcast | grep -v ' lo ' | \
awk '{for(i=1; i<=NF; i++) if($i~/dev/) print $(i+1)}')";
for interface in $interfaces; do \
ip addr show "$interface" | awk '/inet / {print $2}'; \
done | sort -u

The magic word here is 'broadcast'. Apparently, since broadcast makes no sense on a WAN, this category gets only applied to LAN devices.

Unfortunately, this heuristic only works for ipv4 since there is no broadcast category for ipv6. There, I'm still stuck. I guess, I could find the LAN interface using the above heuristic and then check its ipv6 addresses. This comes with some assumptions though that I'd like to avoid if possible. Would love to hear if anyone has an idea.

I'd really appreciate if some people reading this could test the command and tell me whether it outputs their LAN ip subnet on their Linux PC or on their router, or both if not too much trouble. (I swear, the shell command contains no viruses :slight_smile:)

well, without external service, you will never know is this local/public/nat-ed,cgnat-ed, etc ... address

I could make an exception for something like 'traceroute google.com' if that helps. What I don't want to use is an external service like findmyip dot com or similar.

which is again, unreliable
many network admins simply disable traceroute / tracepath UDP ports to hide their internal routing nodes and protect network

I want to stress the point that what I'm after is not the public ip address but rather the local network subnet. But if you think that an external service might help in any way with that, I'm curious to hear how.

ok, then i miss read title of topic

Heuristic to find local and public ipv4 and ipv6 subnets

You are right, the title may be a little misleading. I'm editing it rn. The reason that I'm also interested in the public ip address is that I'm thinking about cross-referencing the gathered data, and if I see that the found address is a public one then it can be excluded.

I just discovered the JSON output mode of ip, that makes parsing easier (but requries more lines of code and requires an external utility to parse it in bash - OpenWrt has jshn.sh for this purpose):

ip --json addr show and ip --json route show.

Based on the default gateway, you could rule out public IP addresses and CG-NAT IP addresses.

openwrt 'ip' command doesn't understand the --json option... So this kinda narrows down the scope. Plus, I'm not sure what benefits it gives over a custom parsing solution like the examples in the post. I'm comfortable with awk and grep, so custom parsing is really not a problem for me.

As to CG-NAT IP addresses, I'm not really familiar with that term. I'll read up on that, but in the meanwhile - can you offer a practical way to get the relevant data from CLI?

BTW, to anyone replying, I'd be grateful if you could check whether the 1st command (for finding local ipv4 subnet) does produce the expected result on your Linux machine. And if testing on the router then does it produce your actual public ipv4 address?Just for the purpose of gathering a little bit of statistics on that.

It's working fine on my 23.05 router.

Carrier-Grade NAT. If the ISP follows RFC 1918, it's easy to detect.

$ ip addr show "$(ip -4 route | grep -v -e "unreachable" -e ' lo ' | grep "^default" | awk -F 'dev' 'NF>1{ sub(/^ */,"",$NF); sub(/ .*/,"",$NF); print $NF }')" | grep -v 'scope link' | grep 'inet ' | awk '{print $2}'
Device "enp12s0u2u1u2
wlp0s20f3" does not exist.

The problem seems to be that ip doesn't support being given two interfaces. This is on a notebook with WiFi and Ethernet connected!

1 Like

Ah yes, my router has a an older version of OpenWRT from 2020 and i can't really upgrade for now (for some technical reasons). So that might explain the difference.

Thank you for testing. And yes, it makes sense of course. I suppose the script will need to handle such conditions.

What about this one?

while read -r interface; do ip addr show "$interface" | grep -v 'scope link' | grep 'inet ' | awk '{print $2}'; done < <(ip -4 route | grep -v -e 'unreachable' -e ' lo ' | grep '^default' | awk -F 'dev' 'NF>1{sub(/^ */,"",$NF); sub (/ .*/,"",$NF); print $NF}')

Should produce an ipv4 subnet for each of your 'default' interfaces.

Yes:

192.168.17.123/24
192.168.17.231/24

It's the same subnet, so you will have to handle that as well.

1 Like

Thank you again for confirming. Having 2 interfaces on the same subnet is not really a problem. In fact, I will probably end up whitelisting by interface, not by ip. So I suppose the main challenge will be to identify the correct interfaces.
Edit: that only makes sense for a router of course.

Also, if it's not too much to ask, does that same command produce your public ipv4 if ran on your router? No need to post the ip here, of course.

You can also obtain from the ipv4-address and IPv6 sections of:

ifstatus lan

Yes, both public IP addresses are printed (it's an mwan3 setup with two ISPs).

1 Like

Well-known IP ranges and can be whitelisted preemptively:
https://en.wikipedia.org/wiki/Reserved_IP_addresses

You can also use built-in functions to get the local subnets:

ipset_local() {
local IPSET_NAME="local${IPV%4}"
local IPSET_FAMILY="ipv${IPV}"
local IPSET_FILE="/var/ipset-${IPSET_NAME}"
uci -q batch << EOI
set firewall.'${IPSET_NAME}'='ipset'
set firewall.'${IPSET_NAME}'.name='${IPSET_NAME}'
set firewall.'${IPSET_NAME}'.family='${IPSET_FAMILY}'
set firewall.'${IPSET_NAME}'.match='net'
set firewall.'${IPSET_NAME}'.loadfile='${IPSET_FILE}'
EOI
rm -f "${IPSET_FILE}"
config_foreach iface_proc interface
}

iface_proc() {
local NET_CONF="${1}"
network_flush_cache
eval network_get_subnets"${IPV%4}" NET_SUB "${NET_CONF}"
if [ -n "${NET_SUB}" ]
then cat << EOI >> "${IPSET_FILE}"
${NET_SUB// /$'\n'}
EOI
fi
}

. /lib/functions.sh
. /lib/functions/network.sh
config_load network
for IPV in 4 6
do ipset_local
done
uci commit firewall
service firewall reload