blob: ac222a9a55626a64c13913ff0c092696bf59a2ba [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 };
Serge Bazanski82fc1312021-12-08 14:09:51 +0000143 neighbors = mkOption {
144 type = listOf str;
145 };
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200146 };
147 });
148 description = "Interface configuration";
149 };
150 };
151 });
152 description = "Area configuration";
153 };
154 };
155 });
156 default = {};
157 description = "${ospf} configuration";
158 };
159
160 ospfRender = af: n: v: let
161 v4 = af == "ipv4";
162 v6 = af == "ipv6";
163 ip = if v4 then "4" else "6";
164 name = "ospf_${af}_${n}";
165
Serge Bazanski82fc1312021-12-08 14:09:51 +0000166 interfaces = mapAttrsToList (iface: ifaceConfig: let
167 neighbors = ''
168 neighbors {
169 ${concatStringsSep "\n" (map (n: "${n};") ifaceConfig.neighbors)}
170 };
171 '';
172 in ''
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200173 interface "${iface}" {
174 type ${ifaceConfig.type};
175 cost ${toString ifaceConfig.cost};
176 ${if ifaceConfig.stub then "stub yes;" else ""}
Serge Bazanski82fc1312021-12-08 14:09:51 +0000177 ${if ifaceConfig.type == "ptmp" then neighbors else ""}
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200178 };
179 '');
180 areas = mapAttrsToList (area: areaConfig: ''
181 area ${area} {
182 ${concatStringsSep "\n" (interfaces areaConfig.interfaces)}
183 };
184 '') v.area;
185 in ''
186 filter ${name}_in {
187 ${v.filterIn}
188 };
189 filter ${name}_out {
190 ${v.filterOut}
191 };
192 protocol ospf ${if v4 then "v2" else "v3"} ${name} {
193 ${af} {
194 table ${v.table}${ip};
195 import filter ${name}_in;
196 export filter ${name}_out;
197 };
198 ${concatStringsSep "\n" areas}
199 }
200 '';
201
202 pipeType = af: with types; mkOption {
203 type = attrsOf (submodule {
204 options = {
205 table = mkOption {
206 type = nullOr str;
207 description = "BIRD table to which session should be connected.";
208 };
209 peerTable = mkOption {
210 type = nullOr str;
211 description = "BIRD 'remote' table to which session should be connected.";
212 };
213 filterIn = mkOption {
214 type = str;
215 default = "accept";
216 description = "BIRD filter definition for routes received from peerTable";
217 };
218 filterOut = mkOption {
219 type = str;
220 default = "reject;";
221 description = "BIRD filter definition for routes sent to peerTable";
222 };
223 };
224 });
225 default = {};
226 description = "${pretty} prefixes to pipe from one table to another.";
227 };
228
229 pipeRender = af: n: v: let
230 name = "pipe_${af}_${n}";
231 v4 = af == "ipv4";
232 v6 = af == "ipv6";
233 ip = if v4 then "4" else "6";
234 in ''
235 filter ${name}_in {
236 ${v.filterIn}
237 };
238 filter ${name}_out {
239 ${v.filterOut}
240 };
241 protocol pipe ${name} {
242 table ${v.table}${ip};
243 peer table ${v.peerTable}${ip};
244 import filter ${name}_in;
245 export filter ${name}_out;
246 }
247 '';
248
249 bgpSessionsType = af: let
250 v4 = af == "ipv4";
251 v6 = af == "ipv6";
252 pretty = if v4 then "IPv4" else "IPv6";
253 in with types; mkOption {
254 type = attrsOf (submodule {
255 options = {
256 description = mkOption {
257 type = str;
258 description = "Session description (for BIRD).";
259 };
260 table = mkOption {
261 type = nullOr str;
262 description = "BIRD table to which session should be connected.";
263 };
264 local = mkOption {
265 type = str;
266 description = "${pretty} address of this router.";
267 };
268 asn = mkOption {
269 type = int;
270 description = "ASN of local router - will default to hscloud.routing.asn.";
271 default = cfg.asn;
272 };
273 prepend = mkOption {
274 type = int;
275 default = 0;
276 description = "How many times to prepend this router's ASN on the link.";
277 };
278 pref = mkOption {
279 type = int;
280 default = 100;
281 description = "Preference (BGP local_pref) for routes from this session.";
282 };
283 direct = mkOption {
284 type = nullOr bool;
285 default = null;
286 };
287 filterIn = mkOption {
288 type = str;
289 default = "accept;";
290 description = "BIRD filter definition for received routes.";
291 };
292 filterOut = mkOption {
293 type = str;
294 default = "accept;";
295 description = "BIRD filter definition for sent routes.";
296 };
297 neighbors = mkOption {
298 type = listOf (submodule {
299 options = {
300 address = mkOption {
301 type = str;
302 description = "${pretty} address of neighbor.";
303 };
304 asn = mkOption {
305 type = int;
306 description = "ASN of neighbor.";
307 };
308 password = mkOption {
309 type = nullOr str;
310 default = null;
311 description = "BGP TCP MD5 secret.";
312 };
313 };
314 });
315 description = "BGP Neighbor configuration";
316 };
317 };
318 });
319 default = {};
320 description = "BGP Sesions for ${pretty}";
321 };
322
323 bgpSessionRender = af: n: v: let
324 name = "bgp_${af}_${n}";
325 ip = if af == "ipv4" then "4" else "6";
326 filters = ''
327 filter ${name}_in {
328 if bgp_path.len > 64 then reject;
329 bgp_local_pref = ${toString v.pref};
330 ${v.filterIn}
331 }
332
333 filter ${name}_out {
334 ${if v.prepend > 0 then
335 (concatStringsSep "\n"
336 (map (_: "bgp_path.prepend(${toString v.asn});") (range 0 (v.prepend - 1)))
337 )
338 else ""}
339 ${v.filterOut}
340 }
341 '';
342 peer = ix: peer: ''
343 protocol bgp ${name}_${toString ix} {
344 description "${v.description}";
345
346 ${af} {
347 table ${v.table}${ip};
348 import filter ${name}_in;
349 export filter ${name}_out;
350 };
351
352 local ${v.local} as ${toString v.asn};
353 neighbor ${peer.address} as ${toString peer.asn};
354 ${if peer.password != null then "password \"${peer.password}\";" else ""}
355 ${if v.direct == true then "direct;" else ""}
356 }
357 '';
358 in "${filters}\n${concatStringsSep "\n" (imap1 peer v.neighbors)}";
359
360 tablesFromProtoAF =
361 af: p: filter (el: el != null) (
362 mapAttrsToList (_: v: "${af} table ${v.table}${if af == "ipv4" then "4" else "6"};") p);
363 tablesFromProto = p: (tablesFromProtoAF "ipv4" p.v4) ++ (tablesFromProtoAF "ipv6" p.v6);
364 tables =
365 unique (
366 (tablesFromProto cfg.bgpSessions) ++
367 (tablesFromProto cfg.originate) ++
368 (tablesFromProto cfg.pipe) ++
369 (tablesFromProto cfg.ospf)
370 # TODO(q3k): also slurp in peer tables from pipes.
371 );
372 tablesRender = ''
373 ${concatStringsSep "\n" tables}
374 '';
375 tablesProgram = mapAttrsToList (n: _: n) (filterAttrs (n: v: v.program == true) cfg.tables);
376 tableProgram =
377 if (length tablesProgram) != 1 then
378 (abort "exactly one table must be set to be programmed")
379 else
380 (head tablesProgram);
381
382in {
383 options.hscloud.routing = {
384 enable = mkEnableOption "declarative routing";
385 routerID = mkOption {
386 type = types.str;
387 description = ''
388 Default Router ID for dynamic routing protocols, eg. IPv4 address from
389 loopback interface.
390 '';
391 };
392 asn = mkOption {
393 type = types.int;
394 description = "Default ASN for BGP.";
395 };
396 extra = mkOption {
397 type = types.lines;
398 description = "Extra configuration lines.";
399 };
400 bgpSessions = {
401 v4 = bgpSessionsType "ipv4";
402 v6 = bgpSessionsType "ipv6";
403 };
404 originate = {
405 v4 = originateType "ipv4";
406 v6 = originateType "ipv6";
407 };
Serge Bazanskid9a63652020-10-16 19:07:41 +0200408 static = {
409 v4 = staticType "ipv4";
410 v6 = staticType "ipv6";
411 };
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200412 pipe = {
413 v4 = pipeType "ipv4";
414 v6 = pipeType "ipv6";
415 };
416 ospf = {
417 v4 = ospfType "ipv4";
418 v6 = ospfType "ipv6";
419 };
420 tables = mkOption {
421 type = types.attrsOf (types.submodule {
422 options = {
423 program = mkOption {
424 type = types.bool;
425 default = false;
426 description = "This is the primary table programmed in to the kernel.";
427 };
428 programSourceV4 = mkOption {
429 type = types.nullOr types.str;
430 default = null;
431 description = "If set, programmed routes will have source set to this address.";
432 };
433 programSourceV6 = mkOption {
434 type = types.nullOr types.str;
435 default = null;
436 description = "If set, programmed routes will have source set to this address.";
437 };
438 };
439 });
440 description = "Routing table configuration.";
441 };
442 };
443
444 config = mkIf cfg.enable {
445 services.bird2.enable = true;
446 services.bird2.config = ''
447 log syslog all;
448 debug protocols { states, interfaces, events }
449
450 router id ${cfg.routerID};
451
452 ${cfg.extra}
453
454 ${tablesRender}
455
456 protocol device {
457 scan time 10;
458 };
459
460 protocol kernel kernel_v4 {
461 scan time 60;
462 ipv4 {
463 table ${tableProgram}4;
464 import none;
465 export filter {
466 ${let src = cfg.tables."${tableProgram}".programSourceV4; in if src != null then ''
467 krt_prefsrc = ${src};
468 '' else ""}
469 accept;
470 };
471 };
472 }
473 protocol kernel kernel_v6 {
474 scan time 60;
475 ipv6 {
476 table ${tableProgram}6;
477 import none;
478 export filter {
479 ${let src = cfg.tables."${tableProgram}".programSourceV6; in if src != null then ''
480 krt_prefsrc = ${src};
481 '' else ""}
482 accept;
483 };
484 };
485 };
486
487 ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv4") cfg.bgpSessions.v4)}
488 ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv6") cfg.bgpSessions.v6)}
489 ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv4") cfg.originate.v4)}
490 ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv6") cfg.originate.v6)}
Serge Bazanskid9a63652020-10-16 19:07:41 +0200491 ${concatStringsSep "\n" (mapAttrsToList (staticRender "ipv4") cfg.static.v4)}
492 ${concatStringsSep "\n" (mapAttrsToList (staticRender "ipv6") cfg.static.v6)}
Serge Bazanski6abe4fa2020-10-03 00:18:34 +0200493 ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv4") cfg.pipe.v4)}
494 ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv6") cfg.pipe.v6)}
495 ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv4") cfg.ospf.v4)}
496 ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv6") cfg.ospf.v6)}
497
498 '';
499 };
500}