SwordArMor

Vérification RPKI avec routinator, bird et IOS-XE

Quand on fait partie de la DFZ, on peut en gros annoncer n’importe quel préfixe tant que ça passe les filtres de nos pairs, et on est censé leur faire plus ou moins confiance. Sauf que croire encore à ce système de la confiance en 2020, c’est aussi croire aux bisounours. Il est bien plus sûr de faire des filtres qui taillent dans le vif et de vérifier que les préfixes que l’on reçoit viennent du bon AS. Pour ça, il est possible de signer ses annonces avec RPKI (ou de faire signer par son LIR si l’on n’en est pas un) et aussi de vérifier les signatures RPKI sur les préfixes que l’on reçoit. Les deux étant complètement décorrélés, un peu à la manière de DNSSEC : on peut très bien vérifier les signatures DNSSEC mais ne pas signer son domaine et inversement.

Comme les signatures RPKI sont cryptographiques et que les CPUs des routeurs ne sont pas des foudres de guerre (l’intérêt des routeurs c’est de profiter des ASIC/FPGA, pas de router via le CPU), il existe RPKI-RTR. C’est un protocole qui permet de faire les vérifications sur un serveur (avec un CPU puissant, donc) puis d’envoyer les données pré-machées au routeur pour qu’il puisse les traiter avec très peu de CPU.

J’ai testé deux implémentations : rpki-client d’OpenBSD et routinator de NLnetLabs. Comme je suis feignant, j’ai d’abord regardé si l’une des deux étaient déjà packagée pour gentoo. Ce fut un échec total. À vue de nez, rpki-client me semblait plus simple, j’ai donc commencé par cela, mais j’ai rencontré plusieurs soucis :

  1. La numérotation des versions est un peu étrange, 0_3_0 au lieu de 0.3.0, que je règle en créant une variable $VERSION tirée de $PV : VERSION="VERSION_${PV//./_}", que je ré-utilise donc en lieu et place de $PV dans le reste de l’ebuild
  2. La liste des préfixes pour bird était vide : bug #12
  3. Encore plus gênant, je n’ai jamais réussi à importer la liste générée dans bird, pour cause d’erreur de syntaxe dans le fichier : /var/db/rpki-client/bird:1:1 syntax error, unexpected CF_SYM_UNDEFINED. La première ligne étant roa table roa {. D’après la documentation, le symbole roa est censé se trouver dans un protocole RPKI, sauf que ce protocole demande un serveur RPKI-RTR.
Il était donc assez clair que cette implémentation n’était pas la bonne approche pour mon besoin. De plus, elle n’est pas compatible avec IOS-XE, et j’ai également envie de vérifier RPKI dessus et pas seulement sur les bird.

Je me suis donc lancé dans le packaging de routinator. Comme il est écrit en rust, j’ai utilisé cargo-ebuild afin qu’il me génère un ebuild avec tous les dépendances rust qui ne sont pas dans l’arbre de portage. Et là premier souci : la façon d’installer les binaires a changée.

J’ai donc attendu un peu que la fonction soit mise à jour, puis j’ai pu l’installer. Là je me suis rendu compte que je n’avais pas fait de fichier de configuration ni de script d’init, je les ai donc écris et intégrés dans mon ebuild. À ce stade j’ai maintenant un routinator qui démarre, récupère les TALs et écoute sur le port standard. Il est maintenant temps de passer à l’intégration avec les routeurs.

Pour le cas de bird, la configuration se présente comme suit avec un serveur sur mon infrastructure propre et un autre sur celle de grifon :

roa4 table r4;
roa6 table r6;

protocol rpki rpki_msi {
        roa4 { table r4; };
        roa6 { table r6; };

        remote "msi.no.swordarmor.fr";
}

protocol rpki rpki_conan {
        roa4 { table r4; };
        roa6 { table r6; };

        remote "conan.grifon.fr";
}
Ici, je crée deux tables (r4 et r6) et je demande à mes protocoles de mettre les préfixes RPKI dedans.

Et là, en rechargeant la configuration, je tombe sur une erreur de syntaxe du même type que pour rpki-client. C’est pas très gentil… La vraie raison est que bird a besoin de la libssh pour activer le protocole RPKI. Et ce, même si on utilise le transport TCP classique. C’est un bug connu qui sera corrigé. Il se trouvait donc que l’ebuild de bird n’avait pas cette option. J’ai donc ajouté le USE flag libssh, recompilé et redémarré bird.

Maintenant, les nouveaux protocoles apparaissent puis… rien. Je me suis donc fendu d’un sur la ML de bird et Ondrej Zajicek m’a dirigé vers un commit de leur git qui corrige ce bug. J’ai donc généré le .patch entre ce commit et celui du tag de la version 2.0.7, je l’ai ajouté à mon ebuild, puis j’ai de nouveau recompilé et relancé mon bird. (quand on aime on ne compte pas) Et là, alléluia, j’ai bien les préfixes signés dans mon bird \o/

Il est maintenant temps de passer aux choses sérieuses : le filtrage des préfixes invalides. Pour ce faire, j’ai repris la configuration du LG du NLnog ring que j’ai adaptée à mon besoin :

function check_import_ipv4_rs()
[…]
        # scrub Origin Validation State Extended Community
        bgp_ext_community.delete((unknown 0x4300, 0, 0));
        bgp_ext_community.delete((unknown 0x4300, 0, 1));
        bgp_ext_community.delete((unknown 0x4300, 0, 2));

        # set RPKI Origin Validation State Extended Community
        case roa_check(r4, net, bgp_path.last_nonaggregated) {
                ROA_VALID:
                        /* add rfc8097 marker to routes for which a valid matching ROA exists */
                        bgp_ext_community.add((unknown 0x4300, 0, 0));
                        igp_metric = 10;
                ROA_INVALID:
                        bgp_ext_community.add((unknown 0x4300, 0, 2));
                        return false;
                else:
                        /* add rfc8097 marker to routes for which no covering ROA exists */
                        bgp_ext_community.add((unknown 0x4300, 0, 1));
                        igp_metric = 20;
        }

        return generic_check_import();
}
function check_import_ipv6_rs()
[…]
        # scrub Origin Validation State Extended Community
        bgp_ext_community.delete((unknown 0x4300, 0, 0));
        bgp_ext_community.delete((unknown 0x4300, 0, 1));
        bgp_ext_community.delete((unknown 0x4300, 0, 2));

        # set RPKI Origin Validation State Extended Community
        case roa_check(r6, net, bgp_path.last_nonaggregated) {
                ROA_VALID:
                        /* add rfc8097 marker to routes for which a valid matching ROA exists */
                        bgp_ext_community.add((unknown 0x4300, 0, 0));
                        igp_metric = 10;
                ROA_INVALID:
                        bgp_ext_community.add((unknown 0x4300, 0, 2));
                        return false;
                else:
                        /* add rfc8097 marker to routes for which no covering ROA exists */
                        bgp_ext_community.add((unknown 0x4300, 0, 1));
                        igp_metric = 20;
        }

        return generic_check_import();
}
La fonction roa_check est déjà intégrée dans bird, il n’y a pas besoin de la déclarer. De cette façon, je rejette les annonces invalides, je marque chaque préfixe en fonction de son type de validité et, en modifiant la métrique IGP, je préfère une annonce valide à une annonce non déclarée si le chemin BGP et la MED sont égales entre les deux annonces. Ce cas devrait arriver très rarement, uniquement quand un préfixe vient d’être signé et qu’une route n’a pas encore été mise à jour.

Si jamais vous avez une route par défaut, vous avez certainement envie de marquer la route comme injoignable (dest = RTD_UNREACHABLE) pour éviter d’utiliser la route par défaut si cette route plus spécifique n’est pas dans la FIB.

Pour le cas d’IOS XE, c’est beaucoup plus simple, il suffit de quelques lignes de configuration :

conf t
 router bgp 204092
  bgp rpki server tcp 2A00:5884::8 port 323 refresh 600
  bgp rpki server tcp 2A0E:F42::1 port 323 refresh 600
  address-family ipv4
   neighbor GRIF-iBGP-IPv4 send-community both
   neighbor GRIF-iBGP-IPv4 announce rpki state
  address-family ipv6
   neighbor GRIF-iBGP-IPv6 send-community both
   neighbor GRIF-iBGP-IPv6 announce rpki state
Nous sommes obligés de mettre l’IP directement, c’est spécifié dans la documentation. Les options sur les neighbors permettent de garder la communauté correspondant à la validité RPKI lors de l’export vers les autres routeurs du réseau.