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