Aujourd'hui, nous allons voir comment utiliser RPKI+ROA avec BIRD et plus précisément comment prendre en compte les autorisations signées que sont les ROA comme éléments supplémentaires pour prendre une décision lors de la construction de la table de routage.
Table des matières
Si vous ne savez pas bien ce qu'est RPKI+ROA : sécuriser le routage sur Internet - RPKI+ROA.
On ne traitera pas de la manière de créer ses objets signés (ROA, Ghostbusters) quand on est opérateur ou LIR. Des pistes sont données dans le travail linké ci-dessus mais ça reste dans le cadre d'une maquette.
On se concentrera uniquement sur BIRD, une mise en pratique avec Quagga ayant déjà était réalisée dans le travail linké ci-dessus.
Pour sortir un peu du monde des maquettes, nous mettrons ça en pratique sur l'infra d'ARN, FAI associatif en Alsace. 🙂
« Pour s'amuser » dans le titre de ce billet signifie que ce que je vais vous présenter n'est pas la bonne manière de faire en production (exemples : programme additionnel, choix du cache-validateur, sécurité du dernier kilomètre, ...) et que ce billet rapporte juste une expérience, un délire, une envie de découvrir et de mettre en pratique, bref, un truc fait "for ze fun". Bref, ne suivez pas ce billet pour faire prendre des décisions de routing à des routeurs en production.
Bout d'essai
Nous allons créer une table destinée à stocker les VRP (les assertions « tel AS est autorisé à être à l'origine de tels préfixes » qui sont les contenus des ROA (VRP = Validated ROA Payload)) dans BIRD puis nous ferons prendre à BIRD une décision en fonction de la validité (ou non) d'une annonce BGP conformément à une assertion.
Dans bird6.conf (c'est pareil avec bird.conf, juste que c'est v4), on ajoute :
roa table testroa { roa 2001:660::/32 max 32 as 64501; # RENATER } |
On déclare donc une table des VRP nommée « testroa ». On la remplit avec une assertion : l'allocation v6 de RENATER doit être originée par l'AS 64501 (ASN faisant partie des ASN réservés à l'IANA pour faire des tests). On est d'accord que c'est faux (ASN de RENATER = 2200) mais, dans RPKI+ROA, les assertions font foi : pour BIRD, ça sera l'annonce BGP qu'il recevra qui sera incorrecte et non l'inverse.
Dans BIRD, l'opérateur « roa_check(
- ROA_UNKNOWN : aucun VRP ne couvre le préfixe annoncé.
- ROA_VALID : au moins un VRP couvre le préfixe annoncé et l'AS d'un VRP correspond à l'AS "d'origine" indiqué dans l'annonce BGP.
- ROA_INVALID : au moins un VRP couvre le préfixe annoncé mais aucun n'indique l'AS "d'origine" indiqué dans l'annonce BGP.
On peut donc utiliser roa_check() dans un filtre. Chez ARN, on a déjà un filtre en entrée de nos transitaires qui appelle plusieurs fonctions (une pour filtrer les martians, une pour forcer l'IP de sortie, ...). Voici le montage approximatif que l'on aura :
function verif_roa_test() { # Nos manipulations avec les ROA. } filter cleaner { if avoid_martians() then accept; [...] if verif_roa_test() then accept; reject; } protocol bgp transitaire1 { debug all; description "transitaire1"; local as 60630; neighbor 2001:db8::1 as 64502; import filter cleaner; export filter arn_subnet; } |
Alors oui, on n'est pas obligé de faire une fonction mais on tient à laisser le filtre le plus clair possible.
Dans verif_roa_test(), nous pouvons mettre ce que nous voulons. Exemple :
if roa_check(testroa) = ROA_INVALID then return false; return true; |
Ici, si la vérification sort avec un état « invalide », l'annonce correspondante sera ignorée.
Pour vérifier, il suffit de demander à BIRD de relire la configuration et de créer la table des VRP (configure soft) puis de re-analyser chaque annonce (restart <nom_protocol>) :
# bird6c bird> configure soft Reading configuration from /etc/bird6.conf Reconfigured. bird> restart transitaire1 |
On patiente un peu et on demande :
bird> show route 2001:660::/32 Network not in table bird> |
L'annonce concernant l'allocation v6 de RENATER a bien été ignorée car l'AS qui prétend en être à l'origine (2200) ne correspondait pas à celui indiquée dans l'assertion présente dans la table (AS 64501). Si vous n'obtenez pas ce comportement, alors vous recevez certainement une annonce pour cette alloc' via une autre session BGP (peering, autre transitaire).
Évidement, cet exemple est mauvais : un opérateur préférera toujours la connectivité à la sécurité car c'est son rôle : fournir de la connectivité. Donc, il ne faut pas rejeter les annonces dans le cas d'une vérification qui sort avec l'état « ROA_INVALID ». Je doute que ces annonces soient rejetées un jour sur des routeurs de production. Le fait de rejeter ces annonces ne permet aucune tolérance aux erreurs (erreur humaine dans la RPKI, erreur matérielle dans la RPKI, erreur humaine dans la création des ROA, ROA qui ne correspondent plus à la topologie, ...).
Le mieux est de leur attribuer une préférence locale différente permettant d'établir une hiérarchie : VALID > UNKNOWN > INVALID. Ainsi, si seulement une annonce invalide parvient au routeur, le préfixe de celle-ci sera quand même ajouté à la table de routage. Si des annonces valide et invalide cohabitent (ce qui est le cas lors d'une attaque par détournement de préfixe), alors l'annonce valide sera privilégiée.
Exemple de mise en pratique :
function verif_roa_test() { if roa_check(testroa) = ROA_INVALID then bgp_local_pref = 90; if roa_check(testroa) = ROA_UNKNOWN then bgp_local_pref = 100; if roa_check(testroa) = ROA_VALID then bgp_local_pref = 110; return true; } |
Demandez à BIRD de recharger sa configuration et de re-analyser toutes les annonces et vous verrez que l'allocation v6 de RENATER se voit affecter une local-pref de 90 :
show route all 2001:660::/32 2001:660::/32 via 2001:db8::1 on eth0 [transitaire1 Oct03] * (100) [AS2200i] Type: BGP unicast univ BGP.origin: IGP [...] BGP.local_pref: 90 [...] |
Soyez plus dynamique svp !
Bon, la vérification de la conformité des annonces vis-à-vis des VRP fonctionne mais si l'on doit ajouter toutes les assertions à la main ... Les routeurs doivent récupérer automatiquement ces assertions depuis un cache-validateur. Le protocole utilisé est RTR (RPKI To Router). Les routeurs doivent implémenter ce protocole. Ce n'est pas encore le cas de BIRD.
Néanmoins, BIRD permet de mettre à jour dynamiquement une table de VRP grâce à des commandes dans birdc(6) :
- flush roa table <nom_table> : vider une table de toutes les assertions ajoutées dynamiquement (donc pas celles ajoutées depuis le fichier de configuration).
- add roa <prefixe> max <int_max> as <ASN> table <nom_table> : ajouter une assertion dans une table.
- del roa <prefixe> max <int_max> as <ASN> table <nom_table> : supprimer une assertion dans une table.
Il nous faut donc écrire un petit programme qui se connecte à un cache-validateur et, en fonction de ce qu'il reçoit, ajoute ou supprime des assertions dans notre table dans BIRD en utilisant birdc(6).
La RTRlib permet de se simplifier le travail puisqu'elle prend en charge la connexion à un cache-validateur et la réception des VRP. Je ne vous explique pas comment installer cette lib, c'est déjà très bien expliqué sur le wiki officiel : installation - RTRlib.
rtrclient est un petit logiciel fourni avec la RTRlib qui permet de la tester. Il se contente d'afficher les VRP qu'il récupère depuis un cache-validateur. Il suffit donc de modifier ce programme pour notre usage. En réalité, je me suis contenté :
- D'ajouter un « flush roa table testroa » lors de l'initialisation pour éviter tout problème (programme qui plante, état incomplet, ...).
- De commenter l'affichage « Prefix Prefix Length ASN ».
- De modifier la fonction de callback « update_cb » pour changer l'action à exécuter pour chaque ajout ou suppression d'une assertion.
ÉDIT du 20/12/2016 à 20h30 : Sinon, de nos jours, on utilise bird-rtrlib-cli qui fait pile poil le même travail mais avec du code plus robuste et en lequel avoir confiance. Fin de l'édit.
Ce n'est pas du grand art mais ça fait le job :
#include <stdlib.h> #include <pthread.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #include "rtrlib/rtrlib.h" #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> static void print_usage(char** argv){ printf("Usage:\n"); printf(" %s tcp <host> <port>\n", argv[0]); printf("\nExamples:\n"); printf(" %s tcp rpki.realmv6.org 42420\n", argv[0]); } static void update_cb(struct pfx_table* p __attribute__((unused)), const pfx_record rec, const bool added){ char ip[INET6_ADDRSTRLEN]; ip_addr_to_str(&(rec.prefix), ip, sizeof(ip)); char buff[100]; if(added) sprintf(buff, "add roa %s/%u max %u as %u table testroa", ip, rec.min_len, rec.max_len, rec.asn); else sprintf(buff, "delete roa %s/%u max %u as %u table testroa", ip, rec.min_len, rec.max_len, rec.asn); if (fork() > 0) { /* On ne veut pas d'affichage informatif donc on redirige stdout vers /dev/null */ int fd = open("/dev/null", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); dup2(fd, 1); close(fd); if (rec.prefix.ver == IPV6) execl("/usr/sbin/birdc6", "/usr/sbin/birdc6", buff, (char *) NULL); else execl("/usr/sbin/birdc", "/usr/sbin/birdc", buff, (char *) NULL); } } int main(int argc, char** argv){ enum mode_t { TCP, SSH } mode; char* host; char* port; char* user; char* privkey; char* pubkey; if(argc == 1){ print_usage(argv); return(EXIT_FAILURE); } if(strncasecmp(argv[1], "tcp", strlen(argv[1])) == 0){ if(argc != 4){ print_usage(argv); return(EXIT_FAILURE); } mode = TCP; host = argv[2]; port = argv[3]; } else if(strncasecmp(argv[1], "ssh", strlen(argv[1])) == 0){ if(argc != 7){ print_usage(argv); return(EXIT_FAILURE); } mode = SSH; host = argv[2]; port = argv[3]; user = argv[4]; privkey = argv[5]; pubkey = argv[6]; } else{ print_usage(argv); return(EXIT_FAILURE); } tr_socket tr_sock; if(mode == TCP){ tr_tcp_config config = { host, port, }; tr_tcp_init(&config, &tr_sock); } rtr_socket rtr; rtr.tr_socket = &tr_sock; rtr_mgr_group groups[1]; groups[0].sockets_len = 1; groups[0].sockets = malloc(1 * sizeof(rtr_socket*)); groups[0].sockets[0] = &rtr; groups[0].preference = 1; rtr_mgr_config conf; conf.groups = groups; conf.len = 1; /* On vide la table pour éviter tout problème */ if (fork() > 0) execl("/usr/sbin/birdc", "/usr/sbin/birdc", "flush roa table testroa", (char *) NULL); if (fork() > 0) execl("/usr/sbin/birdc6", "/usr/sbin/birdc6", "flush roa table testroa", (char *) NULL); rtr_mgr_init(&conf, 1, 520, &update_cb); rtr_mgr_start(&conf); //printf("%-40s %3s %3s %3s\n", "Prefix", "Prefix Length", "", "ASN"); pause(); rtr_mgr_stop(&conf); rtr_mgr_free(&conf); free(groups[0].sockets); return(EXIT_SUCCESS); } |
Je ne suis pas satisfait de l'absence de contrôle des valeurs de retour des appels systèmes, du traitement du buffer et des optimisations restantes. Mais je relativise : c'est juste un PoC et je m'en remets au formalisme et à la rigueur de la RTRlib.
Pour le compiler :
gcc rtrclient-bird.c -o rtrclient-bird -lrtr |
Pour l'exécuter (exemple) :
./rtrclient-bird tcp rpki.realmv6.org 42420 |
Dans cet exemple, j'utilise le cache-validateur mis à disposition par les devs de la RTRlib. Je le fais sans sécurité ce qui est très mal dans un contexte de production surtout quand le cache-validateur n'est pas sur le même réseau L2 que le routeur ! Je signale que le cache-validateur rpki.realmv6.org peut aussi être utilisé over SSH. Voir : RTRlib - Usage.
Ce programme est un peu violent à son lancement : environ 6800 ROA à l'heure actuelle donc 6800 « birdc(6) roa add » donc 6800 fork() + exec() ... Mais en vitesse de croisière, avec +/- 1 ROA par-ci, par-là, ça se passe gentiment.
Pensez à supprimer/commenter le préfixe v6 de RENATER de votre table de VRP et à demander un « configure soft » à BIRD.
Voilà : la table des assertions dans BIRD se remplit toute seule et se maintient à jour toute seule (aussi longtemps que vit le programme rtrclient-bird). À partir de maintenant, BIRD prendra en compte ces assertions en fonction de votre filtre. Pour que ça soit rétroactif, il faut « restart <nom_protocol> ».
Faire des stats
Ce qu'on a vu plus haut, c'est du bonus, pour découvrir. Ce n'est clairement pas une bonne idée de faire tourner l'assemblage présenté ci-dessus (un logiciel maison mal-codé non-revu et qui ne sera pas suivi, une librairie installée en dehors d'un système de paquets, ...) sur une machine en production et surtout de baser une partie du processus de choix des routes sur cet assemblage.
Ce que je voulais obtenir, en vrai, c'est des statistiques comme celles de LACNIC ou celles du RIPE (exemple). Parce que c'est sympa d'avoir ses stats personnelles, que c'est formateur de chercher à les obtenir et aussi parce que l'outil de LACNIC est souvent en rade. ÉDIT du 20/12/2016 à 20h30 : Depuis son apparition en 2013, l'observatoire de la résilience de l'Internet français mesure aussi le déploiement de RPKI+ROA. Fin de l'édit.
Et faire des stats, ça ne nécessite pas d'utiliser les VRP comme une donnée supplémentaire dans le processus de sélection des routes. Vous pouvez donc commenter la fonction verif_roa_test() ou supprimer son appel par le filtre (et « reconfigure » BIRD, of course).
Afficher le nombre d'assertions
birdc "show roa table testroa" | wc -l birdc6 "show roa table testroa" | wc -l |
Afficher le nombre de préfixes par état
birdc show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count birdc6 show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count birdc show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count birdc6 show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count birdc show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count birdc6 show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count |
Il n'est pas obligatoire de spécifier un protocole, c'est juste pour éviter les doublons si vous avez plusieurs transitaires, du peering, ...
Résultats
Voilà ce que l'on obtient, en v4, sur le routeur d'ARN :
birdc "show roa table testroa" | wc -l 5881 bird> show route protocol transitaire1 count 463627 of 927572 routes for 463629 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 17167 of 927571 routes for 463628 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 1609 of 927567 routes for 463626 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 444851 of 927571 routes for 463628 networks |
Voilà, ce que l'on obtient, en v6, sur le routeur d'ARN :
birdc6 "show roa table testroa" | wc -l 949 bird> show route protocol transitaire1 count 14375 of 28809 routes for 14376 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 879 of 28809 routes for 14376 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 59 of 28807 routes for 14375 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 13437 of 28809 routes for 14376 networks |
On constate qu'environ 8% des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 6% en v6.
On obtient des chiffres assez proches de ceux de LACNIC (attention à additionner v4 et v6 si vous voulez comparer) :
Route counts for the last 24 hours Current INVALID route count for all repositories: 2000 Bad MaxLen: 1655 Wrong BGP Origin AS: 345 Current VALID route count for all repositories: 18277 |
On constate qu'environ 10% des routes dont au moins un ROA leur est associé sont invalides.
ÉDIT du 26/01/2014 à 20h45 : Voici les stats récoltées aujourd'hui sur le routeur d'ARN :
En v4 :
birdc-arn "show roa table testroa" | wc -l 7005 bird> show route protocol transitaire1 count 474565 of 949513 routes for 474566 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 18640 of 949510 routes for 474567 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 2102 of 949498 routes for 474559 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 453815 of 949497 routes for 474558 networks |
En v6 :
birdc6-arn "show roa table testroa" | wc -l 1094 bird> show route protocol transitaire1 count 15978 of 32033 routes for 15979 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 999 of 32030 routes for 15978 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 60 of 32031 routes for 15978 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 14917 of 32031 routes for 15978 networks |
On constate qu'environ 10% (augmentation) des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 6% en v6 (stagnation).
Le site web de LACNIC est down depuis plusieurs heures et depuis plusieurs points du réseau donc impossible de comparer.
Fin de l'édit
ÉDIT du 01/03/2014 à 14h30 : Voici les stats récoltées aujourd'hui sur le routeur d'ARN :
En v4 :
birdc "show roa table testroa" | wc -l 7654 bird> show route protocol transitaire1 count 478065 of 956517 routes for 478066 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 19182 of 956497 routes for 478056 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 2045 of 956463 routes for 478040 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 456811 of 956463 routes for 478039 networks |
En v6 :
birdc6 "show roa table testroa" | wc -l 1202 bird> show route protocol transitaire1 count 16413 of 32905 routes for 16414 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 1078 of 32905 routes for 16414 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 57 of 32905 routes for 16414 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 15276 of 32902 routes for 16413 networks |
On constate qu'environ 10% (stagnation) des routes v4 dont au moins un ROA leur est associé sont invalides. On est a 5% en v6 (diminution). v4 et v6 cumulés : 9%
En comparaison, les chiffres obtenus par LACNIC :
Route counts for the last 24 hours Current INVALID route count for all repositories: 2596 Bad MaxLen: 2065 Wrong BGP Origin AS: 531 Current VALID route count for all repositories: 20765 Dataset processed on: March 1, 2014 |
Fin de l'édit
ÉDIT du 20/12/2016 à 20h30 : Voici les stats récoltées aujourd'hui sur le routeur d'ARN :
En v4 :
birdc "show roa table testroa" | wc -l 25822 bird> show route protocol transitaire1 count 616508 of 616508 routes for 616508 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 40657 of 616500 routes for 616500 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 3985 of 616492 routes for 616492 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 571869 of 616517 routes for 616517 networks |
En v6 :
birdc6 "show roa table testroa" | wc -l 3650 bird> show route protocol transitaire1 count 34642 of 34642 routes for 34642 networks bird> show route where roa_check(testroa) = ROA_VALID protocol transitaire1 count 3745 of 34651 routes for 34651 networks bird> show route where roa_check(testroa) = ROA_INVALID protocol transitaire1 count 214 of 34652 routes for 34652 networks bird> show route where roa_check(testroa) = ROA_UNKNOWN protocol transitaire1 count 30692 of 34651 routes for 34651 networks |
En comparaison, les chiffres obtenus par LACNIC :
Route counts for the last 24 hours Current INVALID route count for all repositories: 4848 Bad MaxLen: 3575 Wrong BGP Origin AS: 1273 Current VALID route count for all repositories: 45581 Dataset processed on: Dec. 20, 2016 |
Fin de l'édit du 20/12/2016 à 20h30
Progression
En février 2012, le RIPE dénombrait environ 1800 ROA qui couvraient environ 8200 routes. Environ 40 % des routes dont au moins un ROA leur est associé étaient invalides.
En avril dernier, je dénombrais environ 3000 ROA.
Aujourd'hui, je dénombre environ 6800 ROA qui couvrent environ 19700 routes. Environ 8 % à 10 % des routes dont au moins un ROA leur est associé sont invalides.
ÉDIT du 26/01/2014 à 20h45 : Aujourd'hui, je dénombre environ 8100 ROA qui couvrent environ 21800 routes. Environ 8 % des routes dont au moins un ROA leur est associé sont invalides. Fin de l'édit.
ÉDIT du 01/03/2014 à 14h30 : Aujourd'hui, je dénombre environ 8800 ROA qui couvrent environ 22300 routes. Environ 9 % des routes dont au moins un ROA leur est associé sont invalides. Fin de l'édit.
ÉDIT du 20/12/2016 à 20h30 : Aujourd'hui, je dénombre environ 29500 ROA qui couvrent environ 48600 préfixes. Environ 8,6 % des préfixes dont au moins un ROA leur est associé sont invalides. Fin de l'édit.