1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
{
lib,
config,
pkgs,
...
}:
let
cfg = config.networking.ifstate;
initrdCfg = config.boot.initrd.network.ifstate;
settingsFormat = {
# override generator in order to:
# - use yq and not remarshal because it matches yaml datatype handling with IfState
# - validate json schema
generate =
name: value: package:
pkgs.runCommand name
{
nativeBuildInputs = with pkgs; [
yq
check-jsonschema
];
strictDeps = true;
value = builtins.toJSON value;
__structuredAttrs = true;
}
''
printf "%s" "$value" | yq --yaml-output . > $out
check-jsonschema --schemafile "${cfg.package.passthru.jsonschema}" "$out"
sed -i $'s|\'!include |!include \'|' $out
'';
inherit (pkgs.formats.yaml { }) type;
};
initrdInterfaceTypes = map (interface: interface.link.kind) (
builtins.attrValues initrdCfg.settings.interfaces
);
# IfState interface kind to kernel modules mapping
interfaceKernelModules = {
"ifb" = [ "ifb" ];
"ip6tnl" = [ "ip6tnl" ];
"ipoib" = [ "ib_ipoib" ];
"ipvlan" = [ "ipvlan" ];
"macvlan" = [ "macvlan" ];
"macvtap" = [ "macvtap" ];
"team" = [ "team" ];
"tun" = [ "tun" ];
"vrf" = [ "vrf" ];
"vti" = [ "ip_vti" ];
"vti6" = [ "ip6_vti" ];
"bond" = [ "bonding" ];
"bridge" = [ "bridge" ];
# "physical" = ...;
"dsa" = [ "dsa_core" ];
"dummy" = [ "dummy" ];
"veth" = [ "veth" ];
"vxcan" = [ "vxcan" ];
"vlan" = [ "8021q" ];
"vxlan" = [ "vxlan" ];
"ipip" = [ "ipip" ];
"sit" = [ "sit" ];
"gre" = [ "ip_gre" ];
"gretap" = [ "ip_gre" ];
"ip6gre" = [ "ip6_gre" ];
"ip6gretap" = [ "ip6_gre" ];
"geneve" = [ "geneve" ];
"wireguard" = [ "wireguard" ];
"xfrm" = [ "xfrm_interface" ];
};
# https://github.com/systemd/systemd/blob/main/units/systemd-networkd.service.in
commonServiceConfig = {
after = [
"network-pre.target"
"systemd-sysusers.service"
"systemd-sysctl.service"
];
before = [
"network.target"
"multi-user.target"
"shutdown.target"
"initrd-switch-root.target"
];
conflicts = [
"shutdown.target"
"initrd-switch-root.target"
];
wants = [
"network.target"
];
unitConfig = {
# Avoid default dependencies like "basic.target", which prevents ifstate from starting before luks is unlocked.
DefaultDependencies = "no";
};
};
in
{
meta.maintainers = with lib.maintainers; [ marcel ];
options = {
networking.ifstate = {
enable = lib.mkEnableOption "networking using IfState";
package = lib.mkPackageOption pkgs "ifstate" { };
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
description = "Content of IfState's configuration file. See <https://ifstate.net/2.2/schema/> for details.";
};
};
boot.initrd.network.ifstate = {
enable = lib.mkEnableOption "initrd networking using IfState";
allowIfstateToDrasticlyIncreaseInitrdSize = lib.mkOption {
type = lib.types.bool;
default = false;
description = "IfState in initrd drastically increases the size of initrd, your boot partition may be too small and/or you may have significantly fewer generations. By setting this option, you acknowledge this fact and keep it in mind when reporting issues.";
};
package = lib.mkOption {
type = lib.types.package;
default = cfg.package.override {
withConfigValidation = false;
};
defaultText = lib.literalExpression "pkgs.ifstate.override { withConfigValidation = false; }";
description = "The initrd IfState package to use.";
};
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
description = "Content of IfState's initrd configuration file. See <https://ifstate.net/2.2/schema/> for details.";
};
cleanupSettings = lib.mkOption {
inherit (settingsFormat) type;
# required by json schema
default.interfaces = { };
description = "Content of IfState's initrd cleanup configuration file. See <https://ifstate.net/2.0/schema/> for details. This configuration gets applied before systemd switches to stage two. The goal is to deconfigurate the whole network in order to prevent access to services, before the firewall is configured. The stage two IfState configuration will start after the firewall is configured.";
};
};
};
config = lib.mkMerge [
(lib.mkIf (cfg.enable || initrdCfg.enable) {
# sane defaults to not let IfState work against the kernel
boot.extraModprobeConfig = ''
options bonding max_bonds=0
options dummy numdummies=0
options ifb numifbs=0
'';
})
(lib.mkIf cfg.enable {
assertions = [
{
assertion = !config.networking.networkmanager.enable;
message = "IfState and NetworkManager cannot be used at the same time, as both configure the network in a conflicting manner.";
}
{
assertion = !config.networking.useDHCP;
message = "IfState and networking.useDHCP cannot be used at the same time, as both configure the network. Please look into IfState hooks to integrate DHCP: https://codeberg.org/liske/ifstate/issues/111";
}
];
networking.useDHCP = lib.mkDefault false;
environment = {
# ifstatecli command should be available to use user, there are other useful subcommands like check or show
systemPackages = [ cfg.package ];
# match the default value of the --config flag of IfState
etc."ifstate/ifstate.yaml".source = settingsFormat.generate "ifstate.yaml" cfg.settings cfg.package;
};
systemd.services.ifstate = lib.recursiveUpdate commonServiceConfig {
description = "IfState";
wantedBy = [
"multi-user.target"
];
# mount is always available on nixos, avoid adding additional store paths to the closure
path = [ "/run/wrappers" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${lib.getExe cfg.package} --config ${
config.environment.etc."ifstate/ifstate.yaml".source
} apply";
# We wait for the udev events queue to empty in the *hope* that the
# devices needed here become available. This is terribly broken and
# essentially no better than a random sleep(). Same below for initrd.
# FIXME: use .device units dependecies instead.
ExecStartPre = "-${lib.getExe' config.systemd.package "udevadm"} settle --timeout=180";
# because oneshot services do not have a timeout by default
TimeoutStartSec = "2min";
};
};
})
(lib.mkIf initrdCfg.enable {
assertions = [
{
assertion = initrdCfg.allowIfstateToDrasticlyIncreaseInitrdSize;
message = "IfState in initrd drastically increases the size of initrd, your boot partition may be too small and/or you may have significantly fewer generations. By setting boot.initrd.network.initrd.allowIfstateToDrasticlyIncreaseInitrdSize to true, you acknowledge this fact and keep it in mind when reporting issues.";
}
{
assertion = cfg.enable;
message = "If IfState is used in initrd, it should also be used for the stage 2 system (networking.ifstate), as initrd IfState does not clean up the network stack like it was before after execution.";
}
{
assertion = config.boot.initrd.systemd.enable;
message = "IfState only supports systemd stage one. See `boot.initrd.systemd.enable` option.";
}
];
environment.etc = {
"ifstate/ifstate.initrd.yaml".source =
settingsFormat.generate "ifstate.initrd.yaml" initrdCfg.settings
initrdCfg.package;
"ifstate/ifstate.initrd-cleanup.yaml".source =
settingsFormat.generate "ifstate.initrd-cleanup.yaml" initrdCfg.cleanupSettings
initrdCfg.package;
};
boot.initrd = {
network.udhcpc.enable = lib.mkDefault false;
# automatic configuration of kernel modules of virtual interface types
availableKernelModules =
let
enableModule =
type:
if builtins.hasAttr type interfaceKernelModules then interfaceKernelModules."${type}" else [ ];
in
lib.flatten (map enableModule initrdInterfaceTypes);
systemd = {
storePaths = [
(pkgs.runCommand "ifstate-closure"
{
info = pkgs.closureInfo {
rootPaths = [
initrdCfg.package
# copy whole config closure, because it can reference other files using !include
config.environment.etc."ifstate/ifstate.initrd.yaml".source
config.environment.etc."ifstate/ifstate.initrd-cleanup.yaml".source
];
};
}
''
mkdir $out
cat "$info"/store-paths | while read path; do
ln -s "$path" "$out/$(basename "$path")"
done
''
)
];
# https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/system/boot/networkd.nix#L3444
additionalUpstreamUnits = [
"network-online.target"
"network-pre.target"
"network.target"
"nss-lookup.target"
"nss-user-lookup.target"
"remote-fs-pre.target"
"remote-fs.target"
];
services.ifstate-initrd = lib.recursiveUpdate commonServiceConfig {
description = "IfState initrd";
wantedBy = [
"initrd.target"
];
# mount is always available on nixos, avoid adding additional store paths to the closure
# https://github.com/NixOS/nixpkgs/blob/2b8e2457ebe576ebf41ddfa8452b5b07a8d493ad/nixos/modules/system/boot/systemd/initrd.nix#L550-L551
path = [
config.boot.initrd.systemd.package.util-linux
];
serviceConfig = {
Type = "oneshot";
# Otherwise systemd starts ifstate again, after the encryption password was entered by the user
# and we are able to implement the cleanup using ExecStop rather than a separate unit.
RemainAfterExit = true;
ExecStart = "${lib.getExe initrdCfg.package} --config ${
config.environment.etc."ifstate/ifstate.initrd.yaml".source
} apply";
ExecStop = "${lib.getExe initrdCfg.package} --config ${
config.environment.etc."ifstate/ifstate.initrd-cleanup.yaml".source
} apply";
# because oneshot services do not have a timeout by default
TimeoutStartSec = "2min";
# See comment on non-initrd service above
ExecStartPre = "-${lib.getExe' config.boot.initrd.systemd.package "udevadm"} settle --timeout=180";
};
};
};
};
})
];
}
|