1
0

firewall: use watch-outputs to track changes in zone->interface map

includes a horrible hack to work around (claimed (by me)) deficiencies
in the nftables parser
This commit is contained in:
Daniel Barlow 2025-02-28 00:43:20 +00:00
parent 929226ed9e
commit 6649ebeccd
7 changed files with 108 additions and 48 deletions

View File

@ -5,6 +5,9 @@
nftables,
writeFennel,
anoia,
svc,
lua,
output-template,
lualinux,
linotify,
}:
@ -18,14 +21,18 @@ let
inherit (lib.attrsets) mapAttrs' nameValuePair mapAttrsToList;
inherit (lib.strings) concatStringsSep;
inherit (lib.lists) flatten;
inherit (builtins) concatLists attrValues;
inherit (liminix) outputRef;
mkSet =
family: name:
nameValuePair "${name}-set-${family}" {
kind = "set";
inherit name family;
type = "ifname";
elements = map (s: "{{ output(${builtins.toJSON s}, \"ifname\", \"\") }}") zones.${name};
};
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) // (mapAttrs' (n: _: mkSet "ip6" n) zones);
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) //
(mapAttrs' (n: _: mkSet "ip6" n) zones);
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate (builtins.trace sets sets) rules);
script = firewallgen "firewall1.nft" allRules;
ifwatch = writeFennel "ifwatch" {
@ -37,13 +44,26 @@ let
mainFunction = "run";
} ./ifwatch.fnl;
watchArg = z: intfs: map (i: "${z}:${i}/.outputs") intfs;
in
longrun {
name = "firewall";
run = ''
${script}
PATH=${nftables}/bin:$PATH
${ifwatch} ${concatStringsSep " " (flatten (mapAttrsToList watchArg zones))}
'';
finish = "${nftables}/bin/nft flush ruleset";
service = longrun {
inherit name;
run = ''
mkdir -p /run/${name}; in_outputs ${name}
# exec > /dev/console 2>&1
echo RESTARTING FIREWALL >/dev/console
PATH=${nftables}/bin:${lua}/bin:$PATH
${output-template}/bin/output-template '{{' '}}' < ${script} | lua -e 'for x in io.lines() do if not string.match(x, "elements = {%s+}") then print(x) end; end' > /run/${name}/fw.nft
# cat /run/${name}/fw.nft > /dev/console
nft -f /run/${name}/fw.nft
while sleep 86400 ; do : ; done
'';
finish = "${nftables}/bin/nft flush ruleset";
};
in
svc.secrets.subscriber.build {
watch =
concatLists
(mapAttrsToList (_zone : services : map (s: outputRef s "ifname") services) zones);
inherit service;
}

View File

@ -13,12 +13,11 @@
}:
let
inherit (liminix.services) oneshot longrun;
inherit (builtins) length head toString;
inherit (lib) unique optional optionals;
inherit (builtins) map length head toString;
inherit (lib) unique optional optionals concatStringsSep;
inherit (service) name;
watched-services = unique (map (f: f "service") watch);
paths = unique (map (f: f "path") watch);
restart-flag =
{
@ -35,17 +34,11 @@ let
}
.${action};
watched-service =
if length watched-services == 0 then
null
else if length watched-services == 1 then
head watched-services
else
throw "cannot subscribe to more than one source service for secrets";
watcher =
let
name' = "restart-${name}";
refs = concatStringsSep " "
(map (s: "${s "service"}:${s "path"}") watch);
in
longrun {
name = name';
@ -55,16 +48,16 @@ let
if test -e $dir/notification-fd; then flag="-U"; else flag="-u"; fi
${s6}/bin/s6-svwait $flag /run/service/${name} || exit
PATH=${s6-rc}/bin:${s6}/bin:$PATH
${watch-outputs}/bin/watch-outputs ${restart-flag} ${name} ${watched-service.name} ${lib.concatStringsSep " " paths}
${watch-outputs}/bin/watch-outputs ${restart-flag} ${name} ${refs}
'';
};
in
service.overrideAttrs (o: {
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watched-service != null) watcher;
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watch != []) watcher;
dependencies =
(lim.orEmpty o.dependencies)
++ optionals (watched-service != null) [
watcher
watched-service
];
# ++ optionals
# (watch != [])
# ([ watcher ] ++ watched-services);
;
})

View File

@ -61,7 +61,7 @@ let
''
set ${name} {
type ${type}
${if elements != [ ] then "elements = { ${concatStringsSep ", " elements} }" else ""}
${if elements != [ ] then "elements = { ${concatStringsSep ", " (builtins.trace elements elements)} }" else ""}
}
'';

View File

@ -25,9 +25,9 @@
myenv {
: string
:output
(fn [service-path path]
(fn [service-path path default]
(let [s (assert (svc.open (.. service-path "/.outputs")))]
(s:output path)))
(or (s:output path) default)))
:lua_quote #(string.format "%q" %1)
:json_quote (fn [x] (.. "\"" (json-escape x) "\""))
}]

View File

@ -21,6 +21,7 @@ stdenv.mkDerivation {
nativeBuildInputs = [ fennelrepl ] ;
buildPhase = ''
fennelrepl --test ./watch-outputs.fnl
cp -p ${
writeFennel name {
packages = [
@ -29,6 +30,7 @@ stdenv.mkDerivation {
linotify
fennel
];
macros = [ anoia.dev ];
mainFunction = "run";
} ./watch-outputs.fnl
} ${name}

View File

@ -1,10 +1,18 @@
(local { : %% : system : assoc : split : table= : dig } (require :anoia))
(local svc (require :anoia.svc))
(local { : kill } (require :lualinux))
(local { : kill &as ll} (require :lualinux))
(import-macros { : define-tests : expect : expect= } :anoia.assert)
(fn split-paths [paths]
(icollect [_ path (ipairs paths)]
(split "/" path)))
(local { : view } (require :fennel))
(fn output-refs [outputs]
(let [result {}]
(each [_ v (ipairs outputs)]
(let [[service path] (split ":" v)
paths (or (. result service) [])]
(table.insert paths (split "/" path ))
(tset result service paths)))
result))
(fn parse-args [args]
(match args
@ -17,37 +25,73 @@
["-s" signal service & rest] (assoc (parse-args rest)
:controlled-service service
:action [:signal signal])
[watched-service & paths] { : watched-service
:paths (split-paths paths)
}))
outputs { :output-references (output-refs outputs) } ))
(define-tests
(expect= (parse-args ["-r" "daemon"
"/nix/store/s1:out1"
"/nix/store/s2:out1" "/nix/store/s2:out2/ifname"])
{:action "restart"
:controlled-service "daemon"
:output-references
{"/nix/store/s1" [["out1"]]
"/nix/store/s2" [["out1"] ["out2" "ifname"]]}}
))
(fn changed? [paths old-tree new-tree]
(accumulate [changed? false
_ path (ipairs paths)]
(or changed? (not (table= (dig old-tree path) (dig new-tree path))))))
(define-tests
(expect (changed? [["ifname"]] {:ifname "true"} {:ifindex 2}))
(expect (changed? [["ifname"]] {:ifname "true"} {:ifname "false"}))
(expect (not (changed? [["ifname"]] {:ifname "true"} {:ifname "true"})))
(expect (not (changed? [["mtu"]] {:ifname "true"} {:ifname "false"})))
(expect (not (changed? [["mtu"]] {:ifname "true"} {:ifname "false"})))
)
(fn do-action [action service]
(case action
:restart (system (%% "s6-svc -r /run/service/%s" service))
:restart-all (system (%% "s6-rc -b -d %q; s6-rc-up-tree %q" service service))
[:signal n] (system (%% "s6-svc -s %d /run/service/%s" n service))))
(local POLLIN 0x0001)
(local POLLHUP 0x0010)
(fn wait-for-change [services]
(let [pollfds (collect [s _p (ipairs services)]
(bor (lshift (s:fileno) 32)
(lshift (bor POLLIN POLLHUP) 16)))]
(ll.poll pollfds)))
(fn open-services [output-references]
(collect [s p (pairs output-references)]
(values (assert (svc.open (.. s "/.outputs"))) p)))
(fn run []
(let [{
(let [trees {}
{
: output-references
: controlled-service
: action
: watched-service
: paths } (parse-args arg)
dir (.. watched-service "/.outputs")
service (assert (svc.open dir))]
(print (%% "watching %q" watched-service))
(accumulate [tree (service:output ".")
v (service:events)]
(let [new-tree (service:output ".")]
(when (changed? paths tree new-tree)
(print "watched path event:" action controlled-service)
(do-action action controlled-service))
new-tree))))
: paths } (parse-args arg)]
(while true
(let [services (open-services output-references)
trees (collect [s _ (pairs services)]
(values s (s:output ".")))]
(wait-for-change services)
(each [service paths (pairs services)]
(let [new-tree (service:output ".")]
(when (changed? paths (. trees service) new-tree)
(print "watched path event:" action controlled-service)
(do-action action controlled-service))))))))
{ : run }

View File

@ -17,6 +17,7 @@ while { $FINISHED < 10 } {
}
set FINISHED [ expr $FINISHED + 1 ]
}
expect "#READY#"
send "nft list set ip table-ip wan || touch /non/existent\n"
expect {
"ppp0" { puts "ppp0 found " }