bgpwtf/machines: init edge01.waw
This configures our WAW edge router using NixOS. This replaces our
previous Ubuntu installation.
Change-Id: Ibd72bde66ec413164401da407c5b268ad83fd3af
diff --git a/bgpwtf/machines/modules/routing.nix b/bgpwtf/machines/modules/routing.nix
new file mode 100644
index 0000000..e87ab9d
--- /dev/null
+++ b/bgpwtf/machines/modules/routing.nix
@@ -0,0 +1,440 @@
+# Declarative routing configuration. Usees BIRD2 underneath.
+#
+# The mapping from declarative configuration to BIRD is quite straightforward,
+# however, we take a few liberties:
+# - we introduce an 'originate' protocol for originating prefixes (using the
+# static protocol).
+# - routing tables in the configuration are referred to by a common name for
+# IPv4 and IPv4 - while in BIRD, two tables are created (suffixed by '4' and
+# '6', following the two default 'master4' and 'master6' tables).
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+ cfg = config.hscloud.routing;
+
+ originateType = af: let
+ v4 = af == "ipv4";
+ v6 = af == "ipv6";
+ pretty = if v4 then "IPv4" else "IPv6";
+ in with types; mkOption {
+ type = attrsOf (submodule {
+ options = {
+ table = mkOption {
+ type = nullOr str;
+ description = "BIRD table to which session should be connected.";
+ };
+ address = mkOption {
+ type = str;
+ description = "Address part of prefix to announce.";
+ };
+ prefixLength = mkOption {
+ type = int;
+ description = "Prefix length to announce.";
+ };
+ };
+ });
+ default = {};
+ description = "${pretty} prefixes to unconditionally inject into a table.";
+ };
+
+ originateRender = af: n: v: let
+ name = "static_originate_${af}_${n}";
+ ip = if af == "ipv4" then "4" else "6";
+ in ''
+ protocol static ${name} {
+ ${af} {
+ table ${v.table}${ip};
+ import all;
+ export none;
+ };
+
+ route ${v.address}/${toString v.prefixLength} blackhole;
+ }
+ '';
+
+ ospfType = af: let
+ v4 = af == "ipv4";
+ v6 = af == "ipv6";
+ pretty = if v4 then "IPv4" else "IPv6";
+ ospf = if v4 then "OSPFv2" else "OSPFv3";
+ in with types; mkOption {
+ type = attrsOf (submodule {
+ options = {
+ table = mkOption {
+ type = nullOr str;
+ description = "BIRD table to which session should be connected.";
+ };
+ filterIn = mkOption {
+ type = str;
+ default = "accept;";
+ description = "BIRD filter definition for received routes.";
+ };
+ filterOut = mkOption {
+ type = str;
+ default = "accept;";
+ description = "BIRD filter definition for sent routes.";
+ };
+ area = mkOption {
+ type = attrsOf (submodule {
+ options = {
+ interfaces = mkOption {
+ type = attrsOf (submodule {
+ options = {
+ cost = mkOption {
+ type = int;
+ default = 10; # 1Gbps
+ description = "Interface cost (10e9/iface_speed_in_bps).";
+ };
+ type = mkOption {
+ type = enum ["bcast" "nbma" "ptp" "ptmp"];
+ description = "Interface type (dictates BIRD behaviour).";
+ };
+ stub = mkOption {
+ type = bool;
+ default = false;
+ description = "Interface is stub (do not HELLO).";
+ };
+ };
+ });
+ description = "Interface configuration";
+ };
+ };
+ });
+ description = "Area configuration";
+ };
+ };
+ });
+ default = {};
+ description = "${ospf} configuration";
+ };
+
+ ospfRender = af: n: v: let
+ v4 = af == "ipv4";
+ v6 = af == "ipv6";
+ ip = if v4 then "4" else "6";
+ name = "ospf_${af}_${n}";
+
+ interfaces = mapAttrsToList (iface: ifaceConfig: ''
+ interface "${iface}" {
+ type ${ifaceConfig.type};
+ cost ${toString ifaceConfig.cost};
+ ${if ifaceConfig.stub then "stub yes;" else ""}
+ };
+ '');
+ areas = mapAttrsToList (area: areaConfig: ''
+ area ${area} {
+ ${concatStringsSep "\n" (interfaces areaConfig.interfaces)}
+ };
+ '') v.area;
+ in ''
+ filter ${name}_in {
+ ${v.filterIn}
+ };
+ filter ${name}_out {
+ ${v.filterOut}
+ };
+ protocol ospf ${if v4 then "v2" else "v3"} ${name} {
+ ${af} {
+ table ${v.table}${ip};
+ import filter ${name}_in;
+ export filter ${name}_out;
+ };
+ ${concatStringsSep "\n" areas}
+ }
+ '';
+
+ pipeType = af: with types; mkOption {
+ type = attrsOf (submodule {
+ options = {
+ table = mkOption {
+ type = nullOr str;
+ description = "BIRD table to which session should be connected.";
+ };
+ peerTable = mkOption {
+ type = nullOr str;
+ description = "BIRD 'remote' table to which session should be connected.";
+ };
+ filterIn = mkOption {
+ type = str;
+ default = "accept";
+ description = "BIRD filter definition for routes received from peerTable";
+ };
+ filterOut = mkOption {
+ type = str;
+ default = "reject;";
+ description = "BIRD filter definition for routes sent to peerTable";
+ };
+ };
+ });
+ default = {};
+ description = "${pretty} prefixes to pipe from one table to another.";
+ };
+
+ pipeRender = af: n: v: let
+ name = "pipe_${af}_${n}";
+ v4 = af == "ipv4";
+ v6 = af == "ipv6";
+ ip = if v4 then "4" else "6";
+ in ''
+ filter ${name}_in {
+ ${v.filterIn}
+ };
+ filter ${name}_out {
+ ${v.filterOut}
+ };
+ protocol pipe ${name} {
+ table ${v.table}${ip};
+ peer table ${v.peerTable}${ip};
+ import filter ${name}_in;
+ export filter ${name}_out;
+ }
+ '';
+
+ bgpSessionsType = af: let
+ v4 = af == "ipv4";
+ v6 = af == "ipv6";
+ pretty = if v4 then "IPv4" else "IPv6";
+ in with types; mkOption {
+ type = attrsOf (submodule {
+ options = {
+ description = mkOption {
+ type = str;
+ description = "Session description (for BIRD).";
+ };
+ table = mkOption {
+ type = nullOr str;
+ description = "BIRD table to which session should be connected.";
+ };
+ local = mkOption {
+ type = str;
+ description = "${pretty} address of this router.";
+ };
+ asn = mkOption {
+ type = int;
+ description = "ASN of local router - will default to hscloud.routing.asn.";
+ default = cfg.asn;
+ };
+ prepend = mkOption {
+ type = int;
+ default = 0;
+ description = "How many times to prepend this router's ASN on the link.";
+ };
+ pref = mkOption {
+ type = int;
+ default = 100;
+ description = "Preference (BGP local_pref) for routes from this session.";
+ };
+ direct = mkOption {
+ type = nullOr bool;
+ default = null;
+ };
+ filterIn = mkOption {
+ type = str;
+ default = "accept;";
+ description = "BIRD filter definition for received routes.";
+ };
+ filterOut = mkOption {
+ type = str;
+ default = "accept;";
+ description = "BIRD filter definition for sent routes.";
+ };
+ neighbors = mkOption {
+ type = listOf (submodule {
+ options = {
+ address = mkOption {
+ type = str;
+ description = "${pretty} address of neighbor.";
+ };
+ asn = mkOption {
+ type = int;
+ description = "ASN of neighbor.";
+ };
+ password = mkOption {
+ type = nullOr str;
+ default = null;
+ description = "BGP TCP MD5 secret.";
+ };
+ };
+ });
+ description = "BGP Neighbor configuration";
+ };
+ };
+ });
+ default = {};
+ description = "BGP Sesions for ${pretty}";
+ };
+
+ bgpSessionRender = af: n: v: let
+ name = "bgp_${af}_${n}";
+ ip = if af == "ipv4" then "4" else "6";
+ filters = ''
+ filter ${name}_in {
+ if bgp_path.len > 64 then reject;
+ bgp_local_pref = ${toString v.pref};
+ ${v.filterIn}
+ }
+
+ filter ${name}_out {
+ ${if v.prepend > 0 then
+ (concatStringsSep "\n"
+ (map (_: "bgp_path.prepend(${toString v.asn});") (range 0 (v.prepend - 1)))
+ )
+ else ""}
+ ${v.filterOut}
+ }
+ '';
+ peer = ix: peer: ''
+ protocol bgp ${name}_${toString ix} {
+ description "${v.description}";
+
+ ${af} {
+ table ${v.table}${ip};
+ import filter ${name}_in;
+ export filter ${name}_out;
+ };
+
+ local ${v.local} as ${toString v.asn};
+ neighbor ${peer.address} as ${toString peer.asn};
+ ${if peer.password != null then "password \"${peer.password}\";" else ""}
+ ${if v.direct == true then "direct;" else ""}
+ }
+ '';
+ in "${filters}\n${concatStringsSep "\n" (imap1 peer v.neighbors)}";
+
+ tablesFromProtoAF =
+ af: p: filter (el: el != null) (
+ mapAttrsToList (_: v: "${af} table ${v.table}${if af == "ipv4" then "4" else "6"};") p);
+ tablesFromProto = p: (tablesFromProtoAF "ipv4" p.v4) ++ (tablesFromProtoAF "ipv6" p.v6);
+ tables =
+ unique (
+ (tablesFromProto cfg.bgpSessions) ++
+ (tablesFromProto cfg.originate) ++
+ (tablesFromProto cfg.pipe) ++
+ (tablesFromProto cfg.ospf)
+ # TODO(q3k): also slurp in peer tables from pipes.
+ );
+ tablesRender = ''
+ ${concatStringsSep "\n" tables}
+ '';
+ tablesProgram = mapAttrsToList (n: _: n) (filterAttrs (n: v: v.program == true) cfg.tables);
+ tableProgram =
+ if (length tablesProgram) != 1 then
+ (abort "exactly one table must be set to be programmed")
+ else
+ (head tablesProgram);
+
+in {
+ options.hscloud.routing = {
+ enable = mkEnableOption "declarative routing";
+ routerID = mkOption {
+ type = types.str;
+ description = ''
+ Default Router ID for dynamic routing protocols, eg. IPv4 address from
+ loopback interface.
+ '';
+ };
+ asn = mkOption {
+ type = types.int;
+ description = "Default ASN for BGP.";
+ };
+ extra = mkOption {
+ type = types.lines;
+ description = "Extra configuration lines.";
+ };
+ bgpSessions = {
+ v4 = bgpSessionsType "ipv4";
+ v6 = bgpSessionsType "ipv6";
+ };
+ originate = {
+ v4 = originateType "ipv4";
+ v6 = originateType "ipv6";
+ };
+ pipe = {
+ v4 = pipeType "ipv4";
+ v6 = pipeType "ipv6";
+ };
+ ospf = {
+ v4 = ospfType "ipv4";
+ v6 = ospfType "ipv6";
+ };
+ tables = mkOption {
+ type = types.attrsOf (types.submodule {
+ options = {
+ program = mkOption {
+ type = types.bool;
+ default = false;
+ description = "This is the primary table programmed in to the kernel.";
+ };
+ programSourceV4 = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "If set, programmed routes will have source set to this address.";
+ };
+ programSourceV6 = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "If set, programmed routes will have source set to this address.";
+ };
+ };
+ });
+ description = "Routing table configuration.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services.bird2.enable = true;
+ services.bird2.config = ''
+ log syslog all;
+ debug protocols { states, interfaces, events }
+
+ router id ${cfg.routerID};
+
+ ${cfg.extra}
+
+ ${tablesRender}
+
+ protocol device {
+ scan time 10;
+ };
+
+ protocol kernel kernel_v4 {
+ scan time 60;
+ ipv4 {
+ table ${tableProgram}4;
+ import none;
+ export filter {
+ ${let src = cfg.tables."${tableProgram}".programSourceV4; in if src != null then ''
+ krt_prefsrc = ${src};
+ '' else ""}
+ accept;
+ };
+ };
+ }
+ protocol kernel kernel_v6 {
+ scan time 60;
+ ipv6 {
+ table ${tableProgram}6;
+ import none;
+ export filter {
+ ${let src = cfg.tables."${tableProgram}".programSourceV6; in if src != null then ''
+ krt_prefsrc = ${src};
+ '' else ""}
+ accept;
+ };
+ };
+ };
+
+ ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv4") cfg.bgpSessions.v4)}
+ ${concatStringsSep "\n" (mapAttrsToList (bgpSessionRender "ipv6") cfg.bgpSessions.v6)}
+ ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv4") cfg.originate.v4)}
+ ${concatStringsSep "\n" (mapAttrsToList (originateRender "ipv6") cfg.originate.v6)}
+ ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv4") cfg.pipe.v4)}
+ ${concatStringsSep "\n" (mapAttrsToList (pipeRender "ipv6") cfg.pipe.v6)}
+ ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv4") cfg.ospf.v4)}
+ ${concatStringsSep "\n" (mapAttrsToList (ospfRender "ipv6") cfg.ospf.v6)}
+
+ '';
+ };
+}