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