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