Heuristic to find local ipv4 and ipv6 subnets

Thank you, but as mentioned in the post, I am aware of OpenWRT tools for that but I'm not looking for a solution that only works on OpenWRT. I need a general heuristic for a typical Linux machine. Or perhaps a heuristic for a machine that serves as a router and another one for a machine that belongs to a local network.

ifstatus doesn't come built-in with most linux distributions AFAIK (at least I don't have in neither on my Debian server nor on my Linux Mint desktop), and I'd rather not have unnecessary dependencies.

I thought we were discussing OpenWrt. It comes by default.

My apologies.

1 Like

I'll quote the part of the post that addresses that:

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.

1 Like

No need to apologize, really. It would be helpful if you had an idea that works outside of OpenWRT though :slight_smile: Or if you could check whether my current sketched one-liner for finding ipv4 subnets produces the expected results in your setup.

Since that is against the forum guidelines - I don't think I should pursue further discussion.


Your post does not appear to be related to an officially released OpenWrt version, package or supported operation.

It is unlikely that you will receive useful input here.

1 Like

Rough. But again, I explicitly do not want to exclude OpenWRT, and I'm even testing my scripts on it. I just don't need something that only works on OpenWRT.

1 Like

BTW I'll trade secrets: even on json files, custom parsing is very doable, without depending on a json parsing utility. My solution is using sed, here's an example code bite:
ifstatus lan | sed -n -e /ipv6-prefix-assignment/\{:1 -e n\;/]/q\;p\;b1 -e \}
(just using ifstatus for an example input here). This sed command is formatted in a way that should (?) be compatibe even with a trimmed down version of sed like the one in OpenWRT. The "normal' GNU sed command would look like so:
ifstatus lan | sed -n '/ipv6-prefix-assignment/{:1 n;/]/q;p;b1}'

The stuff between the first couple of slashes (in this case 'ipv6-prefix-assignment') is the pattern used to find a section (sorry if using incorrect lingo) that one wants to extract, and the stuff between the second pair of forward slashes (in this case ']') is the pattern for the end of the section. The above command goes through the json input and basically simply prints everything in between (and not including) the name of the section and the closing bracket. (will only print the first section it finds though)

Probably this doesn't always make sense, but at least in the one specific case where I have to deal with json (which involves pretty big files with a few thousands of lines), I discovered that this solution performs 3x faster than jq on my old router CPU, while employing just a standard linux utility (so no extra dependencies).

So in the meanwhile, 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:
ip addr show | grep -Eo '^.*brd' | grep inet | awk '{print $2}'

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

Edit: apparently in some cases the magic word might not be included in the line but there is an alternative way to find this out, which is via the 'ip route show table' command. This is the complete thing:

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

[ edited to avoid bashism ]

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.

And it would be really helpful if a few people reading this could test the above command on either their router or their linux pc (or both if not too much trouble) and let me know if this actually works in their environment, just so I have a little bit of statistics.

Ok so having read up some more on ipv6, i think i have the solution. Simply allow incoming traffic from the ULA (unicast local address) prefix fd00::/8, and the link-local prefix fe00::/64.

A slightly more complex solution that I'm not sure if it makes sense is to detect in some way (either automatically or by asking the user) if running on a router or on a node assigned to a LAN. If it's a router then autodetect the WAN interface and then allow incoming traffic from all other interfaces, while the incoming traffic on the WAN interface gets subjected to geoip whitelist rules. If running on a node assigned to a LAN then allow all traffic from the ULA prefix and from the link-local prefix.

I'm still feeling slightly confused by ipv6 and that makes me worry that I might be missing something. Would love to hear your thoughts.

(BTW I disagree with @lleachii 's judgement that this post violates the forum guidelines in some way. The fact that I prefer to find a generic solution that works on a wider spectrum of devices doesn't make this automatically unrelated to OpenWRT. I am also intending to offer this project to the OpenWRT community when it's ready. And for that, I'm going to some trouble in order to have my project compatible. The questions asked in this post can be answered much more easily if assuming a mainstream linux distribution which is an assumption I'm not making)

1 Like

All right, so here is a little script that seems to be able to consistently find local subnets for both ipv4 and ipv6, regardless of the device it's running on. At least, it does on my 2 linux machines and on my OpenWRT router. I even made it POSIX-compliant so (hopefully) someone here will help me verify that it works in other environments as well.

#!/bin/sh
# shellcheck disable=SC2030,SC2031,SC2034

get_local_subnets() (
# attempts to find local subnets, requires family in 1st arg

	get_local_addresses() (
	# attempts to find local ip addreses, requires family in 1st arg
		family="$1"

		case "$family" in
		inet )
			local_ifaces_ipv4="$(ip -f inet route show table local | grep broadcast | grep -v ' lo ' | \
				awk '{for(i=1; i<=NF; i++) if($i~/dev/) print $(i+1)}' | sort -u)"

			local_addresses="$(
			for iface in $local_ifaces_ipv4; do ip -o -f "$family" addr show "$iface" | \
				awk -v family="$family" '{for(i=1; i<=NF; i++) if($i~/'"$family"'/) print $(i+1)}' | cut -d'/' -f1 | grep -E "${ipv4_regex}\$"; done
			)"
		;;

		inet6 )
			local_addresses="$(ip -f "$family" route show table local | \
				grep -E '^local fe80::|^local fd[0-9a-fA-F]{2}:' | cut -d' ' -f2 | grep -E "${ipv6_regex}\$")"
		;;
		esac

		# shellcheck disable=SC2015
		[ -n "$local_addresses" ] && { printf "%s\n" "$local_addresses"; return 0; }
	)


	family="$1"

	case "$family" in
		inet|inet6 ) eval "subnet_regex=\$subnet_regex_$family" ;;
		* ) echo "get_local_subnets: invalid family '$family'." >&2; return 1 ;;
	esac

	local_addresses="$(get_local_addresses "$family")"
	for address in $local_addresses; do
		# shellcheck disable=SC2154
		local_subnets="$(ip -f "$family" route show to match "$address" | cut -d ' ' -f1 | grep -E "${subnet_regex}")
$local_subnets"
	done

	local_subnets="$(printf "%s" "$local_subnets" | sort -u)"

	if [ -z "$local_subnets" ]; then
		printf "%s" ""
	else
		printf "%s" "$local_subnets"
	fi
)

ipv4_regex='^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])'
ipv6_regex='^(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}:?(\\/(1?[0-2][0-8]|[0-9][0-9]))?)'
netmask_bits_regex='/(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])$'
subnet_regex_inet="${ipv4_regex}${netmask_bits_regex}"
subnet_regex_inet6="${ipv6_regex}${netmask_bits_regex}"


for family in inet inet6; do
	localsubnets="$(get_local_subnets "$family")"
	echo "Local $family subnets:"
	if [ -n "$localsubnets" ]; then echo "$localsubnets"; else echo "None found."; fi
	echo
done

1 Like

So this is the actual solution (the best I can get on my own, at least).

This utilizes another script which I recently created - get-subnet.sh.

The previous solution was utilizing the ip route show to match command to get the subnet, however I discovered that its output doesn't always have the same mask bits as the subnet registered in OpenWRT. For example, I may have fdxx:xxxx:xxxx:10::/60 subnet configured in OpenWRT (and shows up this way in ip addr command output) but ip route show to match command would output a /64 mask bits prefix. So get-subnet.sh calculates the actually correct subnet. It's on my github if anyone's interested:

And this is the (probably) final version of the script that finds local subnets.

#!/bin/sh

# find-local-subnets.sh

export LC_ALL=C

get_local_subnets() (
# attempts to find local subnets, requires family in 1st arg

	family="$1"

	case "$family" in
		inet )
			# get local interface names. filters filters by "broadcast" because this seems to always filter out WAN interfaces
			local_ifaces_ipv4="$(ip -f inet route show table local | grep -i broadcast | grep -i -v ' lo ' | \
				awk '{for(i=1; i<=NF; i++) if($i~/^dev$/) print $(i+1)}' | sort -u)"

			# get ipv4 addresses with mask bits, corresponding to local interfaces
			# awk prints the next word after 'inet'
			# grep validates found string as ipv4 address with mask bits
			local_addresses="$(
				for iface in $local_ifaces_ipv4; do
					ip -o -f "$family" addr show "$iface" | \
					awk '{for(i=1; i<=NF; i++) if($i~/^inet$/) print $(i+1)}' | grep -E "$subnet_regex_ipv4"
				done
			)"
		;;
		inet6 )
			# get local ipv6 addresses with mask bits
			# awk prints the next word after 'inet6'
			# 1st grep filters for ULA (unique local addresses with prefix 'fdxx')
			# 2nd grep validates found string as ipv6 address with mask bits
			local_addresses="$(ip -o -f inet6 addr show | awk '{for(i=1; i<=NF; i++) if($i~/^inet6$/) print $(i+1)}' | \
				grep -E -i '^fd[0-9a-f]{0,2}:' | grep -E -i "$subnet_regex_ipv6")"
		;;
		* ) echo "get_local_subnets: invalid family '$family'." >&2; return 1 ;;
	esac

	for local_address in $local_addresses; do
		# uses external get_subnet.sh script to find the actual subnets corresponding to given mask bits
		local_subnets="$(sh get-subnet.sh "$local_address") $local_subnets"
	done

	# adds link-local subnet fe80::/10
	[ "$family" = "inet6" ] && local_subnets="$local_subnets fe80::/10"

	# removes extra whitespaces, converts to newline-delimited list and removes duplicates
	local_subnets="$(printf "%s" "$local_subnets" | awk '{$1=$1};1' | tr ' ' '\n' | sort -u )"

	[ -n "$local_subnets" ] && { printf "%s" "$local_subnets"; return 0; } || return 1
)



ipv4_regex='((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])'
ipv6_regex='([0-9a-f]{0,4}:){1,7}[0-9a-f]{0,4}:?'
maskbits_regex_ipv6='(12[0-8]|((1[0-1]|[1-9])[0-9])|[8-9])'
maskbits_regex_ipv4='(3[0-2]|([1-2][0-9])|[8-9])'
subnet_regex_ipv4="^${ipv4_regex}/${maskbits_regex_ipv4}$"
subnet_regex_ipv6="^${ipv6_regex}/${maskbits_regex_ipv6}$"

rv=0
for family in inet inet6; do
	localsubnets="$(get_local_subnets "$family")"; rv=$((rv + $?))
	echo "Local $family subnets:" >&2
	if [ -n "$localsubnets" ]; then printf "%s\n" "$localsubnets"; else printf "%s\n" "None found." >&2; fi
	echo >&2
done

exit $rv

Forgive my ignorance, but I thought it was already covered by functions in /lib/functions/network.sh and ubus calls, is it not?

1 Like

It is but as stated in the post, and discussed above, I prefer to implement a solution that does not depend on OpenWRT-specific tools because I want the script to work on other platforms as well. Once I complete the implementation of ipv6 and nftables support, I will probably offer this script to the community. If there will be significant interest, I may make an OpenWRT-specific version which will use OpenWRT-specific utilities.
(by 'script' I mean the geoip blocking script that I'm working on, and which is the reason I needed a heuristic to find local subnets)

Since you are here anyway, allow me to ask if you would kindly test the above shell code on your Linux machine or router (both would be fantastic). If you do, you'll need the get-subnet.sh script from my github (link above) as well, in the same directory. I promise, it won't eat your cat nor your router :slight_smile:
I just want a little bit of statistics to see if there is any reasonable situation where my heuristics fail.

What is the point on this mess? "The basic idea is a geoblocking project" - do what is the point od checkint adresses? You need geoblocking ip list and rules to block those ip's on firewall . On big firewall like pfsense there is pfblockerNG , and they are taking lists od addresses to block from internet github od other points.

I think I explained the point of this "mess".

I want to obviously whitelist the local network when creating a whitelist in the firewall

Isn't this sentence clear enough?

I wouldn't generally expect local connections, especially on a router device, to be going through the firewall so you wouldn't really need to whitelist them. Is the intention of this script to block all incoming IPs and then only whitelist certain (hopefully correct) geo-locations?

Thanks for your time. Thing is, I don't really know who is going to install the software and on what device. Besides, this is not intended only for a router. In some cases, people may need this on a host. If someone installs the software in whitelist mode on a host without whitelisting the local subnets, they risk locking themselves out.

It works either this way in whitelist mode, or in blacklist mode where the user decides which countries to block the traffic from, and traffic from other locations is allowed.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.