1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.frr;
8
9 services = [
10 "static"
11 "bgp"
12 "ospf"
13 "ospf6"
14 "rip"
15 "ripng"
16 "isis"
17 "pim"
18 "ldp"
19 "nhrp"
20 "eigrp"
21 "babel"
22 "sharp"
23 "pbr"
24 "bfd"
25 "fabric"
26 ];
27
28 allServices = services ++ [ "zebra" ];
29
30 isEnabled = service: cfg.${service}.enable;
31
32 daemonName = service: if service == "zebra" then service else "${service}d";
33
34 configFile = service:
35 let
36 scfg = cfg.${service};
37 in
38 if scfg.configFile != null then scfg.configFile
39 else pkgs.writeText "${daemonName service}.conf"
40 ''
41 ! FRR ${daemonName service} configuration
42 !
43 hostname ${config.networking.hostName}
44 log syslog
45 service password-encryption
46 !
47 ${scfg.config}
48 !
49 end
50 '';
51
52 serviceOptions = service:
53 {
54 enable = mkEnableOption (lib.mdDoc "the FRR ${toUpper service} routing protocol");
55
56 configFile = mkOption {
57 type = types.nullOr types.path;
58 default = null;
59 example = "/etc/frr/${daemonName service}.conf";
60 description = lib.mdDoc ''
61 Configuration file to use for FRR ${daemonName service}.
62 By default the NixOS generated files are used.
63 '';
64 };
65
66 config = mkOption {
67 type = types.lines;
68 default = "";
69 example =
70 let
71 examples = {
72 rip = ''
73 router rip
74 network 10.0.0.0/8
75 '';
76
77 ospf = ''
78 router ospf
79 network 10.0.0.0/8 area 0
80 '';
81
82 bgp = ''
83 router bgp 65001
84 neighbor 10.0.0.1 remote-as 65001
85 '';
86 };
87 in
88 examples.${service} or "";
89 description = lib.mdDoc ''
90 ${daemonName service} configuration statements.
91 '';
92 };
93
94 vtyListenAddress = mkOption {
95 type = types.str;
96 default = "localhost";
97 description = lib.mdDoc ''
98 Address to bind to for the VTY interface.
99 '';
100 };
101
102 vtyListenPort = mkOption {
103 type = types.nullOr types.int;
104 default = null;
105 description = lib.mdDoc ''
106 TCP Port to bind to for the VTY interface.
107 '';
108 };
109
110 extraOptions = mkOption {
111 type = types.listOf types.str;
112 default = [];
113 description = lib.mdDoc ''
114 Extra options for the daemon.
115 '';
116 };
117 };
118
119in
120
121{
122
123 ###### interface
124 imports = [
125 {
126 options.services.frr = {
127 zebra = (serviceOptions "zebra") // {
128 enable = mkOption {
129 type = types.bool;
130 default = any isEnabled services;
131 description = lib.mdDoc ''
132 Whether to enable the Zebra routing manager.
133
134 The Zebra routing manager is automatically enabled
135 if any routing protocols are configured.
136 '';
137 };
138 };
139 };
140 }
141 { options.services.frr = (genAttrs services serviceOptions); }
142 ];
143
144 ###### implementation
145
146 config = mkIf (any isEnabled allServices) {
147
148 environment.systemPackages = [
149 pkgs.frr # for the vtysh tool
150 ];
151
152 users.users.frr = {
153 description = "FRR daemon user";
154 isSystemUser = true;
155 group = "frr";
156 };
157
158 users.groups = {
159 frr = {};
160 # Members of the frrvty group can use vtysh to inspect the FRR daemons
161 frrvty = { members = [ "frr" ]; };
162 };
163
164 environment.etc = let
165 mkEtcLink = service: {
166 name = "frr/${service}.conf";
167 value.source = configFile service;
168 };
169 in
170 (builtins.listToAttrs
171 (map mkEtcLink (filter isEnabled allServices))) // {
172 "frr/vtysh.conf".text = "";
173 };
174
175 systemd.tmpfiles.rules = [
176 "d /run/frr 0750 frr frr -"
177 ];
178
179 systemd.services =
180 let
181 frrService = service:
182 let
183 scfg = cfg.${service};
184 daemon = daemonName service;
185 in
186 nameValuePair daemon ({
187 wantedBy = [ "multi-user.target" ];
188 after = [ "network-pre.target" "systemd-sysctl.service" ] ++ lib.optionals (service != "zebra") [ "zebra.service" ];
189 bindsTo = lib.optionals (service != "zebra") [ "zebra.service" ];
190 wants = [ "network.target" ];
191
192 description = if service == "zebra" then "FRR Zebra routing manager"
193 else "FRR ${toUpper service} routing daemon";
194
195 unitConfig.Documentation = if service == "zebra" then "man:zebra(8)"
196 else "man:${daemon}(8) man:zebra(8)";
197
198 restartTriggers = [
199 (configFile service)
200 ];
201 reloadIfChanged = true;
202
203 serviceConfig = {
204 PIDFile = "frr/${daemon}.pid";
205 ExecStart = "${pkgs.frr}/libexec/frr/${daemon} -f /etc/frr/${service}.conf"
206 + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
207 + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}"
208 + " " + (concatStringsSep " " scfg.extraOptions);
209 ExecReload = "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemonName service} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${service}.conf";
210 Restart = "on-abnormal";
211 };
212 });
213 in
214 listToAttrs (map frrService (filter isEnabled allServices));
215
216 };
217
218 meta.maintainers = with lib.maintainers; [ woffs ];
219
220}