From e3012776a657e7a1d84f6c77f57a7730480597ca Mon Sep 17 00:00:00 2001 From: Supremist Date: Thu, 9 Jul 2026 18:23:11 +0300 Subject: Update netbird ips in nft sets on monitor events --- api/netbird-nft-sets.nu | 46 --------------------------- api/nft-event-listener.nu | 72 ++++++++++++++++++++++++++++++++++++++++++ api/update-netbird-nftables.nu | 54 +++++++++++++++++++++++++++++++ test-data/mock-nft-mon.nu | 4 +++ test-data/nft-mon-output.json | 3 ++ 5 files changed, 133 insertions(+), 46 deletions(-) delete mode 100644 api/netbird-nft-sets.nu create mode 100644 api/nft-event-listener.nu create mode 100644 api/update-netbird-nftables.nu create mode 100644 test-data/mock-nft-mon.nu create mode 100644 test-data/nft-mon-output.json diff --git a/api/netbird-nft-sets.nu b/api/netbird-nft-sets.nu deleted file mode 100644 index 2187aa6..0000000 --- a/api/netbird-nft-sets.nu +++ /dev/null @@ -1,46 +0,0 @@ -# Usage: -# ^nft --json list sets | nu --stdin api/netbird-nft-sets.nu -# open --raw test-data/nft-sets.json | nu --stdin api/netbird-nft-sets.nu - -use netbird-api.nu - -def mangle-name [prefix: string, name: string] { - $prefix + ($name - | str replace -ar '\s+' '_' - | str replace -ar '\W' '') -} - -def find-changed-sets [] { - let sets = $in # nft_sets - | rename --column { name: nft_name } - let peers = netbird-api peers list - | insert nft_name { mangle-name 'nbp_' $in.name | str downcase } - | insert new_elem { [$in.ip] } - let groups = netbird-api groups list - | where peers != null - | each { update peers { select id | join $peers id } } - | insert nft_name { mangle-name 'nbg_' $in.name } # allow uppercase - | insert new_elem { $in.peers.ip } - - let set_changes = [$peers, $groups] | flatten - | join $sets nft_name - | where { $in.new_elem != $in.elem } - return $set_changes -} - -def generate-nft-commands [] { -# nft add talbe inet # ignore if table already exists -# nft destroy table inet # ignore if table already removed - let set_changes = $in - for $s in $set_changes { - let path = [$s.family $s.table $s.nft_name] | str join ' ' - print $"flush set ($path)" - print $"add element ($path) { ($s.new_elem | str join ', ') }" - } -} - -def main [] { - $in | from json | get nftables | skip 1 | get set - | find-changed-sets - | generate-nft-commands -} diff --git a/api/nft-event-listener.nu b/api/nft-event-listener.nu new file mode 100644 index 0000000..0554c6f --- /dev/null +++ b/api/nft-event-listener.nu @@ -0,0 +1,72 @@ +def "job recv pending" []: nothing -> list { + "marker" | job send (job id) # send a marker to self + mut events = [] + mut ev = job recv # marker will prevent wait here + while $ev != "marker" { + $events = $events | append $ev + $ev = job recv + } + $events +} + +# may wait up to two intervals, because event timestamps are not accounted +def create-debounce-queue [interval: duration, handler: closure] { + job spawn -d "debounce queue" { + mut event = job recv + loop { + sleep $interval + let new_events = job recv pending + if ($new_events | is-empty) { + do $handler $event + $event = job recv + } else { + $event = ($new_events | last) + } + } + } +} + +def single [] { + let $data = $in + if ($data | length) != 1 { + make error "Expected single element" + } + $data | first +} + +def parse-nft-mon-event [] { + mut rest = $in + mut result = {} + for key_name in ['action', 'type'] { + let key = $rest | items {|k, v| $k} | single + $rest = $rest | get $key + $result = $result | insert $key_name $key + } + $result | merge $rest +} + +def main [--test] { + let debounce_interval = 2sec + let debounce_queue = create-debounce-queue $debounce_interval {|ev| + print "Triggered" $ev + # nu update-netbird-nftables.nu --apply + } + try { + if $test { + nu test-data/mock-nft-mon.nu + } else { + ^nft --json monitor rules + } | lines + | each { from json | parse-nft-mon-event } + | where action == add and type == rule and table == netbird # TODO filter update evenets + | each { job send $debounce_queue } + | ignore + print "Exiting gracefully..." + sleep $debounce_interval + sleep $debounce_interval + } catch { |e| + print $e.rendered + } finally { + job kill $debounce_queue + } +} \ No newline at end of file diff --git a/api/update-netbird-nftables.nu b/api/update-netbird-nftables.nu new file mode 100644 index 0000000..95b9e02 --- /dev/null +++ b/api/update-netbird-nftables.nu @@ -0,0 +1,54 @@ +use netbird-api.nu + +def mangle-name [prefix: string, name: string] { + $prefix + ($name + | str replace -ar '\s+' '_' + | str replace -ar '\W' '') +} + +def find-changed-sets [] { + let sets = $in # nft_sets + | rename --column { name: nft_name } + let peers = netbird-api peers list + | insert nft_name { mangle-name 'nbp_' $in.name | str downcase } + | insert new_elem { [$in.ip] } + let groups = netbird-api groups list + | where peers != null + | each { update peers { select id | join $peers id } } + | insert nft_name { mangle-name 'nbg_' $in.name } # allow uppercase + | insert new_elem { $in.peers.ip } + + let set_changes = [$peers, $groups] | flatten + | join $sets nft_name + | where { $in.new_elem != $in.elem } + return $set_changes +} + +def generate-nft-commands [] { +# nft add talbe inet # ignore if table already exists +# nft destroy table inet # ignore if table already removed + let set_changes = $in + $set_changes | each {|s| + let path = [$s.family $s.table $s.nft_name] | str join ' ' + [ + $"flush set ($path)", + $"add element ($path) { ($s.new_elem | str join ', ') }" + ] + } | flatten +} + +def main [--test, --apply] { + let commands = if $test { + open --raw test-data/nft-sets.json + } else { + ^nft --json list sets + } | from json | get nftables | skip 1 | get set + | find-changed-sets + | generate-nft-commands + | str join "\n" + if $apply { + $commands | ^nft -f - + } else { + $commands + } +} diff --git a/test-data/mock-nft-mon.nu b/test-data/mock-nft-mon.nu new file mode 100644 index 0000000..9048dc3 --- /dev/null +++ b/test-data/mock-nft-mon.nu @@ -0,0 +1,4 @@ +const events = 'test-data/nft-mon-output.json' +open --raw $events | lines | each { sleep 1sec; print $in } | ignore +sleep 5sec +open --raw $events | lines | each { sleep 1sec; print $in } | ignore \ No newline at end of file diff --git a/test-data/nft-mon-output.json b/test-data/nft-mon-output.json new file mode 100644 index 0000000..1080082 --- /dev/null +++ b/test-data/nft-mon-output.json @@ -0,0 +1,3 @@ +{"delete": {"rule": {"family": "ip", "table": "netbird", "chain": "netbird-rt-fwd", "handle": 68, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": "100.66.181.207"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "daddr"}}, "right": "172.30.0.5"}}, {"counter": {"packets": 0, "bytes": 0}}, {"accept": null}]}}} +{"add": {"rule": {"family": "ip", "table": "netbird", "chain": "netbird-rt-fwd", "handle": 69, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": "100.66.181.207"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "daddr"}}, "right": "172.30.0.5"}}, {"counter": {"packets": 0, "bytes": 0}}, {"accept": null}]}}} +{"add": {"rule": {"family": "ip", "table": "netbird", "chain": "netbird-rt-fwd", "handle": 69, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": "100.66.181.207"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "daddr"}}, "right": "172.30.0.5"}}, {"counter": {"packets": 0, "bytes": 0}}, {"accept": null}]}}} \ No newline at end of file -- cgit v1.2.3