Skip to content

Instantly share code, notes, and snippets.

@m7mdcc
Forked from AcidSlide/nft2ipset
Last active October 19, 2025 19:04
Show Gist options
  • Select an option

  • Save m7mdcc/7dea93ac01a6d1611c86b4ab6c392265 to your computer and use it in GitHub Desktop.

Select an option

Save m7mdcc/7dea93ac01a6d1611c86b4ab6c392265 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
# nft -> ipset sync for MWAN3 (with logfile + daemon support)
set -u
SCRIPT="$(basename "$0")"
TMPDIR="/tmp"
LOGFILE="/tmp/nft2ipset.log"
DAEMON=0
# --- parse args ---
while [ $# -gt 0 ]; do
case "$1" in
--log) shift; LOGFILE="$1" ;;
--daemon) DAEMON=1 ;;
esac
shift || break
done
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >>"$LOGFILE"
logger -t "$SCRIPT" -- "$*"
}
log "==== nft2ipset started (PID $$, daemon=$DAEMON) ===="
# --- background if daemon mode ---
if [ "$DAEMON" = "1" ]; then
# if launched via procd, fork to background so it doesn't hang
(
setsid "$0" --log "$LOGFILE" </dev/null &>/dev/null &
) &
exit 0
fi
PID=$$
RAND="${RANDOM:-$(date +%s)}"
MONITORFIFO="$TMPDIR/${SCRIPT}.${PID}.${RAND}.fifo"
MONITORPIDFILE="$TMPDIR/${SCRIPT}.${PID}.${RAND}.pid"
cleanup() {
log "cleanup() called"
if [ -f "$MONITORPIDFILE" ]; then
MONPID="$(cat "$MONITORPIDFILE" 2>/dev/null || echo "")"
[ -n "$MONPID" ] && kill "$MONPID" 2>/dev/null && log "Killed monitor PID $MONPID"
fi
rm -f "$MONITORFIFO" "$MONITORPIDFILE"
log "cleanup done"
}
trap cleanup TERM INT EXIT
# --- collect mwan sets ---
SET_NAMES_REGEX="$(
uci -q export mwan3 |
awk '
$1=="option" && $2=="ipset" {
gsub(/^[ \t'\"']+|[ \t'\"']+$/, "", $3);
if($3!="") names[$3]=1
}
END{ i=0; for(n in names){ if(i++) printf("|"); printf("%s", n) } }
'
)"
if [ -z "$SET_NAMES_REGEX" ]; then
log "no mwan3 ipset definitions found -> exit"
exit 0
fi
log "Detected MWAN3 sets: $SET_NAMES_REGEX"
is_mwan_set() { echo "$1" | grep -Eq "^(${SET_NAMES_REGEX})$"; }
parse_set_family() { echo "$1" | grep -q "ipv6_addr" && echo inet6 || echo inet; }
parse_set_timeout() { echo "$1" | sed -n -E 's/.* timeout ([0-9]+)s.*/\1/p' | head -n1; }
extract_elements() {
echo "$1" | sed -E 's/.*elements = \{ ([^}]+) \}.*/\1/;t; s/.*/ /' |
tr ',' '\n' | sed -E 's/^[[:space:]]+//; s/expires/timeout/g; s/s$//g'
}
ensure_ipset() {
NAME="$1"; FAMILY="$2"; T="${3:-0}"
OPTS=""; [ "$FAMILY" = inet6 ] && OPTS="$OPTS family inet6"
[ "$T" -gt 0 ] 2>/dev/null && OPTS="$OPTS timeout $T"
if ipset -q list "$NAME" >/dev/null 2>&1; then
CUR="$(ipset list "$NAME" 2>/dev/null || true)"
if ! echo "$CUR" | grep -q "family $FAMILY"; then
ipset destroy "$NAME" 2>/dev/null
ipset create "$NAME" hash:ip $OPTS
log "recreated $NAME due to family change"
elif [ "$T" -gt 0 ] && ! echo "$CUR" | grep -q "timeout $T"; then
ipset create "_$NAME" hash:ip $OPTS &&
ipset swap "_$NAME" "$NAME" &&
ipset destroy "_$NAME"
log "rebuilt $NAME with timeout $T"
fi
else
ipset create "$NAME" hash:ip $OPTS
log "created ipset $NAME ($FAMILY) timeout=$T"
fi
}
seed_ipset_from_def() {
DEF="$1"
NAME="$(echo "$DEF" | awk '{print $2}')"
FAMILY="$(parse_set_family "$DEF")"
TIMEOUT="$(parse_set_timeout "$DEF")"; TIMEOUT="${TIMEOUT:-0}"
ensure_ipset "$NAME" "$FAMILY" "$TIMEOUT"
ELEMS="$(extract_elements "$DEF")"
if [ -n "$ELEMS" ]; then
echo "$ELEMS" | while read -r L; do
[ -n "$L" ] && ipset -q add "$NAME" $L
done
log "seeded elements into $NAME"
fi
}
# --- initial sync ---
log "starting initial sync"
nft -nT list sets 2>/dev/null |
tr '\n' ' ' | awk '{$1=$1;print}' |
sed -E 's/(set|table)/\n\1/g' |
grep -E "^set (${SET_NAMES_REGEX}) " |
while read -r DEF; do seed_ipset_from_def "$DEF"; done
log "initial sync complete"
# --- monitor setup ---
mkfifo "$MONITORFIFO"
(nft -nT monitor >"$MONITORFIFO" 2>&1) &
echo $! > "$MONITORPIDFILE"
log "spawned monitor PID $(cat "$MONITORPIDFILE")"
# --- main event loop ---
while read -r LINE; do
case "$LINE" in
*"add element inet fw4 "*)
NAME="$(echo "$LINE" | awk '{print $5}')"
is_mwan_set "$NAME" || continue
IP="$(echo "$LINE" | awk '{print $7}')"
EXPIRES="$(echo "$LINE" | sed -n -E 's/.*expires ([0-9]+)s.*/\1/p' | head -n1)"
[ -n "$EXPIRES" ] && ADDOPTS="timeout $EXPIRES" || ADDOPTS=""
ipset -q del "$NAME" "$IP"
ipset -q add "$NAME" "$IP" $ADDOPTS
log "add $IP to $NAME ($ADDOPTS)"
;;
*"add set inet fw4 "*)
NAME="$(echo "$LINE" | awk '{print $5}')"
is_mwan_set "$NAME" || continue
DEF="$(nft -nT list sets 2>/dev/null |
tr '\n' ' ' | awk '{$1=$1;print}' |
sed -E 's/(set|table)/\n\1/g' |
grep -E "^set ${NAME} ")"
[ -n "$DEF" ] && seed_ipset_from_def "$DEF"
;;
*"delete set inet fw4 "*)
NAME="$(echo "$LINE" | awk '{print $5}')"
is_mwan_set "$NAME" || continue
ipset -q clear "$NAME"
ipset -q destroy "$NAME"
log "deleted ipset $NAME"
;;
esac
done < "$MONITORFIFO"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment