Skip to content

Instantly share code, notes, and snippets.

@AcidSlide
Last active October 20, 2025 16:37
Show Gist options
  • Select an option

  • Save AcidSlide/cd646a8cc81534ad4d3f48075310fe9b to your computer and use it in GitHub Desktop.

Select an option

Save AcidSlide/cd646a8cc81534ad4d3f48075310fe9b to your computer and use it in GitHub Desktop.
/etc/init.d/nft2ipset: An OPTIMIZED version for nftables set to ipset synchronizer for use with OpenWRT/mwan3
#!/bin/sh
#check if the script is already running
PID=$$
SCRIPT="$(basename $0)"
TMPDIR="/tmp"
MONITORPIDFILE="$TMPDIR/$SCRIPT-$$.nftmonitorpid"
MONITORFIFO="$TMPDIR/$SCRIPT-$$.nftmonitorfifo"
mkfifo "$MONITORFIFO"
# Extract all ipset names from MWAN3
SET_NAMES=$(uci show mwan3 | grep '\.ipset=' | awk -F '=' '{gsub(/[ "\047]/, "", $2); print $2}' | sed ':a; N; $!ba; s/\n/|/g')
cleanup () {
# Cleanup nft monitor subprocess
if [ -f "$MONITORPIDFILE" ]; then
MONITORPID="$(cat "$MONITORPIDFILE")"
if [ "$MONITORPID" -gt 1 ]; then
kill "$MONITORPID"
fi
fi
# Remove pid file and fifo
rm "$MONITORFIFO" "$MONITORPIDFILE"
}
trap cleanup TERM INT EXIT
create_or_update_ipset() {
# Determine ipset parameters
local DEF="$1"
local NAME="$(echo "$DEF" | cut -d' ' -f2)"
local OPTS=""
local FAMILY="inet"
if echo "$DEF" | grep -q "ipv6_addr"; then
FAMILY="inet6"
OPTS="$OPTS family $FAMILY"
fi
local TIMEOUT="$(echo "$DEF" | sed -r 's/.*timeout ([0-9]*)s.*/\1/; t; s/.*/0/')"
if [ -n "$TIMEOUT" -a "$TIMEOUT" -gt 0 ]; then
OPTS="$OPTS timeout $TIMEOUT"
fi
# Create or update ipset from nftables set
if [ "$(ipset list -n "$NAME")" = "$NAME" ]; then
CUR="$(ipset list -t "$NAME")"
if ! ( echo "$CUR" | grep -q "family $FAMILY"); then
( ipset destroy "$NAME" 2>&1 | logger -t "$SCRIPT" ) || logger -t "$SCRIPT" "WARNING: Could not destroy ipset with family != $FAMILY"
elif ! ( echo "$CUR" | grep -q "timeout $TIMEOUT"); then
# Swap current iteration of the ipset with a new iteration due to timeout mismatch
ipset create "_$NAME" hash:ip $OPTS
ipset swap "_$NAME" "$NAME"
ipset destroy "_$NAME"
logger -t "$SCRIPT" "Replaced ipset $NAME with new iteration with timeout $TIMEOUT"
fi
fi
if [ "$(ipset list -n "$NAME")" != "$NAME" ]; then
# Create a new ipset with options matching the nftables set
ipset create "$NAME" hash:ip $OPTS
# Restart mwan3 if this ipset is used by it, it is already running but the set name is not found in active rule output
if [ $? = 0 ] && grep -q "option ipset '$NAME'" /etc/config/mwan3 2>/dev/null && ( service | grep mwan3 | grep running ) && ( ! (mwan3 rules | grep -q "match-set $NAME" ) ); then
mwan3 restart
fi
logger -t "$SCRIPT" "Created new ipset $NAME with timeout $TIMEOUT"
fi
# Add already existing entries to the set
echo "$DEF" | sed -re 's/.*elements = \{ ([^\}]+) \}.*/\1/g; t; s/.*//g' | tr ',' '\n' | sed -re 's/^[ ]+//g;s/expires/timeout/g;s/s$//g' | while read LINE; do
if [ -n "$LINE" ]; then
ipset -q add "$NAME" $LINE && logger -t "$SCRIPT" "Added $LINE to $NAME upon ipset creation/update" || true
fi
done
}
# Check if ipsets exist for only MWAN3-related nftsets
nft -nT list sets | tr '\n' ' ' | awk '{$1=$1;print}' | sed -r 's/(set|table)/\n\1/g' | grep -E "^set ($SET_NAMES) " | while read DEF; do
create_or_update_ipset "$DEF"
done
# Start monitoring nftables but filter only relevant sets
nft -nT monitor > "$MONITORFIFO" 2>&1 &
echo $! > "$MONITORPIDFILE"
while read LINE; do
if echo "$LINE" | grep -q "add element inet fw4"; then
# Extract the set name and check if it's in SET_NAMES
NAME="$(echo "$LINE" | cut -d' ' -f 5)"
if echo "$SET_NAMES" | grep -qw "$NAME"; then
# Check if ipset exists or create otherwise
if [ "$(ipset list -n "$NAME" 2>/dev/null)" != "$NAME" ]; then
DEF="$(nft -nT list sets | tr '\n' ' ' | awk '{$1=$1;print}' | sed -r 's/(set|table)/\n\1/g' | grep "^set $NAME")"
create_or_update_ipset "$DEF"
fi
# Add element to ipset
IP="$(echo "$LINE" | cut -d' ' -f 7)"
EXPIRES="$(echo "$LINE" | sed -re 's/.*expires ([0-9]+)s.*/\1/; t; s/.*/0/')"
ADDOPTS=""
if [ "$EXPIRES" -gt 0 ]; then
ADDOPTS="timeout $EXPIRES"
fi
if ipset -q test "$NAME" "$IP"; then
ipset -q del "$NAME" "$IP"
ipset -q add "$NAME" "$IP" $ADDOPTS
else
ipset -q add "$NAME" "$IP" $ADDOPTS
logger -t "$SCRIPT" "Added $IP to ipset $NAME $ADDOPTS"
fi
fi
elif echo "$LINE" | grep -q "add set inet fw4"; then
# Extract the set name and check if it's in SET_NAMES
NAME="$(echo "$LINE" | cut -d' ' -f 5)"
if echo "$SET_NAMES" | grep -qw "$NAME"; then
DEF="$(nft -nT list sets | tr '\n' ' ' | awk '{$1=$1;print}' | sed -r 's/(set|table)/\n\1/g' | grep "^set $NAME")"
create_or_update_ipset "$DEF"
fi
elif echo "$LINE" | grep -q "delete set inet fw4"; then
# Extract the set name and check if it's in SET_NAMES
NAME="$(echo "$LINE" | cut -d' ' -f 5)"
if echo "$SET_NAMES" | grep -qw "$NAME"; then
ipset clear "$NAME"
ipset destroy "$NAME" 2>&1 | logger -t "$SCRIPT"
fi
fi
done < "$MONITORFIFO"
EOT
}
start_service() {
write_script "$SCRIPTPATH"
chmod +x "$SCRIPTPATH"
procd_open_instance
procd_set_param command "$SCRIPTPATH"
procd_set_param respawn
procd_close_instance
}
service_stopped() {
# Remove the script file
rm -f "$SCRIPTPATH"
# Kill any running nft -nT monitor processes
pkill -f "nft -nT monitor" 2>/dev/null
}
# vim: ts=2 sw=2 et
@AcidSlide
Copy link
Author

AcidSlide commented Feb 26, 2025

This is my optimized version of the original script by Kishi85, and is in here: https://gist.github.com/Kishi85/b7f379f9aa19f4878af28b8e1a8887ab

This is very helpful especially if you have other NFT Table Sets being used on your OpenWRT (for example BanIP OpenWrt package, see readme here)

Key Optimization: Pulls all IPSETs used in MWAN3 Rules and only process, monitor and sync those IPSETS with MWAN3 IPSET.
The issue I encountered with the original script was that all the NFT sets being set by BanIP (for firewall) was also being added into the MWAN3 IPSET which made my router crash and triggering a lot of OOM.

For this to work, you need a custom build dnsmasq-full with the IPSET enabled. Right now, openwrt automated builds (openwrt package repos) doesn't have the IPSET enabled (as of Feb 2025). @leleb (thank you by the way) corrected me on this and the original script by Kishi doesn't need the IPSET enabled in dnsmasq-full requirements anymore and since this is based on the original script it should work with just the regular dnsmasq. Sorry I can't test that since I do my own builds with IPSET support enabled by default.

Fixed also issue when restarting or stopping the service as the nft -nT monitor is left running. So multiple restart or stoping and starting the service creates multiple nft -nT monitor running.

To install in OpenWRT:

wget -O /etc/init.d/nft2ipset https://gist.github.com/AcidSlide/cd646a8cc81534ad4d3f48075310fe9b
chmod +x /etc/init.d/nft2ipset
/etc/init.d/nft2ipset enable
/etc/init.d/nft2ipset start
sleep 10
/etc/init.d/mwan3 restart
echo '/etc/init.d/nft2ipset' >> /etc/sysupgrade.conf

The last line makes sure that it's included in the sysupgrade backup. Just take note, when restoring backup or flashing firmware on your OpenWRT, you need to manually run /etc/init.d/nft2ipset enable and /etc/init.d/nf2ipset start one time.

@leleb
Copy link

leleb commented Feb 27, 2025

Sorry, I'm not sure I understood the instructions: is it necessary to replace dnsmasq with dnsmasq-full?

@AcidSlide
Copy link
Author

Sorry, I'm not sure I understood the instructions: is it necessary to replace dnsmasq with dnsmasq-full?

Only dnsmasq-full can have IPSETs but honestly right now I don't know where to get a dnsmasq-full that is pre-compiled with IPSET enabled. I do my own openwrt builds and on the options when building dnsmasq-full I enable "IPSET Support" which is disabled by default by OpenWRT.

See this forum post: https://forum.openwrt.org/t/dnsmasq-full-ipset-support-removed-in-23-05-and-master/150274/21?page=2

@AcidSlide
Copy link
Author

If anybody wants a pre-compiled dnsmasq-full with IPSETS, i only currently build 3 different architectures (because I only have those types of routers hahaha). And I can provide a download links for those.

  • mipsel_24kc
  • aarch64_cortex-a53
  • x86_64

@leleb
Copy link

leleb commented Feb 27, 2025

Sorry, I'm not sure I understood the instructions: is it necessary to replace dnsmasq with dnsmasq-full?

Only dnsmasq-full can have IPSETs but honestly right now I don't know where to get a dnsmasq-full that is pre-compiled with IPSET enabled. I do my own openwrt builds and on the options when building dnsmasq-full I enable "IPSET Support" which is disabled by default by OpenWRT.

See this forum post: https://forum.openwrt.org/t/dnsmasq-full-ipset-support-removed-in-23-05-and-master/150274/21?page=2

I am running mwan3 with the" workaround script in Openwrt 24.10 x86 (with default dnsmasq) and it seems to work.
Do you mean that it shouldn't ?
From what I read here
openwrt/packages#22474 (comment)
"...Removing the need to recompile dnsmasq-full with ipset and enabling mwan3 to work with dnsmasq's set functionality again..."

@AcidSlide
Copy link
Author

AcidSlide commented Feb 27, 2025

I am running mwan3 with the" workaround script in Openwrt 24.10 x86 (with default dnsmasq) and it seems to work. Do you mean that it shouldn't ? From what I read here openwrt/packages#22474 (comment) "...Removing the need to recompile dnsmasq-full with ipset and enabling mwan3 to work with dnsmasq's set functionality again..."

Oh really? Didn't know it works even without IPSETs enabled and even with just dnsmasq . Then if it works it works.

Does my updated script works for you? If it does, I'll update my comment and remove mentioning it as a requirement.

@AcidSlide
Copy link
Author

Posted latest version as of 2025-07-12

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