Compare commits
21 Commits
7e2b0068e6
...
9ab77a7d7e
Author | SHA1 | Date | |
---|---|---|---|
9ab77a7d7e | |||
c6918fec00 | |||
d4e46dbe28 | |||
d1f87a56e0 | |||
8c39b47cae | |||
2c7a16d792 | |||
d6b06abb63 | |||
6b32aa569e | |||
234d1bd87e | |||
c38f180fb7 | |||
9a8b22997c | |||
c32d09bd83 | |||
6649ebeccd | |||
929226ed9e | |||
a98f026210 | |||
f4dc001b71 | |||
024c018262 | |||
e1293e3778 | |||
0c406058e9 | |||
19d441333c | |||
a726c09ae4 |
176
THOUGHTS.txt
176
THOUGHTS.txt
@ -7020,3 +7020,179 @@ Sun Feb 9 21:33:57 GMT 2025
|
|||||||
nft update set @lan
|
nft update set @lan
|
||||||
|
|
||||||
echo 'flush set table-ip lan; add element table-ip lan { eth0,lo }' | nft -f -
|
echo 'flush set table-ip lan; add element table-ip lan { eth0,lo }' | nft -f -
|
||||||
|
|
||||||
|
Tue Feb 11 18:30:09 GMT 2025
|
||||||
|
|
||||||
|
outstanding for 1.0:
|
||||||
|
|
||||||
|
1) security audit fedback
|
||||||
|
|
||||||
|
a) ask ROS if I can ship their report, with a response doc
|
||||||
|
showing the commits that address each finding/non-finding
|
||||||
|
b) firewall rules: icmp rate limit, DNS, doc for icmpv6 packet dropping
|
||||||
|
c) look over env var inputs and parse them properly instead of
|
||||||
|
string glommeration
|
||||||
|
|
||||||
|
2) docs:
|
||||||
|
- for each device, add "finishedness" status and link to build status
|
||||||
|
- generally read them over and spruce up
|
||||||
|
- porting guide
|
||||||
|
|
||||||
|
3) some kconfig magic to generate minimal kconfig files so that
|
||||||
|
device modules don't end up as copy-pastes of the openwrt defconfig
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apparently 5% of available bandwidth is a reasonable rate limit for
|
||||||
|
icmp
|
||||||
|
|
||||||
|
% nft add rule filter input limit rate over 10 mbytes/second drop
|
||||||
|
|
||||||
|
but nftables has no way to get interface bandwidth and indeed nor does
|
||||||
|
the device generally: the 1000Mb/s ethernet interface might be
|
||||||
|
connected to a 70Mb/s pppoe upstream and how would it know? So the
|
||||||
|
site operator needs to say somewhere what the upstream bandwidth is.
|
||||||
|
|
||||||
|
Sun Feb 16 22:16:29 GMT 2025
|
||||||
|
|
||||||
|
we probably didn't need to write that service, we could have used the
|
||||||
|
thing that makes templated config files _and_ if we somehow contrive
|
||||||
|
to write the interface bandwidth as an interface output we could get
|
||||||
|
that the same way
|
||||||
|
|
||||||
|
if only I could remember how it worked :-)
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
* watch-output watches only _one_ service and is called with a list of
|
||||||
|
outputs inside that service, so not exactly what we need. we can
|
||||||
|
extend it easily enough to watch multiple services using poll() if we
|
||||||
|
can figure out the syntax we want. Luckily all the places that call it
|
||||||
|
go through modules/secrets/subscriber.nix so it's easy enough to change
|
||||||
|
existing uses
|
||||||
|
|
||||||
|
we could do
|
||||||
|
watch-outputs -r foo /nix/store/blah/.outputs/ifname /nix/store/eee/.outputs/ifname ...
|
||||||
|
|
||||||
|
or
|
||||||
|
watch-outputs -r foo /nix/store/blah:ifname /nix/store/eee:ifname /nix/store/eee:bandwidth
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
watch-outputs -r foo /nix/store/blah:ifname /nix/store/eee:ifname:bandwidth
|
||||||
|
|
||||||
|
which I quite like insofar as it's shorter but has no other real merit
|
||||||
|
|
||||||
|
then we need to decide how to represent an output reference in a firewall rule.
|
||||||
|
Since each rule is basically text already, might just put the handlebars straight in
|
||||||
|
|
||||||
|
let qq = builtins.toJSON ;
|
||||||
|
in "icmp6 limit rate over {{ tonumber(output(${qq (intf "service")}, ${qq (intf "bandwidth")})) / 20 }} bytes/second drop"
|
||||||
|
|
||||||
|
probably we should do a separate rule for each interface in the wan zone
|
||||||
|
|
||||||
|
Sun Feb 23 00:34:34 GMT 2025
|
||||||
|
|
||||||
|
looks like we have no tests for anything involving watched services or subscribers,
|
||||||
|
or if we do I can't see what
|
||||||
|
|
||||||
|
Thu Feb 27 20:47:03 GMT 2025
|
||||||
|
|
||||||
|
- use output-template to write firewall rule file
|
||||||
|
- wrap firewall in svc.secrets.subscriber.build (c.f. e745991) with zones as
|
||||||
|
watched services
|
||||||
|
- put the handlebars in the firewall config
|
||||||
|
|
||||||
|
we have uncommitted changes to watch-outputs that I'm relunctant to
|
||||||
|
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
|
||||||
|
current watch-outputs (because only one service) and then pass when we
|
||||||
|
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
|
||||||
|
@ -18,10 +18,16 @@ in
|
|||||||
(drop "ip6 saddr 2001:db8::/32") # documentation addresses
|
(drop "ip6 saddr 2001:db8::/32") # documentation addresses
|
||||||
(drop "ip6 daddr 2001:db8::/32")
|
(drop "ip6 daddr 2001:db8::/32")
|
||||||
|
|
||||||
# I think this means "check FIB for (saddr, iif) to see if we
|
# Reverse path filtering: drop packet if it's not coming from
|
||||||
# could route a packet to that address using that interface",
|
# the same interface that we'd use to send a reply. Works by
|
||||||
# and if we can't then it was an inapproppriate source address
|
# doing a lookup in the FIB to find how we'd route a packet _to_
|
||||||
# for packets received _from_ said interface
|
# saddr through iif, and then checking the output interface
|
||||||
|
# returned by the lookup. if oif is 0, that means no route was
|
||||||
|
# found for that address with that interface, so the packet can
|
||||||
|
# be dropped
|
||||||
|
#
|
||||||
|
# https://wiki.nftables.org/wiki-nftables/index.php/Matching_routing_information#fib
|
||||||
|
# https://thr3ads.net/netfilter-buglog/2018/01/2843000-Bug-1220-New-Reverse-path-filtering-using-fib-needs-better-documentation
|
||||||
(drop "fib saddr . iif oif eq 0")
|
(drop "fib saddr . iif oif eq 0")
|
||||||
|
|
||||||
(drop "icmpv6 type router-renumbering")
|
(drop "icmpv6 type router-renumbering")
|
||||||
@ -49,8 +55,6 @@ in
|
|||||||
(drop "ip6 daddr ::FFFF:0:0/96")
|
(drop "ip6 daddr ::FFFF:0:0/96")
|
||||||
(drop "ip6 saddr fe80::/10") # link-local unicast
|
(drop "ip6 saddr fe80::/10") # link-local unicast
|
||||||
(drop "ip6 daddr fe80::/10")
|
(drop "ip6 daddr fe80::/10")
|
||||||
(drop "ip6 saddr fc00::/7") # unique-local addresses
|
|
||||||
(drop "ip6 daddr fc00::/7")
|
|
||||||
(drop "ip6 saddr 2001:10::/28") # ORCHID [RFC4843].
|
(drop "ip6 saddr 2001:10::/28") # ORCHID [RFC4843].
|
||||||
(drop "ip6 daddr 2001:10::/28")
|
(drop "ip6 daddr 2001:10::/28")
|
||||||
|
|
||||||
@ -196,9 +200,7 @@ in
|
|||||||
type = "filter";
|
type = "filter";
|
||||||
family = "ip";
|
family = "ip";
|
||||||
|
|
||||||
rules = [
|
rules = [];
|
||||||
(accept "udp sport 53")
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
input-ip4 = {
|
input-ip4 = {
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
nftables,
|
nftables,
|
||||||
writeFennel,
|
writeFennel,
|
||||||
anoia,
|
anoia,
|
||||||
|
svc,
|
||||||
|
lua,
|
||||||
|
output-template,
|
||||||
lualinux,
|
lualinux,
|
||||||
linotify,
|
linotify,
|
||||||
}:
|
}:
|
||||||
@ -18,32 +21,61 @@ 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 (liminix) outputRef;
|
||||||
mkSet =
|
mkSet =
|
||||||
family: name:
|
family: name:
|
||||||
nameValuePair "${name}-set-${family}" {
|
nameValuePair "${name}-set-${family}" {
|
||||||
kind = "set";
|
kind = "set";
|
||||||
inherit name family;
|
inherit name family;
|
||||||
type = "ifname";
|
type = "ifname";
|
||||||
};
|
extraText = ''
|
||||||
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) // (mapAttrs' (n: _: mkSet "ip6" n) zones);
|
{{;
|
||||||
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate (builtins.trace sets sets) rules);
|
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) //
|
||||||
|
(mapAttrs' (n: _: mkSet "ip6" n) zones);
|
||||||
|
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate 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;
|
||||||
in
|
|
||||||
longrun {
|
|
||||||
name = "firewall";
|
name = "firewall";
|
||||||
run = ''
|
service = longrun {
|
||||||
${script}
|
inherit name;
|
||||||
PATH=${nftables}/bin:$PATH
|
run = ''
|
||||||
${ifwatch} ${concatStringsSep " " (flatten (mapAttrsToList watchArg zones))}
|
PATH=${nftables}/bin:${lua}/bin:$PATH
|
||||||
'';
|
reload() {
|
||||||
finish = "${nftables}/bin/nft flush ruleset";
|
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}
|
||||||
|
reload
|
||||||
|
while :; do
|
||||||
|
# signals sent to ash won't interrupt sleep, but will interrupt wait
|
||||||
|
sleep 86400 & wait
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
finish = "${nftables}/bin/nft flush ruleset";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
svc.secrets.subscriber.build {
|
||||||
|
action = "usr1";
|
||||||
|
watch =
|
||||||
|
concatLists
|
||||||
|
(mapAttrsToList (_zone : services : map (s: outputRef s "ifname") services) zones);
|
||||||
|
|
||||||
|
inherit service;
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,11 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (liminix.services) oneshot longrun;
|
inherit (liminix.services) oneshot longrun;
|
||||||
inherit (builtins) length head toString;
|
inherit (builtins) map length head toString;
|
||||||
inherit (lib) unique optional optionals;
|
inherit (lib) unique optional optionals concatStringsSep;
|
||||||
inherit (service) name;
|
inherit (service) name;
|
||||||
|
|
||||||
watched-services = unique (map (f: f "service") watch);
|
watched-services = unique (map (f: f "service") watch);
|
||||||
paths = unique (map (f: f "path") watch);
|
|
||||||
|
|
||||||
restart-flag =
|
restart-flag =
|
||||||
{
|
{
|
||||||
@ -29,23 +28,17 @@ let
|
|||||||
"quit" = "-s 3";
|
"quit" = "-s 3";
|
||||||
"kill" = "-s 9";
|
"kill" = "-s 9";
|
||||||
"term" = "-s 15";
|
"term" = "-s 15";
|
||||||
"winch" = "-s 28";
|
"winch" = "-s 20";
|
||||||
"usr1" = "-s 10";
|
"usr1" = "-s usr1";
|
||||||
"usr2" = "-s 12";
|
"usr2" = "-s usr2";
|
||||||
}
|
}
|
||||||
.${action};
|
.${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 =
|
watcher =
|
||||||
let
|
let
|
||||||
name' = "restart-${name}";
|
name' = "restart-${name}";
|
||||||
|
refs = concatStringsSep " "
|
||||||
|
(map (s: "${s "service"}:${s "path"}") watch);
|
||||||
in
|
in
|
||||||
longrun {
|
longrun {
|
||||||
name = name';
|
name = name';
|
||||||
@ -55,16 +48,13 @@ let
|
|||||||
if test -e $dir/notification-fd; then flag="-U"; else flag="-u"; fi
|
if test -e $dir/notification-fd; then flag="-U"; else flag="-u"; fi
|
||||||
${s6}/bin/s6-svwait $flag /run/service/${name} || exit
|
${s6}/bin/s6-svwait $flag /run/service/${name} || exit
|
||||||
PATH=${s6-rc}/bin:${s6}/bin:$PATH
|
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
|
in
|
||||||
service.overrideAttrs (o: {
|
service.overrideAttrs (o: {
|
||||||
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watched-service != null) watcher;
|
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watch != []) watcher;
|
||||||
dependencies =
|
dependencies =
|
||||||
(lim.orEmpty o.dependencies)
|
(lim.orEmpty o.dependencies)
|
||||||
++ optionals (watched-service != null) [
|
++ optional (watch != []) watcher;
|
||||||
watcher
|
|
||||||
watched-service
|
|
||||||
];
|
|
||||||
})
|
})
|
||||||
|
@ -10,6 +10,7 @@ let
|
|||||||
splitString
|
splitString
|
||||||
hasInfix
|
hasInfix
|
||||||
substring
|
substring
|
||||||
|
optionalString
|
||||||
;
|
;
|
||||||
inherit (lib.lists) groupBy;
|
inherit (lib.lists) groupBy;
|
||||||
inherit (lib.attrsets) mapAttrsToList;
|
inherit (lib.attrsets) mapAttrsToList;
|
||||||
@ -56,12 +57,14 @@ let
|
|||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
elements ? [ ],
|
elements ? [ ],
|
||||||
|
extraText ? null,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
''
|
''
|
||||||
set ${name} {
|
set ${name} {
|
||||||
type ${type}
|
type ${type}
|
||||||
${if elements != [ ] then "elements = { ${concatStringsSep ", " elements} }" else ""}
|
${if elements != [ ] then "elements = { ${concatStringsSep ", " (builtins.trace elements elements)} }" else ""}
|
||||||
|
${optionalString (extraText != null) extraText}
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
check:
|
|
||||||
./output-template '{{' '}}' < example.ini > output
|
|
||||||
diff -u output example.ini.expected
|
|
@ -2,6 +2,7 @@
|
|||||||
fetchurl,
|
fetchurl,
|
||||||
writeFennel,
|
writeFennel,
|
||||||
fennel,
|
fennel,
|
||||||
|
fennelrepl,
|
||||||
runCommand,
|
runCommand,
|
||||||
lua,
|
lua,
|
||||||
anoia,
|
anoia,
|
||||||
@ -17,9 +18,9 @@ stdenv.mkDerivation {
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
buildInputs = [ lua ];
|
buildInputs = [ lua ];
|
||||||
doCheck = true;
|
nativeBuildInputs = [ fennelrepl ] ;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
|
fennelrepl --test ./output-template.fnl
|
||||||
cp -p ${
|
cp -p ${
|
||||||
writeFennel name {
|
writeFennel name {
|
||||||
packages = [
|
packages = [
|
||||||
@ -27,11 +28,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}
|
||||||
'';
|
'';
|
||||||
|
@ -24,21 +24,43 @@
|
|||||||
(let [delim (.. opening "(.-)" closing)
|
(let [delim (.. opening "(.-)" closing)
|
||||||
myenv {
|
myenv {
|
||||||
: string
|
: string
|
||||||
|
: table
|
||||||
|
: ipairs
|
||||||
:output
|
:output
|
||||||
(fn [service-path path]
|
(fn [service-path path default]
|
||||||
(let [s (assert (svc.open (.. service-path "/.outputs")))]
|
(let [s (assert (svc.open (.. service-path "/.outputs")))]
|
||||||
(s:output path)))
|
(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]
|
||||||
(assert ((load (.. "return " x) x :t myenv))
|
(let [chunk (if (= (x:sub 1 1) ";")
|
||||||
(string.format "missing value for %q" x))))))
|
(x:sub 2)
|
||||||
|
(.. "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 }
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
check:
|
|
||||||
./output-template '{{' '}}' < example.ini > output
|
|
||||||
diff -u output example.ini.expected
|
|
@ -2,6 +2,7 @@
|
|||||||
fetchurl,
|
fetchurl,
|
||||||
writeFennel,
|
writeFennel,
|
||||||
fennel,
|
fennel,
|
||||||
|
fennelrepl,
|
||||||
runCommand,
|
runCommand,
|
||||||
lua,
|
lua,
|
||||||
anoia,
|
anoia,
|
||||||
@ -17,9 +18,10 @@ stdenv.mkDerivation {
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
buildInputs = [ lua ];
|
buildInputs = [ lua ];
|
||||||
# doCheck = true;
|
nativeBuildInputs = [ fennelrepl ] ;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
|
fennelrepl --test ./watch-outputs.fnl
|
||||||
cp -p ${
|
cp -p ${
|
||||||
writeFennel name {
|
writeFennel name {
|
||||||
packages = [
|
packages = [
|
||||||
@ -28,11 +30,12 @@ stdenv.mkDerivation {
|
|||||||
linotify
|
linotify
|
||||||
fennel
|
fennel
|
||||||
];
|
];
|
||||||
|
macros = [ anoia.dev ];
|
||||||
mainFunction = "run";
|
mainFunction = "run";
|
||||||
} ./watch-outputs.fnl
|
} ./watch-outputs.fnl
|
||||||
} ${name}
|
} ${name}
|
||||||
'';
|
'';
|
||||||
# checkPhase = "make check";
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
install -D ${name} $out/bin/${name}
|
install -D ${name} $out/bin/${name}
|
||||||
'';
|
'';
|
||||||
|
@ -1 +0,0 @@
|
|||||||
a11
|
|
@ -1 +0,0 @@
|
|||||||
a33
|
|
@ -1 +0,0 @@
|
|||||||
a55
|
|
@ -1 +0,0 @@
|
|||||||
a66
|
|
@ -1 +0,0 @@
|
|||||||
000000
|
|
@ -1 +0,0 @@
|
|||||||
0000ff
|
|
@ -1 +0,0 @@
|
|||||||
00ff00
|
|
@ -1 +0,0 @@
|
|||||||
ff0000
|
|
@ -1 +0,0 @@
|
|||||||
eth1
|
|
@ -1,3 +0,0 @@
|
|||||||
wpa_passphrase={{ output("./example-service","colours/black") }}
|
|
||||||
think = {{ string.format("%q", output("./example-service","colours/blue")) }}
|
|
||||||
argonaut = {{ json_quote "hello\ngoodbye\tnext\027" }}
|
|
@ -1,3 +0,0 @@
|
|||||||
wpa_passphrase=000000
|
|
||||||
think = "0000ff"
|
|
||||||
argonaut = "hello\ngoodbye\tnext\u001B"
|
|
@ -1,44 +0,0 @@
|
|||||||
(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 }
|
|
@ -1,10 +1,18 @@
|
|||||||
(local { : %% : system : assoc : split : table= : dig } (require :anoia))
|
(local { : %% : system : assoc : split : table= : dig } (require :anoia))
|
||||||
(local svc (require :anoia.svc))
|
(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]
|
(local { : view } (require :fennel))
|
||||||
(icollect [_ path (ipairs paths)]
|
|
||||||
(split "/" path)))
|
(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]
|
(fn parse-args [args]
|
||||||
(match args
|
(match args
|
||||||
@ -17,37 +25,72 @@
|
|||||||
["-s" signal service & rest] (assoc (parse-args rest)
|
["-s" signal service & rest] (assoc (parse-args rest)
|
||||||
:controlled-service service
|
:controlled-service service
|
||||||
:action [:signal signal])
|
:action [:signal signal])
|
||||||
[watched-service & paths] { : watched-service
|
outputs { :output-references (output-refs outputs) } ))
|
||||||
:paths (split-paths paths)
|
|
||||||
}))
|
(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]
|
(fn changed? [paths old-tree new-tree]
|
||||||
(accumulate [changed? false
|
(accumulate [changed? false
|
||||||
_ path (ipairs paths)]
|
_ path (ipairs paths)]
|
||||||
(or changed? (not (table= (dig old-tree path) (dig new-tree path))))))
|
(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]
|
(fn do-action [action service]
|
||||||
(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 %d /run/service/%s" n service))))
|
[:signal n] (system (%% "s6-svc -s %q /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 (svc.open (.. s "/.outputs")) p)))
|
||||||
|
|
||||||
(fn run []
|
(fn run []
|
||||||
(let [{
|
(let [trees {}
|
||||||
|
{
|
||||||
|
: output-references
|
||||||
: controlled-service
|
: controlled-service
|
||||||
: action
|
: action
|
||||||
: watched-service
|
: watched-service
|
||||||
: paths } (parse-args arg)
|
: paths } (parse-args arg)]
|
||||||
dir (.. watched-service "/.outputs")
|
(while true
|
||||||
service (assert (svc.open dir))]
|
(let [services (open-services output-references)
|
||||||
(print (%% "watching %q" watched-service))
|
trees (collect [s _ (pairs services)]
|
||||||
(accumulate [tree (service:output ".")
|
(values s (s:output ".")))]
|
||||||
v (service:events)]
|
(wait-for-change services)
|
||||||
(let [new-tree (service:output ".")]
|
(each [service paths (pairs services)]
|
||||||
(when (changed? paths tree new-tree)
|
(let [new-tree (service:output ".")]
|
||||||
(print "watched path event:" action controlled-service)
|
(when (changed? paths (. trees service) new-tree)
|
||||||
(do-action action controlled-service))
|
(do-action action controlled-service))))))))
|
||||||
new-tree))))
|
|
||||||
|
|
||||||
|
|
||||||
{ : run }
|
{ : run }
|
||||||
|
@ -15,6 +15,7 @@ rec {
|
|||||||
../../modules/ppp
|
../../modules/ppp
|
||||||
../../modules/dnsmasq
|
../../modules/dnsmasq
|
||||||
../../modules/network
|
../../modules/network
|
||||||
|
../../modules/firewall
|
||||||
];
|
];
|
||||||
|
|
||||||
services.pppoe = svc.pppoe.build {
|
services.pppoe = svc.pppoe.build {
|
||||||
@ -23,6 +24,13 @@ rec {
|
|||||||
password = "NotReallyTheSecret";
|
password = "NotReallyTheSecret";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.firewall = svc.firewall.build {
|
||||||
|
zones = {
|
||||||
|
wan = [ services.pppoe ];
|
||||||
|
lan = [ services.lan4 ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
services.defaultroute4 = svc.network.route.build {
|
services.defaultroute4 = svc.network.route.build {
|
||||||
via = "$(output ${services.pppoe} address)";
|
via = "$(output ${services.pppoe} address)";
|
||||||
target = "default";
|
target = "default";
|
||||||
@ -39,5 +47,5 @@ rec {
|
|||||||
domain = "fake.liminix.org";
|
domain = "fake.liminix.org";
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultProfile.packages = [ pkgs.hello ];
|
defaultProfile.packages = with pkgs; [ nftables hello ];
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,46 @@
|
|||||||
set timeout 60
|
set timeout 60
|
||||||
|
|
||||||
spawn socat unix-connect:vm/console -
|
spawn socat unix-connect:vm/console -
|
||||||
|
expect "s6-linux-init"
|
||||||
send "\r\n"
|
send "\r\n"
|
||||||
expect "#"
|
expect "#"
|
||||||
|
send "PS1=\$(echo 'I1JFQURZIyA=' | base64 -d); stty -echo\n"
|
||||||
|
expect "#READY#"
|
||||||
set FINISHED 0
|
set FINISHED 0
|
||||||
set EXIT "1"
|
set EXIT "1"
|
||||||
while { $FINISHED < 10 } {
|
while { $FINISHED < 20 } {
|
||||||
send "ip address show dev ppp0 | grep ppp0\r\n"
|
send "ip address show dev ppp0 | grep ppp0\n"
|
||||||
expect {
|
expect {
|
||||||
"192.168.100.1" { set FINISHED 20; set EXIT 0; }
|
"192.168.100.1" { set FINISHED 200; set EXIT 0; }
|
||||||
"can't find device" { send_user "waiting ..." ; send "\r\n"; sleep 3 }
|
"can't find device" { send_user "waiting ..." ; sleep 3 }
|
||||||
"DOWN" { send_user "waiting ..." ; send "\r\n"; sleep 3 }
|
"DOWN" { send_user "waiting ..." ; sleep 3 }
|
||||||
}
|
}
|
||||||
set FINISHED [ expr $FINISHED + 1 ]
|
set FINISHED [ expr $FINISHED + 1 ]
|
||||||
}
|
}
|
||||||
|
expect "#READY#"
|
||||||
|
send "s6-svwait -U /run/service/wan.link.pppoe\n"
|
||||||
|
expect "#READY#"
|
||||||
|
|
||||||
|
set timeout 30
|
||||||
|
send "nft list set ip table-ip lan || touch /non/existent\n"
|
||||||
|
expect {
|
||||||
|
"lan" { puts "lan found" }
|
||||||
|
"{ }" { puts "missing ifname"; exit 1 }
|
||||||
|
"No such file or directory" { exit 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user