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)}
+
+    '';
+  };
+}