Mininet : tester ses applications, dans un contexte réseau réaliste, sans surcoût

Mininet : tester ses applications, dans un contexte réseau réaliste, sans surcoût

Introduction

Tester un logiciel n’est pas toujours chose aisée, en particulier lorsque celui-ci est distribué et donc tributaire de son environnement réseau. Mettre en place un contexte réseau de test proche de la réalité en production peut être fastidieux mais également (très) coûteux.

Cet article a pour but de vous présenter Mininet, une solution logicielle, qui vous permettra de mettre en place des topologies réseau au plus proche des besoins de vos tests, simplement.

Plus concrètement, Mininet vous permet de déployer, sur une seule machine, des réseaux, commutateurs, routeurs, pare feux ou tout autre équipement réseau issu de votre imagination débordante, ainsi que vos applications dans des "hôtes virtuels". Le tout, automatiquement et dynamiquement. Belle promesse, non ?

Prise en main

Il est possible d’utiliser Mininet directement via l’outil en ligne de commande, ou bien de s’en servir comme une librairie. La première option est parfaite pour découvrir les fonctionnalités de l’outil et construire une topologie simple. La seconde sera plus utile pour mettre en place une topologie avancée, intégrer vos applicatifs, ou écrire vos scénarii de test.

Pour une première utilisation de l’outil nous pouvons simplement lancer :

sudo mn

Note

A ce stade, si vous obtenez le message suivant :

Exception: Please shut down the controller which is running on port 6653:
Connexions Internet actives (serveurs et établies)
tcp        0      0 0.0.0.0:6653            0.0.0.0:*               LISTEN      2175/ovs-testcontro

C’est que comme moi, vous avez déjà joué avec OpenVSwitch sur votre machine, il convient donc d’éteindre le contrôleur SDN de test d’OpenVSwitch avant de continuer :

sudo systemctl stop openvswitch-testcontroller

Vous avez des doutes sur ce que sont le SDN et OpenVSwitch ? La réponse dans cet article. Un article dédié à OpenVSwitch devrait également faire son apparition sur le blog d’ici la fin de l’année. Quel rapport avec notre article ? Mininet est également une solution de SDN, puisqu’il permet de manipuler des ressources réseau grâce à une API (une librairie Python dans notre cas).

---

Une fois Mininet lancé, nous devrions avoir cet output :

*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2 
*** Adding switches:
s1 
*** Adding links:
(h1, s1) (h2, s1) 
*** Configuring hosts
h1 h2 
*** Starting controller
c0 
*** Starting 1 switches
s1 ...
*** Starting CLI:
mininet>

Nous voyons ici que Mininet crée deux “hôtes virtuels” qui sont en réalité des network namespaces. Mininet utilise cette fonctionnalité du noyau pour permettre à différents processus de disposer d’interfaces réseau, et de tables de routage distinctes, sur la même machine. Pour plus d’information concernant les network namespaces, voir cet article.

La cli de Mininet nous permet de lister les éléments provisionnés (ou nodes) :

mininet> nodes
available nodes are:
c0 h1 h2 s1

Nous pouvons exécuter des commandes sur chaque noeud. Regardons les interfaces réseau disponibles sur chaque host :

mininet> h1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: h1-eth0@if41: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether c6:21:4d:28:11:da brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.1/8 brd 10.255.255.255 scope global h1-eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::c421:4dff:fe28:11da/64 scope link
       valid_lft forever preferred_lft forever

mininet> h2 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: h2-eth0@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 1e:c4:d8:47:5a:bf brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.2/8 brd 10.255.255.255 scope global h2-eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::1cc4:d8ff:fe47:5abf/64 scope link
       valid_lft forever preferred_lft forever

et sur le switch virtuel :

mininet> s1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s31f6:
[...]
41: s1-eth1@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ovs-system state UP group default qlen 1000
    link/ether 4a:4b:91:9c:26:9c brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::484b:91ff:fe9c:269c/64 scope link
       valid_lft forever preferred_lft forever
42: s1-eth2@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ovs-system state UP group default qlen 1000
    link/ether 9e:b7:28:0d:50:85 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::9cb7:28ff:fe0d:5085/64 scope link
       valid_lft forever preferred_lft forever
43: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether f2:77:24:a3:3c:da brd ff:ff:ff:ff:ff:ff
44: s1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 96:c9:a8:45:82:4b brd ff:ff:ff:ff:ff:ff

Nous voyons donc que chaque host dispose de son propre network namespace. s1 est quand à lui dans le namespace de notre machine locale (id 0). Il en va de même pour le contrôleur c0 (vous pouvez également regarder les tables de routage ou les règles iptables mises en place pour vous en convaincre).

Mininet facilite la communication entre les différents noeuds. Exemple, nous pouvons pinger directement h2 depuis h1 de cette façon :

mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=3.13 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.485 ms

Il est donc très simple de tester une application web, par exemple. Il suffit pour cela de lancer le serveur sur l’un des hosts et de lancer des requêtes depuis un autre host qui jouera le rôle de client.

Exemple :

mininet> h1 python -m SimpleHTTPServer 80 &
mininet> h2 wget -O - h1

Mininet est également très utile pour tester des applications distribuées, puisqu’il permet de déployer de nombreux noeuds, au sein d’une topologie que l’on aura choisie.

Voyons justement comment déployer une topologie plus complexe, depuis la ligne de commande. Mininet dispose de topologies type, que l’on peut utiliser simplement, exemple avec la topologie linear (n hosts, un switch par host, tous les switchs sont connectés, le même sous-réseau est utilisé) :

sudo mn --topo linear,4
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2 h3 h4
*** Adding switches:
s1 s2 s3 s4
*** Adding links:
(h1, s1) (h2, s2) (h3, s3) (h4, s4) (s2, s1) (s3, s2) (s4, s3)
*** Configuring hosts
h1 h2 h3 h4
*** Starting controller
c0
*** Starting 4 switches
s1 s2 s3 s4 ...
*** Starting CLI:
mininet>

La documentation du projet comprend la liste des topologies pré-configurées et la manière de s’en servir.

Allons un peu plus loin

Nous avons vu les bases de l’utilisation de Mininet. Voyons maintenant ce qui fait sa plus-value. Nous pouvons induire de la latence, ou réduire la bande passante des différents liens, ce qui peut être très pratique pour tester les performances de nos applications dans un contexte réaliste. Mininet s’appuie sur tc pour ce faire. Voici un exemple :

sudo mn --link tc,bw=10,delay=10ms
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2
*** Adding switches:
s1
*** Adding links:
(10.00Mbit 10ms delay) (10.00Mbit 10ms delay) (h1, s1) (10.00Mbit 10ms delay) (10.00Mbit 10ms delay) (h2, s1)
*** Configuring hosts
h1 h2
*** Starting controller
c0
*** Starting 1 switches
s1 ...(10.00Mbit 10ms delay) (10.00Mbit 10ms delay)

Ici nous créons une topologie simple avec deux hosts et un switch. Chaque lien avec le switch est soumis à une latence de 10 ms et à une capacité maximum de 10 Mb. Vérifions cela :

mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=40.2 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=40.2 ms

Le temps de réponse en ICMP entre les deux hosts est de 40 ms, puisque nous avons deux liens à parcourir qui induisent chacun 10 ms de latence et qu’il faut faire l’aller-retour.

Mininet peut également lancer un test iperf pour nous :

mininet> iperf
*** Iperf: testing TCP bandwidth between h1 and h2
*** Results: ['9.48 Mbits/sec', '11.7 Mbits/sec']

On tourne bien autour de 10Mbps de débit maximum.

Le paramétrage de variation des liens peut être poussé bien plus loin si l’on se sert de la librairie Python Mininet. Nous en parlerons dans la section suivante.

Il est également possible de couper les liens entre nos machines, très pratique pour tester la résilience d’une application distribuée par exemple :

mininet> link s1 h1 down
mininet> h1 ping h2
connect: Le réseau n'est pas accessible
mininet> link s1 h1 up
mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=41.0 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=40.3 ms

Utiliser l’”API” Python

Nous avons exploré les fonctionnalités de base de Mininet. Mais cet outil est bien plus puissant (et automatisable) si l’on programme avec. Il nous est alors possible de créer des topologies dynamiques, d’y inclure des routeurs, des firewalls, des proxys, etc…

Voici un exemple basique, dans lequel je définis un noeud routeur, qui aura une interface dans deux sous-réseaux différents. Chaque réseau comprend un switch et un host.

#!/usr/bin/python

from mininet.node import OVSController
from mininet.net import Mininet
from mininet.cli import CLI
from mininet.log import lg, info
from mininet.node import Node
from mininet.topolib import TreeTopo
from mininet.topo import Topo
from subprocess import call


class LinuxRouter( Node ):
    "A Node with IP forwarding enabled."
    def __init__(self, name, **params):
        self.nat = True
        self.subnets = [ "192.168.101.0/24", "192.168.102.0/24" ]
        super(LinuxRouter, self).__init__(name, **params)

    def flush_nat(self):
        self.cmd( 'sysctl -w net.ipv4.ip_forward=0' )
        self.cmd( 'iptables -F' )
        self.cmd( 'iptables -t nat -F' )
        # Create default entries for unmatched traffic
        self.cmd( 'iptables -P INPUT ACCEPT' )
        self.cmd( 'iptables -P OUTPUT ACCEPT' )
        self.cmd( 'iptables -P FORWARD ACCEPT' )

    def config( self, **params ):
        super( LinuxRouter, self).config( **params )
        # Enable forwarding on the router
        self.flush_nat()
        self.cmd( 'sysctl -w net.ipv4.ip_forward=1' )
        self.cmd('iptables -t nat -A POSTROUTING -j MASQUERADE')
        self.cmd('ip addr flush r0-ethost1')
        self.cmd('ip addr add 192.168.101.1/24 dev r0-ethost1')

    def terminate( self ):
        self.cmd( 'sysctl net.ipv4.ip_forward=0' )
        super( LinuxRouter, self ).terminate()

class NetworkTopo( Topo ):

    def build( self, **_opts ):

        router = self.addNode( 'r0', cls=LinuxRouter )

        s1, s2 = [ self.addSwitch( s ) for s in ( 's1', 's2') ]

        self.addLink( s1, router, intfName2='r0-ethost1',
          params2={ 'ip' : '192.168.101.1/24' } )
        self.addLink( s2, router, intfName2='r0-ethost2',
          params2={ 'ip' : '192.168.102.1/24' } )

        host1 = self.addHost( 'host1', ip='192.168.101.100/24',
               defaultRoute='via 192.168.101.1' )
        host2 = self.addHost( 'host2', ip='192.168.102.100/24',
               defaultRoute='via 192.168.102.1' )

        for h, s in [ (host1, s1), (host2, s2) ]:
                self.addLink( h, s )

if __name__ == '__main__':

    lg.setLogLevel('info')
    net = Mininet(topo=NetworkTopo(), controller=OVSController)
    net.start()

    for h in net.hosts:
        h.cmd("mkdir -p /run/sshd")
        h.cmd("/usr/sbin/sshd -D &")
    info("\nssh daemons starting")

    CLI(net)
    for host in net.hosts:
            host.cmd( 'kill /usr/sbin/sshd' )
    net.stop()

On remarque l’utilisation de deux classes principales, Node et Topo, issues du paquet Mininet, qui permettent de définir des hosts et des topologies personnalisées en en héritant.

On peut démarrer cet exemple simplement :

sudo python mn.py
*** Creating network
*** Adding controller
*** Adding hosts:
host1 host2 r0
*** Adding switches:
s1 s2
*** Adding links:
(host1, s1) (host2, s2) (s1, r0) (s2, r0)
*** Configuring hosts
host1 host2 r0
*** Starting controller
c0
*** Starting 2 switches
s1 s2 ...

ssh daemons starting*** Starting CLI:
mininet>

Vérifions que host1 passe bien par le routeur pour joindre host2 :

mininet> host1 mtr host2 -n --report -c 2
Start: 2018-04-07T17:29:08+0200
HOST:                               Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- 192.168.101.1              0.0%     2    0.7   9.3   0.7  17.8  12.1
  2.|-- 192.168.102.100            0.0%     2    0.8  14.8   0.8  28.7  19.7

Dans cet exemple, je m’assure également qu’un agent ssh écoute sur le port 22 pour chaque host. Vérifions :

mininet> host1 ssh host2
The authenticity of host '192.168.102.100 (192.168.102.100)' can't be established.
ECDSA key fingerprint is SHA256:umX/IIn54gnLAI9A7QklhoRGiT1/G3SV9daoaXm1B48.
Are you sure you want to continue connecting (yes/no)?

De nombreux exemples de topologies et de cas d’usage sont proposés sur github.

Conclusion

Mininet est très utilisé dans le cadre de développement SDN à base d’OpenFlow, puisqu’il est possible de le piloter à partir d’un contrôleur externe. Dans la pratique, cet outil peut être très utile dans le cadre d’applications plus classiques, puisqu’il permet de créer automatiquement et par le code, des topologies personnalisées dans lesquelles on peut induire des contraintes réseau diverses. Puisque toute l’infrastructure créée par Mininet tourne sur une seule machine, il est simple de l’utiliser dans le cadre de tests applicatifs via votre outil de CI préféré. C’est également un très bon outil pour mettre en place des architectures de test ou de démonstration. Je l’ai déjà vu être utilisé dans de nombreuses conférences.

Sources