blob: 50f5ff83a1fe9d39a80064c924d94f590308f2c3 [file] [log] [blame]
Serge Bazanski6abe4fa2020-10-03 00:18:34 +02001# Declarative routing configuration. Usees BIRD2 underneath.
2#
3# The mapping from declarative configuration to BIRD is quite straightforward,
4# however, we take a few liberties:
5# - we introduce an 'originate' protocol for originating prefixes (using the
6# static protocol).
7# - routing tables in the configuration are referred to by a common name for
8# IPv4 and IPv4 - while in BIRD, two tables are created (suffixed by '4' and
9# '6', following the two default 'master4' and 'master6' tables).
10
11{ config, lib, pkgs, ... }:
12
13with lib;
14let
15 cfg = config.hscloud.routing;
16
Serge Bazanskid9a63652020-10-16 19:07:41 +020017 staticType = af: let
18 v4 = af == "ipv4";
19 v6 = af == "ipv6";
20 pretty = if v4 then "IPv4" else "IPv6";
21 in with types; mkOption {
22 type = attrsOf (submodule {
23 options = {
24 table = mkOption {
25 type = nullOr str;
26 description = "BIRD table to which session should be connected.";
27 };
28 address = mkOption {
29 type = str;
30 description = "Address part of prefix to announce.";
31 };
32 prefixLength = mkOption {
33 type = int;
34 description = "Prefix length to announce.";
35 };
36 via = mkOption {
37 type = str;
38 description = "Target address for static route.";
39 };
40 };
41 });
42 default = {};
43 description = "${pretty} static routes to inject into a table.";
44 };
45
46 staticRender = af: n: v: let
47 name = "static_static_${af}_${n}";
48 ip = if af == "ipv4" then "4" else "6";
49 in ''
50 protocol static ${name} {
51 ${af} {
52 table ${v.table}${ip};
53 import all;
54 export none;
55 };
56
57 route ${v.address}/${toString v.prefixLength} via ${v.via};
58 }
59 '';
60
Serge Bazanski6abe4fa2020-10-03 00:18:34 +020061 originateType = af: let
62 v4 = af == "ipv4";
63 v6 = af == "ipv6";
64 pretty = if v4 then "IPv4" else "IPv6";
65 in with types; mkOption {
66 type = attrsOf (submodule {
67 options = {
68 table = mkOption {
69 type = nullOr str;
70 description = "BIRD table to which session should be connected.";
71 };
72 address = mkOption {
73 type = str;
74 description = "Address part of prefix to announce.";
75 };
76 prefixLength = mkOption {
77 type = int;
78 description = "Prefix length to announce.";
79 };
80 };
81 });
82 default = {};
83 description = "${pretty} prefixes to unconditionally inject into a table.";
84 };
85
86 originateRender = af: n: v: let
87 name = "static_originate_${af}_${n}";
88 ip = if af == "ipv4" then "4" else "6";
89 in ''
90 protocol static ${name} {
91 ${af} {
92 table ${v.table}${ip};
93 import all;
94 export none;
95 };
96
97 route ${v.address}/${toString v.prefixLength} blackhole;
98 }
99 '';
100
101 ospfType = af: let
102 v4 = af == "ipv4";
103 v6 = af == "ipv6";
104 pretty = if v4 then "IPv4" else "IPv6";
105 ospf = if v4 then "OSPFv2" else "OSPFv3";
106 in with types; mkOption {
107 type = attrsOf (submodule {
108 options = {
109 table = mkOption {
110 type = nullOr str;
111 description = "BIRD table to which session should be connected.";
112 };
113 filterIn = mkOption {
114 type = str;
115 default = "accept;";
116 description = "BIRD filter definition for received routes.";
117 };
118 filterOut = mkOption {
119 type = str;
120 default = "accept;";
121 description = "BIRD filter definition for sent routes.";
122 };
123 area = mkOption {
124 type = attrsOf (submodule {
125 options = {
126 interfaces = mkOption {
127 type = attrsOf (submodule {
128 options = {
129 cost = mkOption {
130 type = int;
131 default = 10; # 1Gbps
132 description = "Interface cost (10e9/iface_speed_in_bps).";
133 };
134 type = mkOption {
135 type = enum ["bcast" "nbma" "ptp" "ptmp"];
136 description = "Interface type (dictates BIRD behaviour).";
137 };
138 stub = mkOption {
139 type = bool;
140 default = false;
141 description = "Interface is stub (do not HELLO).";
142 };
143 };
144 });
145 description = "Interface configuration";
146 };
147 };
148 });
149 description = "Area configuration";
150 };
151 };
152 });
153 default = {};
154 description = "${ospf} configuration";
155 };
156
157 ospfRender = af: n: v: let
158 v4 = af == "ipv4";
159 v6 = af == "ipv6";
160 ip = if v4 then "4" else "6";
161 name = "ospf_${af}_${n}";
162
163 interfaces = mapAttrsToList (iface: ifaceConfig: ''
164 interface "${iface}" {
165 type ${ifaceConfig.type};
166 cost ${toString ifaceConfig.cost};
167 ${if ifaceConfig.stub then "stub yes;" else ""}
168 };
169 '');
170 areas = mapAttrsToList (area: areaConfig: ''
171 area ${area} {
172 ${concatStringsSep "\n" (interfaces areaConfig.interfaces)}
173 };
174 '') v.area;
175 in ''
176 filter ${name}_in {
177 ${v.filterIn}
178 };
179 filter ${name}_out {
180 ${v.filterOut}
181 };
182 protocol ospf ${if v4 then "v2" else "v3"} ${name} {
183 ${af} {
184 table ${v.table}${ip};
185 import filter ${name}_in;
186 export filter ${name}_out;
187 };
188 ${concatStringsSep "\n" areas}
189 }
190 '';
191
192 pipeType = af: with types; mkOption {
193 type = attrsOf (submodule {
194 options = {
195 table = mkOption {
196 type = nullOr str;
197 description = "BIRD table to which session should be connected.";
198 };
199 peerTable = mkOption {
200 type = nullOr str;
201 description = "BIRD 'remote' table to which session should be connected.";
202 };
203 filterIn = mkOption {
204 type = str;
205 default = "accept";
206 description = "BIRD filter definition for routes received from peerTable";
207 };
208 filterOut = mkOption {
209 type = str;
210 default = "reject;";
211 description = "BIRD filter definition for routes sent to peerTable";
212 };
213 };
214 });
215 default = {};
216 description = "${pretty} prefixes to pipe from one table to another.";
217 };
218
219 pipeRender = af: n: v: let
220 name = "pipe_${af}_${n}";
221 v4 = af == "ipv4";
222 v6 = af == "ipv6";
223 ip = if v4 then "4" else "6";
224 in ''
225 filter ${name}_in {
226 ${v.filterIn}
227 };
228 filter ${name}_out {
229 ${v.filterOut}
230 };
231 protocol pipe ${name} {
232 table ${v.table}${ip};
233 peer table ${v.peerTable}${ip};
234 import filter ${name}_in;
235 export filter ${name}_out;
236 }
237 '';
238
239 bgpSessionsType = af: let
240 v4 = af == "ipv4";
241 v6 = af == "ipv6";
242 pretty = if v4 then "IPv4" else "IPv6";
243 in with types; mkOption {
244 type = attrsOf (submodule {
245 options = {
246 description = mkOption {
247 type = str;
248 description = "Session description (for BIRD).";
249 };
250 table = mkOption {
251 type = nullOr str;
252 description = "BIRD table to which session should be connected.";
253 };
254 local = mkOption {
255 type = str;
256 description = "${pretty} address of this router.";
257 };
258 asn = mkOption {
259 type = int;
260 description = "ASN of local router - will default to hscloud.routing.asn.";
261 default = cfg.asn;
262 };
263 prepend = mkOption {
264 type = int;
265 default = 0;
266 description = "How many times to prepend this router's ASN on the link.";
267 };
268 pref = mkOption {
269 type = int;
270 default = 100;
271 description = "Preference (BGP local_pref) for routes from this session.";
272 };
273 direct = mkOption {
274 type = nullOr bool;
275 default = null;
276 };
277 filterIn = mkOption {
278 type = str;
279 default = "accept;";
280 description = "BIRD filter definition for received routes.";
281 };
282 filterOut = mkOption {
283 type = str;
284 default = "accept;";
285 description = "BIRD filter definition for sent routes.";
286 };
287 neighbors = mkOption {
288 type = listOf (submodule {
289 options = {
290 address = mkOption {
291 type = str;
292 description = "${pretty} address of neighbor.";
293 };
294 asn = mkOption {
295 type = int;
296 description = "ASN of neighbor.";
297 };
298 password = mkOption {
299 type = nullOr str;
300 default = null;
301 description = "BGP TCP MD5 secret.";
302 };
303 };
304 });
305 description = "BGP Neighbor configuration";
306 };
307 };
308 });
309 default = {};
310 description = "BGP Sesions for ${pretty}";
311 };
312
313 bgpSessionRender = af: n: v: let
314 name = "bgp_${af}_${n}";
315 ip = if af == "ipv4" then "4" else "6";
316 filters = ''
317 filter ${name}_in {
318 if bgp_path.len > 64 then reject;
319 bgp_local_pref = ${toString v.pref};
320 ${v.filterIn}
321 }
322
323 filter ${name}_out {
324 ${if v.prepend > 0 then
325 (concatStringsSep "\n"
326 (map (_: "bgp_path.prepend(${toString v.asn});") (range 0 (v.prepend - 1)))
327 )
328 else ""}
329 ${v.filterOut}
330 }
331 '';
332 peer = ix: peer: ''
333 protocol bgp ${name}_${toString ix} {
334 description "${v.description}";
335
336 ${af} {
337 table ${v.table}${ip};
338 import filter ${name}_in;
339 export filter ${name}_out;
340 };
341
342 local ${v.local} as ${toString v.asn};
343 neighbor ${peer.address} as ${toString peer.asn};
344 ${if peer.password != null then "password \"${peer.password}\";" else ""}
345 ${if v.direct == true then "direct;" else ""}
346 }
347 '';
348 in "${filters}\n${concatStringsSep "\n" (imap1 peer v.neighbors)}";
349
350 tablesFromProtoAF =
351 af: p: filter (el: el != null) (
352 mapAttrsToList (_: v: "${af} table ${v.table}${if af == "ipv4" then "4" else "6"};") p);
353 tablesFromProto = p: (tablesFromProtoAF "ipv4" p.v4) ++ (tablesFromProtoAF "ipv6" p.v6);
354 tables =
355 unique (
356 (tablesFromProto cfg.bgpSessions) ++
357 (tablesFromProto cfg.originate) ++
358 (tablesFromProto cfg.pipe) ++
359 (tablesFromProto cfg.ospf)
360 # TODO(q3k): also slurp in peer tables from pipes.
361 );
362 tablesRender = ''
363 ${concatStringsSep "\n" tables}
364 '';
365 tablesProgram = mapAttrsToList (n: _: n) (filterAttrs (n: v: v.program == true) cfg.tables);
366 tableProgram =
367 if (length tablesProgram) != 1 then
368 (abort "exactly one table must be set to be programmed")
369 else
370 (head tablesProgram);
371
372in {
373 options.hscloud.routing = {
374 enable = mkEnableOption "declarative routing";
375 routerID = mkOption {
376 type = types.str;
377 description = ''
378 Default Router ID for dynamic routing protocols, eg. IPv4 address from
379 loopback interface.
380 '';
381 };
382 asn = mkOption {
383 type = types.int;
384 description = "Default ASN for BGP.";
385 };
386 extra = mkOption {
387 type = types.lines;
388 description = "Extra configuration lines.";
389 };
390 bgpSessions = {
391 v4 = bgpSessionsType "ipv4";
392 v6 = bgpSessionsType "ipv6";
393 };
394 originate = {
395 v4 = originateType "ipv4";
396 v6 = originateType "ipv6";
397 };
Serge Bazanskid9a63652020-10-16 19:07:41 +0200398 static = {
399 v4 = staticType "ipv4";
400 v6 = staticType "ipv6";
401 };
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200402 pipe = {
403 v4 = pipeType "ipv4";
404 v6 = pipeType "ipv6";
405 };
406 ospf = {
407 v4 = ospfType "ipv4";
408 v6 = ospfType "ipv6";
409 };
410 tables = mkOption {
411 type = types.attrsOf (types.submodule {
412 options = {
413 program = mkOption {
414 type = types.bool;
415 default = false;
416 description = "This is the primary table programmed in to the kernel.";
417 };
418 programSourceV4 = mkOption {
419 type = types.nullOr types.str;
420 default = null;
421 description = "If set, programmed routes will have source set to this address.";
422 };
423 programSourceV6 = mkOption {
424 type = types.nullOr types.str;
425 default = null;
426 description = "If set, programmed routes will have source set to this address.";
427 };
428 };
429 });
430 description = "Routing table configuration.";
431 };
432 };
433
434 config = mkIf cfg.enable {
435 services.bird2.enable = true;
436 services.bird2.config = ''
437 log syslog all;
438 debug protocols { states, interfaces, events }
439
440 router id ${cfg.routerID};
441
442 ${cfg.extra}
443
444 ${tablesRender}
445
446 protocol device {
447 scan time 10;
448 };
449
450 protocol kernel kernel_v4 {
451 scan time 60;
452 ipv4 {
453 table ${tableProgram}4;
454 import none;
455 export filter {
456 ${let src = cfg.tables."${tableProgram}".programSourceV4; in if src != null then ''
457 krt_prefsrc = ${src};
458 '' else ""}
459 accept;
460 };
461 };
462 }
463 protocol kernel kernel_v6 {
464 scan time 60;
465 ipv6 {
466 table ${tableProgram}6;
467 import none;
468 export filter {
469 ${let src = cfg.tables."${tableProgram}".programSourceV6; in if src != null then ''
470 krt_prefsrc = ${src};
471 '' else ""}
472 accept;
473 };
474 };
475 };
476
477 ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv4") cfg.bgpSessions.v4)}
478 ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv6") cfg.bgpSessions.v6)}
479 ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv4") cfg.originate.v4)}
480 ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv6") cfg.originate.v6)}
Serge Bazanskid9a63652020-10-16 19:07:41 +0200481 ${concatStringsSep "\n" (mapAttrsToList (staticRender "ipv4") cfg.static.v4)}
482 ${concatStringsSep "\n" (mapAttrsToList (staticRender "ipv6") cfg.static.v6)}
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200483 ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv4") cfg.pipe.v4)}
484 ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv6") cfg.pipe.v6)}
485 ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv4") cfg.ospf.v4)}
486 ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv6") cfg.ospf.v6)}
487
488 '';
489 };
490}