1
0

Compare commits

..

No commits in common. "9ab77a7d7e0af144c493e3f054e9a1194bcd04e5" and "6649ebeccd95848062ca114ec0aee7714eb62c9f" have entirely different histories.

23 changed files with 109 additions and 174 deletions

View File

@ -7109,90 +7109,3 @@ commit until I have some way to see if they're working. the pppoe test
will check both firewall zones so _should_ start to fail with the will check both firewall zones so _should_ start to fail with the
current watch-outputs (because only one service) and then pass when we current watch-outputs (because only one service) and then pass when we
put the new one in put the new one in
Fri Feb 28 01:00:03 GMT 2025
Well, it works at least well enough to pass the test. There is an awful hack
though, because nftables doesn't accept "elements = { }" as valid syntax
for a set with no elements, so we post-process the file to wipe those lines
I wonder if we could instead create the set empty and then use the "other"
nftables format to generate commands that add the elements. If it's
all in the same file (or included files) it will continue to be atomic
Other options
- is the nftables json format any better? we will have to rebuild it
with json support, may be bugger
- write lua bindings to libnftables
Fri Feb 28 23:31:06 GMT 2025
adding json would add 76 + 88k to the image, but I think it would also
mean we have to rewrite all the default rules in json format
with json
[dan@loaclhost:~/src/liminix]$ du result/
20 result/share/doc/nftables/examples
24 result/share/doc/nftables
28 result/share/doc
12 result/share/man/man3
16 result/share/man/man5
44 result/share/man/man8
76 result/share/man
60 result/share/nftables
168 result/share
36 result/etc/nftables/osf
40 result/etc/nftables
44 result/etc
76 result/bin
8 result/include/nftables
12 result/include
8 result/lib/pkgconfig
1172 result/lib
1476 result/
[dan@loaclhost:~/src/liminix]$ du /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/
84 /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/lib
88 /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/
without:
[dan@loaclhost:~/src/liminix]$ du result/
20 result/share/doc/nftables/examples
24 result/share/doc/nftables
28 result/share/doc
12 result/share/man/man3
16 result/share/man/man5
44 result/share/man/man8
76 result/share/man
60 result/share/nftables
168 result/share
36 result/etc/nftables/osf
40 result/etc/nftables
44 result/etc
76 result/bin
8 result/include/nftables
12 result/include
8 result/lib/pkgconfig
1096 result/lib
1400 result/
Sat Mar 1 23:43:17 GMT 2025
I don't think json is going to help because either we'd have to do
elements = map (f: "{{ lookup(f, \"ifname\") }}") zones.${zone}
and there would be null elements in the places for the interfaces that don't exist
yet, or we'd have to write actual json syntax at runtime, at which point why don't
we write the trad nftables syntax instead?
let's write a firewall .nftables file consisting of the zone set
elements plus an "include" directive for the rest of the firewall. NOTE THAT
we may still need to template the rest of the firewall if we want to have
other variables (rate limits) in it, because the rules for that need to be
inserted ahead of the rules for accepting icmp, and there's no way to
do that without

View File

@ -21,7 +21,7 @@ let
inherit (lib.attrsets) mapAttrs' nameValuePair mapAttrsToList; inherit (lib.attrsets) mapAttrs' nameValuePair mapAttrsToList;
inherit (lib.strings) concatStringsSep; inherit (lib.strings) concatStringsSep;
inherit (lib.lists) flatten; inherit (lib.lists) flatten;
inherit (builtins) concatLists toJSON attrValues; inherit (builtins) concatLists attrValues;
inherit (liminix) outputRef; inherit (liminix) outputRef;
mkSet = mkSet =
family: name: family: name:
@ -29,50 +29,38 @@ let
kind = "set"; kind = "set";
inherit name family; inherit name family;
type = "ifname"; type = "ifname";
extraText = '' elements = map (s: "{{ output(${builtins.toJSON s}, \"ifname\", \"\") }}") zones.${name};
{{; };
local services = { ${concatStringsSep ", " (map toJSON zones.${name})} }
local ifnames = {}
for _, v in ipairs(services) do
local o = output(v, "ifname")
if o then table.insert(ifnames, o) end
end
if (#ifnames > 0) then
return "elements = { " .. table.concat(ifnames, ", ") .. " }\n"
else
return ""
end
}}
'';
};
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) // sets = (mapAttrs' (n: _: mkSet "ip" n) zones) //
(mapAttrs' (n: _: mkSet "ip6" n) zones); (mapAttrs' (n: _: mkSet "ip6" n) zones);
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate sets rules); allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate (builtins.trace sets sets) rules);
script = firewallgen "firewall1.nft" allRules; script = firewallgen "firewall1.nft" allRules;
ifwatch = writeFennel "ifwatch" {
packages = [
anoia
lualinux
linotify
];
mainFunction = "run";
} ./ifwatch.fnl;
watchArg = z: intfs: map (i: "${z}:${i}/.outputs") intfs; watchArg = z: intfs: map (i: "${z}:${i}/.outputs") intfs;
name = "firewall"; name = "firewall";
service = longrun { service = longrun {
inherit name; inherit name;
run = '' run = ''
PATH=${nftables}/bin:${lua}/bin:$PATH
reload() {
echo reloading firewall
${output-template}/bin/output-template '{{' '}}' < ${script} > /run/${name}/fw.nft;
nft -f /run/${name}/fw.nft ;
}
trap reload SIGUSR1
mkdir -p /run/${name}; in_outputs ${name} mkdir -p /run/${name}; in_outputs ${name}
reload # exec > /dev/console 2>&1
while :; do echo RESTARTING FIREWALL >/dev/console
# signals sent to ash won't interrupt sleep, but will interrupt wait PATH=${nftables}/bin:${lua}/bin:$PATH
sleep 86400 & wait ${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
done # cat /run/${name}/fw.nft > /dev/console
nft -f /run/${name}/fw.nft
while sleep 86400 ; do : ; done
''; '';
finish = "${nftables}/bin/nft flush ruleset"; finish = "${nftables}/bin/nft flush ruleset";
}; };
in in
svc.secrets.subscriber.build { svc.secrets.subscriber.build {
action = "usr1";
watch = watch =
concatLists concatLists
(mapAttrsToList (_zone : services : map (s: outputRef s "ifname") services) zones); (mapAttrsToList (_zone : services : map (s: outputRef s "ifname") services) zones);

View File

@ -28,9 +28,9 @@ let
"quit" = "-s 3"; "quit" = "-s 3";
"kill" = "-s 9"; "kill" = "-s 9";
"term" = "-s 15"; "term" = "-s 15";
"winch" = "-s 20"; "winch" = "-s 28";
"usr1" = "-s usr1"; "usr1" = "-s 10";
"usr2" = "-s usr2"; "usr2" = "-s 12";
} }
.${action}; .${action};
@ -56,5 +56,8 @@ service.overrideAttrs (o: {
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watch != []) watcher; buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watch != []) watcher;
dependencies = dependencies =
(lim.orEmpty o.dependencies) (lim.orEmpty o.dependencies)
++ optional (watch != []) watcher; # ++ optionals
# (watch != [])
# ([ watcher ] ++ watched-services);
;
}) })

View File

@ -10,7 +10,6 @@ let
splitString splitString
hasInfix hasInfix
substring substring
optionalString
; ;
inherit (lib.lists) groupBy; inherit (lib.lists) groupBy;
inherit (lib.attrsets) mapAttrsToList; inherit (lib.attrsets) mapAttrsToList;
@ -57,14 +56,12 @@ let
name, name,
type, type,
elements ? [ ], elements ? [ ],
extraText ? null,
... ...
}: }:
'' ''
set ${name} { set ${name} {
type ${type} type ${type}
${if elements != [ ] then "elements = { ${concatStringsSep ", " (builtins.trace elements elements)} }" else ""} ${if elements != [ ] then "elements = { ${concatStringsSep ", " (builtins.trace elements elements)} }" else ""}
${optionalString (extraText != null) extraText}
} }
''; '';

View File

@ -0,0 +1,3 @@
check:
./output-template '{{' '}}' < example.ini > output
diff -u output example.ini.expected

View File

@ -2,7 +2,6 @@
fetchurl, fetchurl,
writeFennel, writeFennel,
fennel, fennel,
fennelrepl,
runCommand, runCommand,
lua, lua,
anoia, anoia,
@ -18,9 +17,9 @@ stdenv.mkDerivation {
src = ./.; src = ./.;
buildInputs = [ lua ]; buildInputs = [ lua ];
nativeBuildInputs = [ fennelrepl ] ; doCheck = true;
buildPhase = '' buildPhase = ''
fennelrepl --test ./output-template.fnl
cp -p ${ cp -p ${
writeFennel name { writeFennel name {
packages = [ packages = [
@ -28,11 +27,11 @@ stdenv.mkDerivation {
lualinux lualinux
linotify linotify
]; ];
macros = [ anoia.dev ];
mainFunction = "run"; mainFunction = "run";
} ./output-template.fnl } ./output-template.fnl
} ${name} } ${name}
''; '';
checkPhase = "make check";
installPhase = '' installPhase = ''
install -D ${name} $out/bin/${name} install -D ${name} $out/bin/${name}
''; '';

View File

@ -24,43 +24,21 @@
(let [delim (.. opening "(.-)" closing) (let [delim (.. opening "(.-)" closing)
myenv { myenv {
: string : string
: table
: ipairs
:output :output
(fn [service-path path default] (fn [service-path path default]
(let [s (assert (svc.open (.. service-path "/.outputs")))] (let [s (assert (svc.open (.. service-path "/.outputs")))]
(or (s:output path) default))) (or (s:output path) default)))
:lua_quote #(string.format "%q" $1) :lua_quote #(string.format "%q" %1)
:json_quote (fn [x] (.. "\"" (json-escape x) "\"")) :json_quote (fn [x] (.. "\"" (json-escape x) "\""))
}] }]
(string.gsub text delim (string.gsub text delim
(fn [x] (fn [x]
(let [chunk (if (= (x:sub 1 1) ";") (assert ((load (.. "return " x) x :t myenv))
(x:sub 2) (string.format "missing value for %q" x))))))
(.. "return " x))]
(assert ((load chunk x :t myenv))
(string.format "missing value for %q" x)))))))
(fn run [] (fn run []
(let [[opening closing] arg (let [[opening closing] arg
out (substitute (: (io.input) :read "*a") opening closing)] out (substitute (: (io.input) :read "*a") opening closing)]
(io.write out))) (io.write out)))
(import-macros { : define-tests : expect : expect= } :anoia.assert)
(define-tests
(expect= (pick-values 1 (substitute "var={{ 2 + 3 }}" "{{" "}}")) "var=5")
(expect= (pick-values 1 (substitute "{{ json_quote(\"o'reilly\") }}" "{{" "}}"))
"\"o'reilly\"")
(expect= (pick-values 1 (substitute "{{; local a=9; return a }}" "{{" "}}")) "9")
;; "globals" set in one interpolation are available in subsequent ones
(expect= (pick-values 1 (substitute "{{; a=42; return a }} {{ a and 999 or 0 }}" "{{" "}}")) "42 999")
(fn slurp [name]
(with-open [f (assert (io.open name))] (f:read "*a")))
(expect=
(pick-values 1 (substitute (slurp "example.ini") "{{" "}}"))
(slurp "example.ini.expected")))
{ : run } { : run }

View File

@ -0,0 +1,3 @@
check:
fennelrepl ./test.fnl '{{' '}}' < example.ini > output
diff -u output example.ini.expected

View File

@ -34,6 +34,7 @@ stdenv.mkDerivation {
mainFunction = "run"; mainFunction = "run";
} ./watch-outputs.fnl } ./watch-outputs.fnl
} ${name} } ${name}
make check
''; '';
installPhase = '' installPhase = ''

View File

@ -0,0 +1 @@
a11

View File

@ -0,0 +1 @@
a33

View File

@ -0,0 +1 @@
a55

View File

@ -0,0 +1 @@
a66

View File

@ -0,0 +1 @@
000000

View File

@ -0,0 +1 @@
0000ff

View File

@ -0,0 +1 @@
00ff00

View File

@ -0,0 +1 @@
ff0000

View File

@ -0,0 +1 @@
eth1

View File

@ -0,0 +1,3 @@
wpa_passphrase={{ output("./example-service","colours/black") }}
think = {{ string.format("%q", output("./example-service","colours/blue")) }}
argonaut = {{ json_quote "hello\ngoodbye\tnext\027" }}

View File

@ -0,0 +1,3 @@
wpa_passphrase=000000
think = "0000ff"
argonaut = "hello\ngoodbye\tnext\u001B"

View File

@ -0,0 +1,44 @@
(local svc (require :anoia.svc))
(fn json-escape [s]
;; All Unicode characters may be placed within the quotation marks,
;; except for the characters that MUST be escaped:
;; quotation mark, reverse solidus, and the control characters (U+0000
;; through U+001F). (RFC 8259)
(-> s
(string.gsub
"[\"\b\f\n\r\t]" {
"\b" "\\b"
"\"" "\\\""
"\f" "\\f"
"\n" "\\n"
"\r" "\\r"
"\t" "\\t"
})
(string.gsub
"([\x00-\x1b])"
(fn [x] (string.format "\\u%04X" (string.byte x))))))
(fn substitute [text opening closing]
(let [delim (.. opening "(.-)" closing)
myenv {
: string
:output
(fn [service-path path]
(let [s (assert (svc.open (.. service-path "/.outputs")))]
(s:output path)))
:lua_quote #(string.format "%q" $1)
:json_quote (fn [x] (.. "\"" (json-escape x) "\""))
}]
(string.gsub text delim
(fn [x]
(assert ((load (.. "return " x) x :t myenv))
(string.format "missing value for %q" x))))))
(fn run []
(let [[opening closing] arg
out (substitute (: (io.input) :read "*a") opening closing)]
(io.write out)))
{ : run }

View File

@ -58,7 +58,7 @@
(case action (case action
:restart (system (%% "s6-svc -r /run/service/%s" service)) :restart (system (%% "s6-svc -r /run/service/%s" service))
:restart-all (system (%% "s6-rc -b -d %q; s6-rc-up-tree %q" service service)) :restart-all (system (%% "s6-rc -b -d %q; s6-rc-up-tree %q" service service))
[:signal n] (system (%% "s6-svc -s %q /run/service/%s" n service)))) [:signal n] (system (%% "s6-svc -s %d /run/service/%s" n service))))
(local POLLIN 0x0001) (local POLLIN 0x0001)
(local POLLHUP 0x0010) (local POLLHUP 0x0010)
@ -71,7 +71,7 @@
(fn open-services [output-references] (fn open-services [output-references]
(collect [s p (pairs output-references)] (collect [s p (pairs output-references)]
(values (svc.open (.. s "/.outputs")) p))) (values (assert (svc.open (.. s "/.outputs"))) p)))
(fn run [] (fn run []
(let [trees {} (let [trees {}
@ -89,6 +89,7 @@
(each [service paths (pairs services)] (each [service paths (pairs services)]
(let [new-tree (service:output ".")] (let [new-tree (service:output ".")]
(when (changed? paths (. trees service) new-tree) (when (changed? paths (. trees service) new-tree)
(print "watched path event:" action controlled-service)
(do-action action controlled-service)))))))) (do-action action controlled-service))))))))

View File

@ -8,39 +8,29 @@ send "PS1=\$(echo 'I1JFQURZIyA=' | base64 -d); stty -echo\n"
expect "#READY#" expect "#READY#"
set FINISHED 0 set FINISHED 0
set EXIT "1" set EXIT "1"
while { $FINISHED < 20 } { while { $FINISHED < 10 } {
send "ip address show dev ppp0 | grep ppp0\n" send "ip address show dev ppp0 | grep ppp0\n"
expect { expect {
"192.168.100.1" { set FINISHED 200; set EXIT 0; } "192.168.100.1" { set FINISHED 20; set EXIT 0; }
"can't find device" { send_user "waiting ..." ; sleep 3 } "can't find device" { send_user "waiting ..." ; sleep 3 }
"DOWN" { send_user "waiting ..." ; sleep 3 } "DOWN" { send_user "waiting ..." ; sleep 3 }
} }
set FINISHED [ expr $FINISHED + 1 ] set FINISHED [ expr $FINISHED + 1 ]
} }
expect "#READY#" expect "#READY#"
send "s6-svwait -U /run/service/wan.link.pppoe\n" send "nft list set ip table-ip wan || touch /non/existent\n"
expect {
"ppp0" { puts "ppp0 found " }
"{ }" { puts "missing ifname"; exit 1 }
"No such file or directory" { exit 1 }
}
expect "#READY#" expect "#READY#"
set timeout 30
send "nft list set ip table-ip lan || touch /non/existent\n" send "nft list set ip table-ip lan || touch /non/existent\n"
expect { expect {
"lan" { puts "lan found" } "lan" { puts "lan found" }
"{ }" { puts "missing ifname"; exit 1 } "{ }" { puts "missing ifname"; exit 1 }
"No such file or directory" { exit 1 } "No such file or directory" { exit 1 }
} }
expect "#READY#" expect "#READY#"
# if the test fails for no apparent reason, it is quite likely because
# the firewall hasn't had time to reload after the new interface
# appears and you just have to make this sleep longer. Ew, yes
send "sleep 10; nft list set ip table-ip wan || touch /non/existent\n"
expect {
"ppp0" { puts "ppp0 found " }
"{ }" { puts "missing ifname"; exit 1 }
"No such file or directory" { exit 1 }
timeout { exit 1 }
}
expect "#READY#"
exit $EXIT exit $EXIT