Last active
December 6, 2025 15:45
-
-
Save Kishi85/b7f379f9aa19f4878af28b8e1a8887ab to your computer and use it in GitHub Desktop.
/etc/init.d/nft2ipset: An nftables set to ipset synchronizer for use with OpenWRT/mwan3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh /etc/rc.common | |
| # Start before firewall and mwan3 which are at Prio 19 | |
| START=18 | |
| APP=nft2ipset | |
| USE_PROCD=1 | |
| SCRIPTPATH="/tmp/nft2ipset" | |
| write_script() { | |
| cat > "$1" <<'EOT' | |
| #!/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" | |
| 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 QUIT ABRT | |
| 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 all currently existing nftsets or create otherwise | |
| nft -nT list sets | tr '\n' ' ' | awk '{$1=$1;print}' | sed -r 's/ (set|table)/\n\1/g' | grep "^set" | while read DEF; do | |
| NAME="$(echo "$DEF" | cut -d' ' -f2)" | |
| if [ -z "$NAME" ] || ! grep -q "option ipset '$NAME'" /etc/config/mwan3; then | |
| logger -t "$SCRIPT" "Ignored set '$NAME' as it is invalid or not used by mwan3" | |
| else | |
| create_or_update_ipset "$DEF" | |
| fi | |
| done | |
| # Monitor nftables rule changes | |
| nft -nT monitor > "$MONITORFIFO" 2>&1 & | |
| echo $! > "$MONITORPIDFILE" | |
| while read LINE; do | |
| # Update ipsets according to specified nft monitor option (this should be always one operation per line) | |
| if echo "$LINE" | grep -q "add element inet"; then | |
| NAME="$(echo "$LINE" | cut -d' ' -f 5)" | |
| if [ -z "$NAME" ] || ! grep -q "option ipset '$NAME'" /etc/config/mwan3; then | |
| logger -t "$SCRIPT" "Ignored set '$NAME' as it is invalid or not used by mwan3" | |
| else | |
| # Check if ipset exists or create otherwise | |
| if [ "$(ipset list -n $NAME)" != "$NAME" ]; then | |
| DEF="$(nft -tnT 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 | |
| # Refresh the entry by deleting it first if already existing | |
| 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"; then | |
| NAME="$(echo "$LINE" | cut -d' ' -f 5)" | |
| if [ -z "$NAME" ] || ! grep -q "option ipset '$NAME'" /etc/config/mwan3; then | |
| logger -t "$SCRIPT" "Ignored set '$NAME' as it is invalid or not used by mwan3" | |
| else | |
| # Create or update ipset | |
| 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"; then | |
| NAME="$(echo "$LINE" | cut -d' ' -f 5)" | |
| if [ -z "$NAME" ] || ! grep -q "option ipset '$NAME'" /etc/config/mwan3; then | |
| logger -t "$SCRIPT" "Ignored set '$NAME' as it is invalid or not used by mwan3" | |
| else | |
| # Clear and try to delete removed ipset (This will fail if it is in use by any iptables rule) | |
| 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_set_param pidfile /var/run/nft2ipset.pid | |
| procd_close_instance | |
| } | |
| service_stopped() { | |
| rm "$SCRIPTPATH" | |
| } | |
| # vim: ts=2 sw=2 et |
Author
Works OpenWrt 24.10.2
Replace dnsmaq to dnsmasq-full to suppor nftset
- /luci/admin/network/firewall/ipsets
- /luci/admin/network/dhcp
Necessary Family IPv4+6 (does not work only with IPv4)
- /luci/admin/network/mwan3/rule
handwritten ipset
- /etc/init.d/nft2ipset start
Test
nft list set inet fw4 filtrado

Works perfectly with the mwan3 rule
Hello.
Excuse me. Why did you use hash:ip? I created nft ipsets with an ipset-extras incuding different Geo-IP, different ASN and I guess the nft2ipset doesn't work well with it. All the IPs aren't included to the ipset, I guess. I changed hash:ip to hash:net in script, and it looks probably good, but maybe I don't know something?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

That is odd indeed and seems like something wrong with the procd parameters as the service and script should only be able to be started once. The script itself should be fine here but somehow the service starts twice? Would explain the restart behaviour as well. This could also be an odd racecondition in procd for the restart case?
Anyway, I've added an explicit pid file parameter to the procd configuration of the script so the real pid of the script gets stored somewhere for procd to reference. Maybe procd is just loosing track of the script pid it started without that for some odd reason? Then this should help, otherwise at least no further harm is done.