forked from hswaw/hscloud
Serge Bazanski
82fc1318e2
Change-Id: Ib36048a83641b62210ad0d63b7b7ecda999da542 Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1201 Reviewed-by: q3k <q3k@hackerspace.pl>
500 lines
14 KiB
Nix
500 lines
14 KiB
Nix
# 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;
|
|
|
|
staticType = 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.";
|
|
};
|
|
via = mkOption {
|
|
type = str;
|
|
description = "Target address for static route.";
|
|
};
|
|
};
|
|
});
|
|
default = {};
|
|
description = "${pretty} static routes to inject into a table.";
|
|
};
|
|
|
|
staticRender = af: n: v: let
|
|
name = "static_static_${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} via ${v.via};
|
|
}
|
|
'';
|
|
|
|
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).";
|
|
};
|
|
neighbors = mkOption {
|
|
type = listOf str;
|
|
};
|
|
};
|
|
});
|
|
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: let
|
|
neighbors = ''
|
|
neighbors {
|
|
${concatStringsSep "\n" (map (n: "${n};") ifaceConfig.neighbors)}
|
|
};
|
|
'';
|
|
in ''
|
|
interface "${iface}" {
|
|
type ${ifaceConfig.type};
|
|
cost ${toString ifaceConfig.cost};
|
|
${if ifaceConfig.stub then "stub yes;" else ""}
|
|
${if ifaceConfig.type == "ptmp" then neighbors 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";
|
|
};
|
|
static = {
|
|
v4 = staticType "ipv4";
|
|
v6 = staticType "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 (staticRender "ipv4") cfg.static.v4)}
|
|
${concatStringsSep "\n" (mapAttrsToList (staticRender "ipv6") cfg.static.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)}
|
|
|
|
'';
|
|
};
|
|
}
|