lalahop

RPKI+ROA et BIRD pour s’amuser

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() » permet d'examiner la conformité d'une annonce BGP vis-à-vis d'une table de VRP (source : documentation BIRD - opérateurs). Il sort selon les 3 états normalisés (RFC 6811) :

  • 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.