528 lines
23 KiB
Nix
528 lines
23 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
let
|
|
inherit (lib)
|
|
attrValues
|
|
concatMapStringsSep
|
|
concatStrings
|
|
concatStringsSep
|
|
filterAttrs
|
|
flatten
|
|
hasAttr
|
|
hasPrefix
|
|
listToAttrs
|
|
mapAttrs
|
|
mapAttrs'
|
|
mapAttrsToList
|
|
mkEnableOption
|
|
mkIf
|
|
mkOption
|
|
nameValuePair
|
|
optional
|
|
optionalAttrs
|
|
optionalString
|
|
range
|
|
types
|
|
;
|
|
|
|
cfg = config.hedge.dn42;
|
|
wireguardKeyType = with lib; types.addCheck types.str (v: (stringLength v) > 40);
|
|
in
|
|
{
|
|
options.hedge.dn42 = {
|
|
enable = mkEnableOption "enable dn42 configuration";
|
|
enableDebugLogging = mkEnableOption "dn42 bgp logging";
|
|
bgp = mkOption {
|
|
type = types.submodule {
|
|
options = {
|
|
asn = mkOption { type = types.ints.unsigned; };
|
|
routerId = mkOption { type = types.str; };
|
|
staticRoutes = mkOption {
|
|
type = types.submodule {
|
|
options = {
|
|
ipv4 = mkOption { type = types.listOf types.str; default = [ ]; };
|
|
ipv6 = mkOption { type = types.listOf types.str; default = [ ]; };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
peers = mkOption
|
|
{
|
|
type = (
|
|
types.attrsOf (
|
|
types.submodule {
|
|
options = {
|
|
tunnelType = mkOption {
|
|
type = types.nullOr (types.enum [ "wireguard" ]);
|
|
description = "tunnel technology used";
|
|
};
|
|
mtu = mkOption {
|
|
type = types.nullOr types.ints.unsigned;
|
|
default = null;
|
|
description = "mtu on the interface";
|
|
};
|
|
interfaceName = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
};
|
|
wireguardConfig = mkOption {
|
|
type = types.submodule {
|
|
options = {
|
|
localPort = mkOption { type = types.ints.unsigned; };
|
|
remoteEndpoint = mkOption { type = types.nullOr types.str; };
|
|
remotePort = mkOption { type = types.port; };
|
|
remotePublicKey = mkOption { type = wireguardKeyType; };
|
|
};
|
|
};
|
|
};
|
|
bgp = mkOption {
|
|
type = types.submodule {
|
|
options = {
|
|
asn = mkOption { type = types.ints.unsigned; };
|
|
announce = mkOption { type = types.enum [ "all" "own" ]; default = "own"; };
|
|
accept = mkOption { type = types.enum [ "all" "own" ]; default = "own"; };
|
|
local_pref = mkOption { type = types.ints.unsigned; };
|
|
#export_med = mkOption { type = types.nullOr types.ints.unsigned; default = null; };
|
|
export_prepend = mkOption { type = types.ints.unsigned; default = 0; };
|
|
import_prepend = mkOption { type = types.ints.unsigned; default = 0; };
|
|
import_limit = mkOption { type = types.nullOr types.ints.unsigned; default = null; };
|
|
import_reject = mkOption { type = types.bool; default = false; };
|
|
export_reject = mkOption { type = types.bool; default = false; };
|
|
multi_protocol = mkOption { type = types.bool; default = true; };
|
|
export_additional_asns = mkOption { type = types.listOf types.int; default = [ ]; };
|
|
ipv6 = mkOption {
|
|
default = { };
|
|
type = types.submodule {
|
|
options = {
|
|
extended_next_hop = mkOption { type = types.bool; default = false; };
|
|
};
|
|
};
|
|
};
|
|
|
|
ipv4 = mkOption {
|
|
default = { };
|
|
type = types.submodule {
|
|
options = {
|
|
next_hop_self = mkOption { type = types.bool; default = true; };
|
|
next_hop_address = mkOption { type = types.nullOr types.str; default = null; };
|
|
gateway_recursive = mkOption { type = types.bool; default = true; };
|
|
extended_next_hop = mkOption { type = types.bool; default = false; };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
addresses = mkOption {
|
|
type = types.submodule {
|
|
options = {
|
|
ipv6 = mkOption {
|
|
default = null;
|
|
type = types.nullOr (
|
|
types.submodule {
|
|
options = {
|
|
local_address = mkOption { type = types.str; };
|
|
remote_address = mkOption { type = types.str; };
|
|
prefix_length = mkOption { type = types.ints.unsigned; default = 128; };
|
|
};
|
|
}
|
|
);
|
|
};
|
|
ipv4 = mkOption {
|
|
default = null;
|
|
type = types.nullOr (
|
|
types.submodule {
|
|
options = {
|
|
local_address = mkOption { type = types.str; };
|
|
remote_address = mkOption { type = types.str; };
|
|
prefix_length = mkOption { type = types.ints.unsigned; default = 32; };
|
|
};
|
|
}
|
|
);
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
default = { };
|
|
} // {
|
|
check = v: (if v.tunnelType == "wireguard" then hasAttr "wireguardConfig" v else true);
|
|
};
|
|
};
|
|
|
|
config =
|
|
let
|
|
wireguardPeers = filterAttrs (n: v: v.tunnelType == "wireguard" && v ? wireguardConfig) cfg.peers;
|
|
|
|
wireguardInterfaceNameMapping = mapAttrs (_: v: v.interfaceName) (filterAttrs (n: v: hasPrefix "wg-" v.interfaceName) config.hedge.wireguardBackbone.peers);
|
|
wireguardInterfaceNames = attrValues wireguardInterfaceNameMapping;
|
|
|
|
interfaceNames = wireguardInterfaceNames ++ (mapAttrsToList (_: p: p.interfaceName) (filterAttrs (_: p: p.interfaceName != null) cfg.peers));
|
|
interfaceNameMapping = wireguardInterfaceNameMapping;
|
|
|
|
|
|
bgpPeers =
|
|
lib.mapAttrsToList
|
|
(
|
|
name: v: {
|
|
inherit name;
|
|
interfaceName = if v.interfaceName != null then v.interfaceName else interfaceNameMapping."dn42_${name}";
|
|
inherit (v) bgp;
|
|
} // (
|
|
optionalAttrs (v.addresses.ipv4 != null) {
|
|
remoteV4 = v.addresses.ipv4.remote_address;
|
|
}
|
|
) // (
|
|
optionalAttrs (v.addresses.ipv6 != null) {
|
|
remoteV6 = v.addresses.ipv6.remote_address;
|
|
}
|
|
)
|
|
)
|
|
cfg.peers;
|
|
|
|
in
|
|
mkIf cfg.enable {
|
|
hedge.bird.enable = true;
|
|
|
|
environment.systemPackages = with pkgs; [ mtr tcpdump ];
|
|
|
|
boot.kernel.sysctl = {
|
|
"net.ipv6.all.default.forwarding" = 1;
|
|
} // (
|
|
listToAttrs (map (iface: nameValuePair "net.ipv4.conf.${iface}.forwarding" 1) interfaceNames)
|
|
);
|
|
networking.firewall = {
|
|
checkReversePath = false;
|
|
interfaces = lib.listToAttrs (
|
|
map
|
|
(
|
|
name: nameValuePair name {
|
|
allowedTCPPorts = [ 179 ];
|
|
}
|
|
)
|
|
interfaceNames
|
|
);
|
|
extraCommands = concatStringsSep "\n" (
|
|
flatten (
|
|
map
|
|
(
|
|
iiface: (
|
|
map
|
|
(
|
|
oiface:
|
|
if iiface != oiface then [
|
|
"iptables -A FORWARD -i ${iiface} -o ${oiface} -j ACCEPT"
|
|
"ip6tables -A FORWARD -i ${iiface} -o ${oiface} -j ACCEPT"
|
|
] else [ ]
|
|
)
|
|
interfaceNames
|
|
)
|
|
)
|
|
interfaceNames
|
|
)
|
|
);
|
|
extraStopCommands = concatStringsSep "\n" (
|
|
flatten (
|
|
map
|
|
(
|
|
iiface: (
|
|
map
|
|
(
|
|
oiface:
|
|
if iiface != oiface then [
|
|
"iptables -D FORWARD -i ${iiface} -o ${oiface} -j ACCEPT || :"
|
|
"ip6tables -D FORWARD -i ${iiface} -o ${oiface} -j ACCEPT || :"
|
|
] else [ ]
|
|
)
|
|
interfaceNames
|
|
)
|
|
)
|
|
interfaceNames
|
|
)
|
|
);
|
|
};
|
|
|
|
hedge.wireguardBackbone.peers = mapAttrs'
|
|
(
|
|
name: value: nameValuePair "dn42_${name}" (
|
|
{
|
|
inherit (value.wireguardConfig) localPort remoteEndpoint remotePort remotePublicKey;
|
|
remoteAddresses = (optional (value.addresses.ipv6 != null && value.addresses.ipv6 ? remote_address) value.addresses.ipv6.remote_address)
|
|
++ (optional (value.addresses.ipv4 != null && value.addresses.ipv4 ? remote_address) value.addresses.ipv4.remote_address);
|
|
localAddresses = optional (value.addresses.ipv6 != null && value.addresses.ipv6 ? local_address)
|
|
(
|
|
if (value.addresses.ipv6.prefix_length != 128) then
|
|
"${value.addresses.ipv6.local_address}/${toString value.addresses.ipv6.prefix_length}"
|
|
else
|
|
{
|
|
local = "${value.addresses.ipv6.local_address}/128";
|
|
peer = "${toString value.addresses.ipv6.remote_address}/128";
|
|
}
|
|
)
|
|
++ (optional (value.addresses.ipv4 != null && value.addresses.ipv4 ? local_address) "${value.addresses.ipv4.local_address}/${toString value.addresses.ipv4.prefix_length}");
|
|
} // optionalAttrs (value.mtu != null) {
|
|
inherit (value) mtu;
|
|
}
|
|
)
|
|
)
|
|
wireguardPeers;
|
|
|
|
services.bird2.config = ''
|
|
#
|
|
# DN42 peering configuration
|
|
#
|
|
|
|
ipv4 table dn42_v4;
|
|
ipv6 table dn42_v6;
|
|
|
|
${optionalString (interfaceNames != [ ]) ''
|
|
protocol direct dn42_direct {
|
|
interface ${concatMapStringsSep ", " (iface: "\"${iface}\"") interfaceNames};
|
|
}
|
|
''}
|
|
|
|
protocol static dn42_static_v4 {
|
|
ipv4 { table dn42_v4; };
|
|
route 172.20.0.0/14 blackhole; # summary route so we only route traffic where we have more specifics
|
|
${concatMapStringsSep "\n" (net: "route ${net} blackhole;") cfg.bgp.staticRoutes.ipv4}
|
|
};
|
|
|
|
protocol static dn42_static_v6 {
|
|
ipv6 { table dn42_v6; };
|
|
route fd00::/8 blackhole; # summary route so we only route traffic where we have more specifics
|
|
${concatMapStringsSep "\n" (net: "route ${net} blackhole;") cfg.bgp.staticRoutes.ipv6}
|
|
};
|
|
|
|
function dn42_is_valid_prefix (prefix n) {
|
|
case n.type {
|
|
NET_IP4: if n ~ [
|
|
172.20.0.0/14{21,29}, # dn42
|
|
172.20.0.0/24{28,32}, # dn42 Anycast
|
|
172.21.0.0/24{28,32}, # dn42 Anycast
|
|
172.22.0.0/24{28,32}, # dn42 Anycast
|
|
172.23.0.0/24{28,32}, # dn42 Anycast
|
|
#172.31.0.0/16+, # ChaosVPN
|
|
#10.100.0.0/14+, # ChaosVPN
|
|
#10.127.0.0/16{16,32}, # neonetwork
|
|
10.0.0.0/8{15,24} # Freifunk.net
|
|
] then return true;
|
|
NET_IP6: if n ~ [ fd00::/8{40,64} ] then return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function dn42_is_own_prefix(prefix n) {
|
|
case n.type {
|
|
NET_IP4: if n ~ [ ${concatMapStringsSep ",\n" (net: "${net}+") cfg.bgp.staticRoutes.ipv4} ] then return true;
|
|
NET_IP6: if n ~ [ ${concatMapStringsSep ",\n" (net: "${net}+") cfg.bgp.staticRoutes.ipv6} ] then return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
roa4 table dn42_roa_v4;
|
|
roa6 table dn42_roa_v6;
|
|
|
|
protocol static {
|
|
roa4 { table dn42_roa_v4; };
|
|
include "${pkgs.dn42-roa.roa4}";
|
|
};
|
|
|
|
protocol static {
|
|
roa6 { table dn42_roa_v6; };
|
|
include "${pkgs.dn42-roa.roa6}";
|
|
};
|
|
|
|
function dn42_roa_check(prefix n; bgppath p) {
|
|
case n.type {
|
|
NET_IP4: return (roa_check(dn42_roa_v4, net, p.last) = ROA_VALID);
|
|
NET_IP6: return (roa_check(dn42_roa_v6, net, p.last) = ROA_VALID);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protocol pipe dn42_v4_pipe {
|
|
peer table master4;
|
|
table dn42_v4;
|
|
export all;
|
|
import none;
|
|
}
|
|
|
|
protocol pipe dn42_v6_pipe {
|
|
peer table master6;
|
|
table dn42_v6;
|
|
export all;
|
|
import none;
|
|
}
|
|
|
|
${lib.concatMapStringsSep "\n\n"
|
|
(
|
|
peer: ''
|
|
#
|
|
# Peer: ${peer.name}
|
|
# Remote ASN: ${toString peer.bgp.asn}
|
|
#
|
|
|
|
filter dn42_${peer.name}_import {
|
|
${optionalString peer.bgp.import_reject "reject \"rejecting all import prefixes\";"}
|
|
if !dn42_is_valid_prefix(net) then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
print "Not a valid DN42 prefix from asn ${toString peer.bgp.asn} net:" , net, " bgp_path: ", bgp_path;
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "Not a valid dn42 prefix"''};
|
|
}
|
|
${optionalString (peer.bgp.asn != cfg.bgp.asn)
|
|
# eBGP isn't allowed to annouce me my own prefixes
|
|
''
|
|
|
|
if bgp_path.first != ${toString peer.bgp.asn} then reject "Not accepting spoofed AS path";
|
|
|
|
${optionalString (peer.bgp.accept == "own") ''
|
|
if delete(bgp_path, [${toString peer.bgp.asn}]).len > 0 then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
print "rejecting prefix that isn't from the peer asn ${toString peer.bgp.asn}", " net: ", net, " bgp_path: ", bgp_path;
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "Not from peer ASN";''};
|
|
}
|
|
#if bgp_path.len > 1 && bgp_path.last != bgp_path.first then reject "Only accepting paths originating at the peer as";
|
|
''}
|
|
|
|
if dn42_is_own_prefix(net) then reject "Not accepting own prefix from eBGP peer.";
|
|
if filter(bgp_path, [${toString cfg.bgp.asn}]).len > 0 then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
print "Not accepting paths from my own ASN via eBGP from asn: ${toString peer.bgp.asn} net: ", net, " bgp_path: ", bgp_path;
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "Not accepting my own path via eBGP"''};
|
|
}
|
|
if !dn42_roa_check(net, bgp_path) then {
|
|
printn "DN42 ROA check failed for ", net;
|
|
reject "DN42 ROA check failed";
|
|
}
|
|
|
|
${optionalString (peer.bgp.import_prepend != 0)
|
|
(concatStrings (map (x: "bgp_path.prepend(${toString cfg.bgp.asn});\n") (range 0 peer.bgp.import_prepend)))}
|
|
|
|
bgp_local_pref = ${toString peer.bgp.local_pref};
|
|
|
|
''}
|
|
accept;
|
|
}
|
|
filter dn42_${peer.name}_export {
|
|
${optionalString peer.bgp.export_reject "reject \"rejecting all export prefixes\";"}
|
|
|
|
# only propagate static and BGP routes.
|
|
if source != RTS_STATIC && source != RTS_BGP then reject${lib.optionalString cfg.enableDebugLogging '' "invalid route source"''};
|
|
|
|
${optionalString (peer.bgp.asn != cfg.bgp.asn && peer.bgp.announce == "own") ''
|
|
if source != RTS_STATIC && delete(bgp_path, [${toString cfg.bgp.asn}]).len > 0 then {
|
|
${optionalString (peer.bgp.export_additional_asns != []) ''
|
|
# export routes for other ASNs that get partial transit
|
|
if source = RTS_BGP && (
|
|
${concatMapStringsSep "||" (asn: "bgp_path.first = ${toString asn}") peer.bgp.export_additional_asns}
|
|
) then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
printn "Propagating route net: ", net, " source: ", source, " path: ", bgp_path, " for external asn ", bgp_path.first;
|
|
''}
|
|
accept;
|
|
}
|
|
''}
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
printn "Only propagating own routes. net: ", net, " source: ", source, " path: ", bgp_path, " deleted path: ", delete(bgp_path, [${toString cfg.bgp.asn}]);
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "Not one of my routes"''};
|
|
}
|
|
''}
|
|
|
|
if !dn42_is_valid_prefix(net) then reject "Not a valid DN42 prefix";
|
|
if proto !~ "dn42_*" then reject "Prefix is not from another dn42 protocol. Rejecting.";
|
|
${optionalString (peer.bgp.export_prepend != 0)
|
|
(concatStrings (map (x: "bgp_path.prepend(${toString cfg.bgp.asn});\n") (range 0 peer.bgp.export_prepend)))}
|
|
if source = RTS_BGP && !dn42_roa_check(net, bgp_path) && bgp_path.len > 0 then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
printn "DN42 ROA check failed for ", net;
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "DN42 ROA check failed";''};
|
|
} else if source = RTS_BGP && bgp_path.len = 0 && !dn42_roa_check(net, prepend(bgp_path, ${toString cfg.bgp.asn})) then {
|
|
${lib.optionalString cfg.enableDebugLogging ''
|
|
printn "DN42 ROA check failed for our net ", net;
|
|
''}
|
|
reject${lib.optionalString cfg.enableDebugLogging '' "Refusing to announce (our) prefix that isn't covered by a valid ROA"''};
|
|
}
|
|
|
|
accept;
|
|
}
|
|
|
|
template bgp dn42_${peer.name}_tpl {
|
|
local as ${toString cfg.bgp.asn};
|
|
graceful restart on;
|
|
graceful restart time 120;
|
|
interpret communities on;
|
|
enable extended messages on;
|
|
enable route refresh on;
|
|
med metric on;
|
|
direct;
|
|
#advertise ipv4 on;
|
|
|
|
ipv4 {
|
|
import table on;
|
|
export table on;
|
|
${optionalString peer.bgp.ipv4.gateway_recursive "gateway recursive;"}
|
|
table dn42_v4;
|
|
igp table master4;
|
|
add paths on;
|
|
import filter dn42_${peer.name}_import;
|
|
export filter dn42_${peer.name}_export;
|
|
import keep filtered on;
|
|
${lib.optionalString (peer.bgp.asn != cfg.bgp.asn && peer.bgp.ipv4.next_hop_self) ''
|
|
next hop self on;
|
|
''}
|
|
${lib.optionalString (peer.bgp.ipv4.next_hop_address != null) ''
|
|
next hop address ${peer.bgp.ipv4.next_hop_address};
|
|
''}
|
|
${optionalString peer.bgp.ipv4.extended_next_hop "extended next hop;"}
|
|
${optionalString (peer.bgp.import_limit != null) "import limit ${toString peer.bgp.import_limit} action block;"}
|
|
};
|
|
ipv6 {
|
|
import table on;
|
|
export table on;
|
|
table dn42_v6;
|
|
igp table master6;
|
|
add paths on;
|
|
import filter dn42_${peer.name}_import;
|
|
export filter dn42_${peer.name}_export;
|
|
import keep filtered on;
|
|
${optionalString peer.bgp.ipv6.extended_next_hop "extended next hop;"}
|
|
${lib.optionalString (peer.bgp.asn != cfg.bgp.asn) ''
|
|
next hop self on;
|
|
''}
|
|
${optionalString (peer.bgp.import_limit != null) "import limit ${toString peer.bgp.import_limit} action block;"}
|
|
};
|
|
}
|
|
|
|
${if peer.bgp.multi_protocol then ''
|
|
protocol bgp dn42_${peer.name} from dn42_${peer.name}_tpl {
|
|
neighbor ${peer.remoteV6} as ${toString peer.bgp.asn};
|
|
interface "${peer.interfaceName}";
|
|
};
|
|
'' else ''
|
|
protocol bgp dn42_${peer.name}_v4 from dn42_${peer.name}_tpl {
|
|
neighbor ${peer.remoteV4} as ${toString peer.bgp.asn};
|
|
interface "${peer.interfaceName}";
|
|
}
|
|
protocol bgp dn42_${peer.name}_v6 from dn42_${peer.name}_tpl {
|
|
#advertise ipv4 off;
|
|
neighbor ${peer.remoteV6} as ${toString peer.bgp.asn};
|
|
${optionalString (hasPrefix "fe80:" peer.remoteV6) ''
|
|
interface "${peer.interfaceName}";
|
|
''}
|
|
}
|
|
''}
|
|
''
|
|
)
|
|
bgpPeers}
|
|
'';
|
|
};
|
|
}
|