diff --git a/README.rst b/README.rst index 8c2f20f..61be446 100644 --- a/README.rst +++ b/README.rst @@ -1,63 +1,158 @@ What ? ====== -Simple tool, based on `mininet `_, to boot a simple network -with n paths and run experiments between two hosts. +This repository provides a **Python runner** built on `Mininet `_ +to create and control simple network topologies with multiple paths. +It can be used for a wide range of networking experiments (TCP, UDP, QUIC, etc.). +In our case, we focus mainly on **QUIC** tests between two hosts. -Usage -===== +Requirements +============ + +To run the experiments, you need the following environment: + +- **Operating system:** Ubuntu 20.04+ (or WSL2 with Ubuntu) +- **Python:** version 3.8 or newer +- **Mininet:** installed system-wide +- **System tools:** ``iproute2``, ``ethtool``, ``tcpdump``, and ``tshark`` (for packet capture) +- **Optional GUI:** Wireshark (to visualize pcap files) + +Quick installation: .. code-block:: console - ./mpPerf -t topo -x xp + sudo apt-get update + sudo apt-get install -y mininet iproute2 ethtool tcpdump tshark python3-pip -The format for the topo file and xp file is simple but could be different based -on the type of topo or experiments. Details should follow. +--- -basic Example -============= +Optional – QUIC experiments (using *quiche*) +-------------------------------------------- -1. Get the CLI --------------- +If you plan to run **QUIC or Multipath QUIC** experiments, you will also need +the `quiche `_ library. + +Build the client and server binaries with: .. code-block:: console - ./mpPerf -t conf/topo/simple_para + git clone --recursive https://github.com/cloudflare/quiche.git + cd quiche + cargo build --release --bin quiche-server --bin quiche-client + +After compilation, you can use the following binaries in your experiment files: + +- ``target/release/quiche-server`` +- ``target/release/quiche-client`` + +Simple Example +============================================= + +1. The topology +---------------------- + +Start a multi-interface topology (in our case ``topo_2.yaml``) **without any experiment** +to access the Mininet CLI: + +This topology creates a simple 3-node setup composed of: + +- a client host with two network interfaces (10.0.0.1 and 10.0.1.1), +- a router connecting both subnets (10.0.x.0/24), +- a server host reachable on the right subnet (10.1.0.1). + +Each interface of the client is connected to the router with its own link characteristics +(delay, bandwidth, queue size). The server receives traffic coming from both paths through the router. -The content of simple_para is: .. code-block:: console - desc:Simple configuration with two para link - topoType:MultiIf - leftSubnet:10.0. - rightSubnet:10.1. - #path_x:delay,queueSize(may be calc),bw - path_0:10,10,5 - path_1:40,40,5 - path_2:30,30,2 - path_3:20,20,1 + sudo python3 runner.py -t config/topo/topo_2 -``topoType`` just specifies that we want to have multiple interfaces, one for -each path. +This opens the interactive Mininet CLI with the nodes already connected +(client, router, server). -Each path is defined by 3 values, delay (one way, int, in ms), queue_size (int, -in packets), and bandwidth (float, in mbit/s). +--- -Once the configuration is up, you have access to the CLI. You can check route -configuration (policy routing etc.) Just by issuing regular commands preceded -by ``Client`` or ``Server`` +To verify that the runner and topology are working correctly, you can start the +topology and interact with it using the Mininet CLI: + +.. code-block:: console -2. Simple experiment --------------------- + sudo python3 runner.py -t config/topo/topo_2.yaml + +This starts the network with three nodes: + +- ``Client_0`` (two interfaces) +- ``Router_0`` +- ``Server_0`` + +Inside the Mininet CLI, you can run simple connectivity tests, for example: .. code-block:: console - ./mpPerf -t conf/topo/simple_para -x conf/xp/4_nc + mininet> Client_0 ping -c 3 Server_0 + +That confirms if the topology is functional without requiring a full QUIC +experiment. + + +YAML Support (New) +================== + +The runner now supports **YAML configuration files** for defining topologies and experiments, +in addition to the legacy ``.para`` format. + +This new format improves readability, structure, and automation. + +Legacy vs YAML Example +---------------------- + +**Legacy format (``topo_2``):** + +.. code-block:: text + + leftSubnet:10.0 + rightSubnet:10.1 + path_c2r_0:100,20,4 + path_c2r_1:1,20,4 + path_r2s_0:10,20,10 + topoType:MultIf + +**YAML format (``topo_2.yaml``):** + +.. code-block:: yaml + + version: 1 + topology: + type: MultiIf + subnets: + left: 10.0 + right: 10.1 + + paths: + - link_type: c2r + id: 0 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: c2r + id: 1 + delay_ms: 1 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: r2s + id: 0 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 + + +Runner Update +------------- + +The ``runner.py`` script automatically detects and parse YAML files -This command will start the same topology and run the experiment defined by 4_nc -The result for this experiment is a simple pcap file. -They are other options and experiments, but the documentation is still to be -written. diff --git a/config/topo/topo_1 b/config/topo/topo_1 deleted file mode 100644 index 4d87e30..0000000 --- a/config/topo/topo_1 +++ /dev/null @@ -1,5 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:10,10,4 -path_c2r_1:40,30,4 -topoType:MultiIf diff --git a/config/topo/topo_1.yaml b/config/topo/topo_1.yaml new file mode 100644 index 0000000..db947f7 --- /dev/null +++ b/config/topo/topo_1.yaml @@ -0,0 +1,19 @@ +version: 1 +topology: + type: MultiIf + subnets: + left: 10.0 + right: 10.1 + + paths: + - link_type: c2r + id: 0 + delay_ms: 10 + queue_pkts: 10 + bw_mbit: 4 + + - link_type: c2r + id: 1 + delay_ms: 40 + queue_pkts: 30 + bw_mbit: 4 diff --git a/config/topo/topo_2 b/config/topo/topo_2 deleted file mode 100644 index e2c79ae..0000000 --- a/config/topo/topo_2 +++ /dev/null @@ -1,6 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:100,20,4 -path_c2r_1:1,20,4 -path_r2s_0:10,20,10 -topoType:MultiIf diff --git a/config/topo/topo_2.yaml b/config/topo/topo_2.yaml new file mode 100644 index 0000000..27ddbad --- /dev/null +++ b/config/topo/topo_2.yaml @@ -0,0 +1,22 @@ +version: 1 +topology: + type: MultiIf + subnets: + left: 10.0. + right: 10.1. + paths: + - link_type: c2r + id: 0 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + - link_type: c2r + id: 1 + delay_ms: 1 + queue_pkts: 20 + bw_mbit: 4 + - link_type: r2s + id: 0 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 diff --git a/config/topo/topo_3 b/config/topo/topo_3 deleted file mode 100644 index 9335d7e..0000000 --- a/config/topo/topo_3 +++ /dev/null @@ -1,5 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:100,20,4 -path_r2s_0:10,20,10 -topoType:MultiIf diff --git a/config/topo/topo_3.yaml b/config/topo/topo_3.yaml new file mode 100644 index 0000000..1ec130b --- /dev/null +++ b/config/topo/topo_3.yaml @@ -0,0 +1,19 @@ +version: 1 +topology: + type: MultiIf + subnets: + left: 10.0 + right: 10.1 + + paths: + - link_type: c2r + id: 0 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: r2s + id: 0 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 diff --git a/config/topo/topo_4 b/config/topo/topo_4 deleted file mode 100644 index 8c7cb9c..0000000 --- a/config/topo/topo_4 +++ /dev/null @@ -1,8 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:100,20,4 -path_c2r_1:100,20,4 -path_c2r_2:100,20,4 -path_r2s_0:10,20,10 -path_r2s_1:10,20,10 -topoType:MultiIf diff --git a/config/topo/topo_4.yaml b/config/topo/topo_4.yaml new file mode 100644 index 0000000..dd6963c --- /dev/null +++ b/config/topo/topo_4.yaml @@ -0,0 +1,37 @@ +version: 1 +topology: + type: MultiIf + subnets: + left: 10.0 + right: 10.1 + + paths: + - link_type: c2r + id: 0 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: c2r + id: 1 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: c2r + id: 2 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: r2s + id: 0 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 + + - link_type: r2s + id: 1 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 diff --git a/config/topo/topo_5 b/config/topo/topo_5 deleted file mode 100644 index 7951be8..0000000 --- a/config/topo/topo_5 +++ /dev/null @@ -1,8 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:100,20,4 -path_c2r_1:100,20,4 -path_r2s_0:10,20,10 -path_r2s_1:10,20,10 -path_r2s_2:10,20,10 -topoType:MultiIf diff --git a/config/topo/topo_5.yaml b/config/topo/topo_5.yaml new file mode 100644 index 0000000..8159754 --- /dev/null +++ b/config/topo/topo_5.yaml @@ -0,0 +1,37 @@ +version: 1 +topology: + type: MultiIf + subnets: + left: 10.0 + right: 10.1 + + paths: + - link_type: c2r + id: 0 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: c2r + id: 1 + delay_ms: 100 + queue_pkts: 20 + bw_mbit: 4 + + - link_type: r2s + id: 0 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 + + - link_type: r2s + id: 1 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 + + - link_type: r2s + id: 2 + delay_ms: 10 + queue_pkts: 20 + bw_mbit: 10 diff --git a/config/topo/topo_cong b/config/topo/topo_cong deleted file mode 100644 index 068b51c..0000000 --- a/config/topo/topo_cong +++ /dev/null @@ -1,5 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:10,10,4 -path_c2r_1:40,30,4 -topoType:MultiIfMultiClient diff --git a/config/xp/pquic b/config/xp/pquic new file mode 100644 index 0000000..ddbb16e --- /dev/null +++ b/config/xp/pquic @@ -0,0 +1,7 @@ +xpType:pquic +pquicPlugins:/home/achraf/pquic/plugins/multipath/multipath.plugin +pquicClientPlugins: +pquicServerPlugins: +pquicSize:2000000 +clientPcap:yes +serverPcap:yes diff --git a/config/xp/quic b/config/xp/quic new file mode 100644 index 0000000..d3d8c43 --- /dev/null +++ b/config/xp/quic @@ -0,0 +1,9 @@ +xpType:quic +fileSizeBytes:0 +quicImpl:quiche +quicPort:6121 +quicCertPath:/home/achraf/quiche/certs +quicNoVerify:1 +quicMultipath:0 +clientPcap:yes +serverPcap:yes diff --git a/config/xp/quic1 b/config/xp/quic1 new file mode 100644 index 0000000..7bb1800 --- /dev/null +++ b/config/xp/quic1 @@ -0,0 +1,9 @@ +xpType:quic1 +fileSizeBytes:0 +quicImpl:quiche +quicPort:6121 +quicCertPath:/home/achraf/quiche/certs +quicNoVerify:1 +quicMultipath:0 +clientPcap:yes +serverPcap:yes diff --git a/config/xp/quic_mpath.yaml b/config/xp/quic_mpath.yaml new file mode 100644 index 0000000..7e00aa0 --- /dev/null +++ b/config/xp/quic_mpath.yaml @@ -0,0 +1,17 @@ +version: 1 +experiment: + xp_type: quic_mpath + + env: + server_ip: 10.1.0.1 + client_ip_a: 10.0.0.1 + client_ip_b: 10.0.1.1 + port: 4433 + quiche_server_bin: /home/achraf/quiche/target/release/quiche-server + quiche_client_bin: /home/achraf/quiche/target/release/quiche-client + cert: /home/achraf/quiche/certs/cert.pem + key: /home/achraf/quiche/certs/key.pem + root: /home/achraf/quiche/quiche/examples + cap_if1: Router_0-eth0 + cap_if2: Router_0-eth1 + cap_count: 200 diff --git a/config/xp/quic_test b/config/xp/quic_test new file mode 100644 index 0000000..6403496 --- /dev/null +++ b/config/xp/quic_test @@ -0,0 +1,25 @@ +xpType: cmd + +serverCmds: + - "mkdir -p /tmp/qlog_server" + - "RUST_LOG=info QLOGDIR=/tmp/qlog_server \ + ~/quiche/target/release/quiche-server \ + --cert ~/quiche/certs/cert.pem \ + --key ~/quiche/certs/key.pem \ + --listen 0.0.0.0:4433 \ + --root ~/quiche > /tmp/quiche_server.log 2>&1 &" + - "sleep 1" # attendre que le serveur démarre + +clientCmds: + - "mkdir -p /tmp/qlog_client" + - "RUST_LOG=info QLOGDIR=/tmp/qlog_client \ + ~/quiche/target/release/quiche-client --no-verify \ + https://10.1.0.1:4433/big.bin > /tmp/quiche_client.log 2>&1" + +routerCmds: + - "tcpdump -n -i r0-eth0 udp port 4433 -w /tmp/r0_eth0_quic.pcap &" + - "tcpdump -n -i r0-eth1 udp port 4433 -w /tmp/r0_eth1_quic.pcap &" + +serverEndCmds: + - "pkill quiche-server || true" + - "pkill tcpdump || true" diff --git a/demo_ticket_store.bin b/demo_ticket_store.bin new file mode 100644 index 0000000..e69de29 diff --git a/experiments/__init__.py b/experiments/__init__.py index b0c7411..0e9b49b 100644 --- a/experiments/__init__.py +++ b/experiments/__init__.py @@ -1,21 +1,27 @@ -import importlib -import pkgutil import os +import pkgutil +import importlib from core.experiment import Experiment -pkg_dir = os.path.dirname(__file__) -for (module_loader, name, ispkg) in pkgutil.iter_modules([pkg_dir]): - importlib.import_module('.' + name, __package__) - -# Track indirect inheritance +# Dictionnaire public: nom d'expérience -> classe EXPERIMENTS = {} -def _get_all_subclasses(BaseClass): - for cls in BaseClass.__subclasses__(): - if hasattr(cls, "NAME"): +# Import dynamique de tous les sous-modules du package 'experiments' +_pkg_dir = os.path.dirname(__file__) +for _loader, _name, _ispkg in pkgutil.iter_modules([_pkg_dir]): + importlib.import_module(f"{__name__}.{_name}") + +def _register_all_subclasses(base_cls): + # Enregistre récursivement toutes les sous-classes qui ont un attribut NAME + for cls in base_cls.__subclasses__(): + if hasattr(cls, "NAME") and isinstance(getattr(cls, "NAME"), str): EXPERIMENTS[cls.NAME] = cls - - _get_all_subclasses(cls) + _register_all_subclasses(cls) + +# Peupler EXPERIMENTS à partir des classes importées +_register_all_subclasses(Experiment) -_get_all_subclasses(Experiment) \ No newline at end of file +# Override explicite: utiliser l'implémentation quiche pour xpType 'quic' +from .quic1 import QuicheHTTP3 +EXPERIMENTS["quic"] = QuicheHTTP3 diff --git a/experiments/pquic.py b/experiments/pquic.py index 3ebe491..0c6f54b 100644 --- a/experiments/pquic.py +++ b/experiments/pquic.py @@ -23,9 +23,10 @@ class PQUIC(Experiment): NAME = "pquic" PARAMETER_CLASS = PQUICParameter - BIN = "~/pquic/picoquicdemo" - CERT_FILE = "~/pquic/certs/cert.pem" - KEY_FILE = "~/pquic/certs/key.pem" + BIN = "/home/achraf/pquic/picoquicdemo" + CERT_FILE = "/home/achraf/pquic/certs/cert.pem" + KEY_FILE = "/home/achraf/pquic/certs/key.pem" + SERVER_LOG = "pquic_server.log" CLIENT_LOG = "pquic_client.log" diff --git a/experiments/quic.py b/experiments/quic.py index e07174c..1b48bc9 100644 --- a/experiments/quic.py +++ b/experiments/quic.py @@ -5,11 +5,19 @@ class QUICParameter(RandomFileParameter): MULTIPATH = "quicMultipath" + IMPL = "quicImpl" + PORT = "quicPort" + CERT_PATH = "quicCertPath" + NO_VERIFY = "quicNoVerify" def __init__(self, experiment_parameter_filename): super(QUICParameter, self).__init__(experiment_parameter_filename) self.default_parameters.update({ QUICParameter.MULTIPATH: "0", + QUICParameter.IMPL: "quic-go", + QUICParameter.PORT: "6121", + QUICParameter.CERT_PATH: "/home/achraf/quiche/certs", + QUICParameter.NO_VERIFY: "1", }) @@ -18,68 +26,90 @@ class QUIC(RandomFileExperiment): PARAMETER_CLASS = QUICParameter GO_BIN = "/usr/local/go/bin/go" - WGET = "~/git/wget/src/wget" - SERVER_LOG = "quic_server.log" - CLIENT_LOG = "quic_client.log" - CLIENT_GO_FILE = "~/go/src/github.com/lucas-clemente/quic-go/example/client_benchmarker_cached/main.go" - SERVER_GO_FILE = "~/go/src/github.com/lucas-clemente/quic-go/example/main.go" - CERTPATH = "~/go/src/github.com/lucas-clemente/quic-go/example/" + WGET = "/home/achraf/git/wget/src/wget" + SERVER_LOG = "/tmp/quic_server.log" + CLIENT_LOG = "/tmp/quic_client.log" + CLIENT_GO_FILE = "/home/achraf/go/src/github.com/lucas-clemente/quic-go/example/client_benchmarker_cached/main.go" + SERVER_GO_FILE = "/home/achraf/go/src/github.com/lucas-clemente/quic-go/example/main.go" + CERTPATH = "/home/achraf/go/src/github.com/lucas-clemente/quic-go/example/" PING_OUTPUT = "ping.log" + QUICHE_SERVER = "/home/achraf/quiche/target/release/quiche-server" + QUICHE_CLIENT = "/home/achraf/quiche/target/release/quiche-client" + def __init__(self, experiment_parameter_filename, topo, topo_config): - # Just rely on RandomFileExperiment super(QUIC, self).__init__(experiment_parameter_filename, topo, topo_config) def ping(self): - self.topo.command_to(self.topo_config.client, "rm " + \ - QUIC.PING_OUTPUT ) + self.topo.command_to(self.topo_config.client, "rm " + QUIC.PING_OUTPUT) count = self.experiment_parameter.get(ExperimentParameter.PING_COUNT) for i in range(0, self.topo_config.client_interface_count()): - cmd = self.ping_command(self.topo_config.get_client_ip(i), - self.topo_config.get_server_ip(), n = count) - self.topo.command_to(self.topo_config.client, cmd) + cmd = self.ping_command( + self.topo_config.get_client_ip(i), + self.topo_config.get_server_ip(), + n=count + ) + self.topo.command_to(self.topo_config.client, cmd) def ping_command(self, fromIP, toIP, n=5): - s = "ping -c " + str(n) + " -I " + fromIP + " " + toIP + \ - " >> " + QUIC.PING_OUTPUT + s = "ping -c " + str(n) + " -I " + fromIP + " " + toIP + " >> " + QUIC.PING_OUTPUT print(s) return s def load_parameters(self): super(QUIC, self).load_parameters() self.multipath = self.experiment_parameter.get(QUICParameter.MULTIPATH) + self.impl = self.experiment_parameter.get(QUICParameter.IMPL) + self.port = self.experiment_parameter.get(QUICParameter.PORT) + self.cert_path = self.experiment_parameter.get(QUICParameter.CERT_PATH) + self.no_verify = self.experiment_parameter.get(QUICParameter.NO_VERIFY) def prepare(self): super(QUIC, self).prepare() - self.topo.command_to(self.topo_config.client, "rm " + \ - QUIC.CLIENT_LOG ) - self.topo.command_to(self.topo_config.server, "rm " + \ - QUIC.SERVER_LOG ) + self.topo.command_to(self.topo_config.client, "rm " + QUIC.CLIENT_LOG) + self.topo.command_to(self.topo_config.server, "rm " + QUIC.SERVER_LOG) + # Fichier servi par le serveur HTTP/3 (quiche) + self.topo.command_to(self.topo_config.server, "echo hello quiche > /tmp/test.txt") def getQUICServerCmd(self): - s = QUIC.GO_BIN + " run " + QUIC.SERVER_GO_FILE - s += " -www . -certpath " + QUIC.CERTPATH + " &>" - s += QUIC.SERVER_LOG + " &" + if self.impl == "quiche": + server_bin = os.path.expanduser(QUIC.QUICHE_SERVER) + cert = os.path.join(os.path.expanduser(self.cert_path), "cert.pem") + key = os.path.join(os.path.expanduser(self.cert_path), "key.pem") + cmd = server_bin + " --listen 0.0.0.0:" + self.port + \ + " --root /tmp --cert " + cert + " --key " + key + " &> " + QUIC.SERVER_LOG + " &" + print(cmd) + return cmd + s = QUIC.GO_BIN + " run " + QUIC.SERVER_GO_FILE + " -www . -certpath " + self.cert_path + \ + " -bind 0.0.0.0:" + self.port + " &> " + QUIC.SERVER_LOG + " &" print(s) return s def getQUICClientCmd(self): + server_ip = self.topo_config.get_server_ip() + if self.impl == "quiche": + client_bin = os.path.expanduser(QUIC.QUICHE_CLIENT) + url = "https://" + server_ip + ":" + self.port + "/test.txt" + args = " --no-verify" if self.no_verify == "1" else "" + cmd = client_bin + " " + url + args + " &> " + QUIC.CLIENT_LOG + print(cmd) + return cmd s = QUIC.GO_BIN + " run " + QUIC.CLIENT_GO_FILE if int(self.multipath) > 0: s += " -m" - s += " https://" + self.topo_config.get_server_ip() + ":6121/random &>" + QUIC.CLIENT_LOG + s += " https://" + server_ip + ":" + self.port + "/random &>" + QUIC.CLIENT_LOG print(s) return s def getCongServerCmd(self, congID): - s = "python " + os.path.dirname(os.path.abspath(__file__)) + \ - "/../utils/https_server.py &> https_server" + str(congID) + ".log &" + s = "python " + os.path.dirname(os.path.abspath(__file__)) + \ + "/../utils/https_server.py &> https_server" + str(congID) + ".log &" print(s) return s def getCongClientCmd(self, congID): - s = "(time " + QUIC.WGET + " https://" + self.topo_config.getCongServerIP(congID) +\ - "/" + self.file + " --no-check-certificate --disable-mptcp) &> https_client" + str(congID) + ".log &" + s = "(time " + QUIC.WGET + " https://" + self.topo_config.getCongServerIP(congID) + \ + "/" + self.file + " --no-check-certificate --disable-mptcp) &> https_client" + str(congID) + ".log &" print(s) return s @@ -90,6 +120,10 @@ def run(self): cmd = self.getQUICServerCmd() self.topo.command_to(self.topo_config.server, "netstat -sn > netstat_server_before") self.topo.command_to(self.topo_config.server, cmd) + # Vérifs serveur + self.topo.command_to(self.topo_config.server, "ps -ef | grep -i quiche-server | grep -v grep | cat") + self.topo.command_to(self.topo_config.server, "ss -lunp | grep :" + self.port + " | cat") + self.topo.command_to(self.topo_config.server, "sleep 1; echo 'exit:' $?; head -n 60 quic_server.log | cat") if isinstance(self.topo_config, MultiInterfaceMultiClientConfig): i = 0 @@ -101,7 +135,6 @@ def run(self): self.topo.command_to(self.topo_config.client, "sleep 2") self.topo.command_to(self.topo_config.client, "netstat -sn > netstat_client_before") - # First run congestion clients, then the main one if isinstance(self.topo_config, MultiInterfaceMultiClientConfig): i = 0 for cc in self.topo_config.cong_clients: @@ -113,18 +146,19 @@ def run(self): self.topo.command_to(self.topo_config.client, cmd) self.topo.command_to(self.topo_config.server, "netstat -sn > netstat_server_after") self.topo.command_to(self.topo_config.client, "netstat -sn > netstat_client_after") - # Wait for congestion traffic to end + if isinstance(self.topo_config, MultiInterfaceMultiClientConfig): for cc in self.topo_config.cong_clients: self.topo.command_to(cc, "while pkill -f wget -0; do sleep 0.5; done") + # Arrêt propre des serveurs self.topo.command_to(self.topo_config.server, "pkill -f " + QUIC.SERVER_GO_FILE) + self.topo.command_to(self.topo_config.server, "pkill -f quiche-server || true") + if isinstance(self.topo_config, MultiInterfaceMultiClientConfig): for cs in self.topo_config.cong_servers: self.topo.command_to(cs, "pkill -f https_server.py") self.topo.command_to(self.topo_config.client, "sleep 2") - # Need to delete the go-build directory in tmp; could lead to no more space left error self.topo.command_to(self.topo_config.client, "rm -r /tmp/go-build*") - # Remove cache data self.topo.command_to(self.topo_config.client, "rm cache_*") diff --git a/experiments/quic1.py b/experiments/quic1.py new file mode 100644 index 0000000..c5a432e --- /dev/null +++ b/experiments/quic1.py @@ -0,0 +1,87 @@ +from core.experiment import RandomFileExperiment, RandomFileParameter, ExperimentParameter +import os + +class QuicheParameter(RandomFileParameter): + PORT = "quicPort" + CERT_PATH = "quicCertPath" + NO_VERIFY = "quicNoVerify" + + def __init__(self, experiment_parameter_filename): + super(QuicheParameter, self).__init__(experiment_parameter_filename) + self.default_parameters.update({ + QuicheParameter.PORT: "6121", + QuicheParameter.CERT_PATH: "/home/achraf/quiche/certs", + QuicheParameter.NO_VERIFY: "1", + }) + +class QuicheHTTP3(RandomFileExperiment): + NAME = "quic1" + PARAMETER_CLASS = QuicheParameter + + SERVER_BIN = "/home/achraf/quiche/target/release/quiche-server" + CLIENT_BIN = "/home/achraf/quiche/target/release/quiche-client" + SERVER_LOG = "/tmp/quic_server.log" + CLIENT_LOG = "/tmp/quic_client.log" + + def __init__(self, experiment_parameter_filename, topo, topo_config): + super(QuicheHTTP3, self).__init__(experiment_parameter_filename, topo, topo_config) + + def load_parameters(self): + super(QuicheHTTP3, self).load_parameters() + self.port = self.experiment_parameter.get(QuicheParameter.PORT) + self.cert_path = self.experiment_parameter.get(QuicheParameter.CERT_PATH) + self.no_verify = self.experiment_parameter.get(QuicheParameter.NO_VERIFY) + + def prepare(self): + super(QuicheHTTP3, self).prepare() + self.topo.command_to(self.topo_config.server, "rm -f " + QuicheHTTP3.SERVER_LOG) + self.topo.command_to(self.topo_config.client, "rm -f " + QuicheHTTP3.CLIENT_LOG) + # place le contenu servi + self.topo.command_to(self.topo_config.server, "echo hello quiche > /tmp/test.txt") + # copie des certs dans un chemin sûr du namespace server + self.topo.command_to(self.topo_config.server, "mkdir -p /tmp/certs && cp -f " + + os.path.join(self.cert_path, "*") + " /tmp/certs/ || true") + + def _server_cmd(self): + # utilise les certs copiés dans /tmp/certs + cert = "/tmp/certs/cert.pem" + key = "/tmp/certs/key.pem" + return (QuicheHTTP3.SERVER_BIN + + " --listen 0.0.0.0:" + self.port + + " --root /tmp --cert " + cert + + " --key " + key + + " &> " + QuicheHTTP3.SERVER_LOG + " &") + + def _client_cmd(self, server_ip): + url = "https://" + server_ip + ":" + self.port + "/test.txt" + args = " --no-verify" if str(self.no_verify) == "1" else "" + return QuicheHTTP3.CLIENT_BIN + " " + url + args + " &> " + QuicheHTTP3.CLIENT_LOG + + def run(self): + # démarrage serveur + self.topo.command_to(self.topo_config.server, self._server_cmd()) + + # diagnostic de démarrage (affiche erreur immédiate si ça plante) + diag = (QuicheHTTP3.SERVER_BIN + " --listen 0.0.0.0:" + self.port + + " --root /tmp --cert /tmp/certs/cert.pem --key /tmp/certs/key.pem") + self.topo.command_to(self.topo_config.server, "timeout 3s " + diag + " 2>&1 | head -n 80 | cat || true") + + # vérifs serveur + logs + self.topo.command_to(self.topo_config.server, "ps -ef | grep -i quiche-server | grep -v grep | cat") + self.topo.command_to(self.topo_config.server, "ss -lunp | grep :" + self.port + " | cat") + self.topo.command_to(self.topo_config.server, "head -n 80 " + QuicheHTTP3.SERVER_LOG + " | cat") + + # petite pause + self.topo.command_to(self.topo_config.client, "sleep 1") + + # client + server_ip = self.topo_config.get_server_ip() + self.topo.command_to(self.topo_config.client, self._client_cmd(server_ip)) + self.topo.command_to(self.topo_config.client, "head -n 80 " + QuicheHTTP3.CLIENT_LOG + " | cat") + + # rapatrier les logs dans le répertoire hôte (si présent) + self.topo.command_to(self.topo_config.server, "test -f /tmp/quic_server.log && cat /tmp/quic_server.log | tee -a logs/quic_server.ns.log >/dev/null || true") + self.topo.command_to(self.topo_config.client, "test -f /tmp/quic_client.log && cat /tmp/quic_client.log | tee -a logs/quic_client.ns.log >/dev/null || true") + + # cleanup + self.topo.command_to(self.topo_config.server, "pkill -f quiche-server || true") diff --git a/experiments/quic_mpath.py b/experiments/quic_mpath.py new file mode 100644 index 0000000..e3ec3fc --- /dev/null +++ b/experiments/quic_mpath.py @@ -0,0 +1,69 @@ +import logging, traceback +from core.experiment import Experiment + +class QuicMpath(Experiment): + NAME = "quic_mpath" + + def run(self): + print(">>> [quic_mpath] run() ENTER", flush=True) + try: + get = self.experiment_parameter.get + + server_ip = get("serverIP") + client_ip_a = get("clientIPA") + client_ip_b = get("clientIPB") + port = int(get("port")) + server_bin = get("serverBin") + client_bin = get("clientBin") + cert = get("cert") + key = get("key") + root = get("root") + cap_if1 = get("capIf1") + cap_if2 = get("capIf2") + cap_raw = get("capCount") + cap_count = int(cap_raw) if cap_raw not in (None, "", "None") else 200 + + print(f"[quic_mpath] params: srv={server_ip}:{port} cliA={client_ip_a} cliB={client_ip_b} cap_if=({cap_if1},{cap_if2}) cap_count={cap_count}", flush=True) + + # --- Sanity: binaires/certificats & interfaces côté r1 --- + self.topo.command_to(self.topo_config.server, f"test -x {server_bin} && echo '[h2] serverBin OK' || echo '[h2] serverBin MISSING: {server_bin}'") + self.topo.command_to(self.topo_config.client, f"test -x {client_bin} && echo '[h1] clientBin OK' || echo '[h1] clientBin MISSING: {client_bin}'") + self.topo.command_to(self.topo_config.server, f"test -r {cert} && test -r {key} && echo '[h2] cert/key OK' || echo '[h2] cert/key MISSING'") + self.topo.command_to(self.topo_config.router, "echo '[r1] ifaces:'; ip -o link | awk -F': ' '{print $2}' | xargs -n1 echo ' -' > /tmp/r1_ifaces.txt; cat /tmp/r1_ifaces.txt") + self.topo.command_to(self.topo_config.router, "echo '[r1] tcpdump -D:'; tcpdump -D > /tmp/r1_tcpdumpD.txt 2>&1; tail -n +1 /tmp/r1_tcpdumpD.txt") + + # --- 1) serveur QUIC --- + cmd_srv = f"nohup {server_bin} --cert {cert} --key {key} --listen 0.0.0.0:{port} --root {root} > /tmp/qserver.log 2>&1 &" + print(f"[quic_mpath] h2$ {cmd_srv}", flush=True) + self.topo.command_to(self.topo_config.server, cmd_srv) + + # --- 2) tcpdump avec logs d’erreurs --- + cmd_cap1 = f"tcpdump -ni {cap_if1} udp port {port} -c {cap_count} -U -vvv -w /tmp/pathA.pcap 2> /tmp/tcpdumpA.err &" + cmd_cap2 = f"tcpdump -ni {cap_if2} udp port {port} -c {cap_count} -U -vvv -w /tmp/pathB.pcap 2> /tmp/tcpdumpB.err &" + print(f"[quic_mpath] r1$ {cmd_cap1}", flush=True) + self.topo.command_to(self.topo_config.router, cmd_cap1) + print(f"[quic_mpath] r1$ {cmd_cap2}", flush=True) + self.topo.command_to(self.topo_config.router, cmd_cap2) + + # --- 3) clients (redirigés vers logs) --- + cmd_cli1 = f"LOCAL_BIND={client_ip_a} {client_bin} https://{server_ip}:{port}/ --no-verify > /tmp/clientA.log 2>&1 &" + cmd_cli2 = f"LOCAL_BIND={client_ip_b} {client_bin} https://{server_ip}:{port}/ --no-verify > /tmp/clientB.log 2>&1 &" + print(f"[quic_mpath] h1$ {cmd_cli1}", flush=True) + self.topo.command_to(self.topo_config.client, cmd_cli1) + print(f"[quic_mpath] h1$ {cmd_cli2}", flush=True) + self.topo.command_to(self.topo_config.client, cmd_cli2) + + # --- 4) attente + stop tcpdump --- + print("[quic_mpath] sleeping 12s then stopping tcpdump…", flush=True) + self.topo.command_to(self.topo_config.router, "sleep 12; pkill -f 'tcpdump -ni' || true") + + # --- 5) bilan fichiers --- + self.topo.command_to(self.topo_config.router, "echo '[r1] pcaps:'; ls -lh /tmp/pathA.pcap /tmp/pathB.pcap || true; echo '[r1] tcpdump errs:'; tail -n +1 /tmp/tcpdumpA.err /tmp/tcpdumpB.err || true") + self.topo.command_to(self.topo_config.client, "echo '[h1] client logs:'; tail -n +1 /tmp/clientA.log /tmp/clientB.log || true") + self.topo.command_to(self.topo_config.server, "echo '[h2] qserver.log:'; tail -n 30 /tmp/qserver.log || true") + + print("[quic_mpath] run() EXIT", flush=True) + + except Exception as e: + print("[quic_mpath] EXCEPTION:", e, flush=True) + traceback.print_exc() diff --git a/netstat_client_after b/netstat_client_after new file mode 100644 index 0000000..ab86feb --- /dev/null +++ b/netstat_client_after @@ -0,0 +1,41 @@ +Ip: + Forwarding: 2 + 0 total packets received + 0 forwarded + 0 incoming packets discarded + 0 incoming packets delivered + 18 requests sent out +Icmp: + 0 ICMP messages received + 0 input ICMP message failed + ICMP input histogram: + 10 ICMP messages sent + 0 ICMP messages failed + ICMP output histogram: + echo requests: 10 +IcmpMsg: + OutType8: 10 +Tcp: + 0 active connection openings + 0 passive connection openings + 0 failed connection attempts + 0 connection resets received + 0 connections established + 0 segments received + 0 segments sent out + 0 segments retransmitted + 0 bad segments received + 0 resets sent +Udp: + 0 packets received + 0 packets to unknown port received + 0 packet receive errors + 8 packets sent + 0 receive buffer errors + 0 send buffer errors +UdpLite: +TcpExt: + 0 packet headers predicted +IpExt: + OutOctets: 10664 +MPTcpExt: diff --git a/netstat_client_before b/netstat_client_before new file mode 100644 index 0000000..1af6284 --- /dev/null +++ b/netstat_client_before @@ -0,0 +1,41 @@ +Ip: + Forwarding: 2 + 0 total packets received + 0 forwarded + 0 incoming packets discarded + 0 incoming packets delivered + 10 requests sent out +Icmp: + 0 ICMP messages received + 0 input ICMP message failed + ICMP input histogram: + 10 ICMP messages sent + 0 ICMP messages failed + ICMP output histogram: + echo requests: 10 +IcmpMsg: + OutType8: 10 +Tcp: + 0 active connection openings + 0 passive connection openings + 0 failed connection attempts + 0 connection resets received + 0 connections established + 0 segments received + 0 segments sent out + 0 segments retransmitted + 0 bad segments received + 0 resets sent +Udp: + 0 packets received + 0 packets to unknown port received + 0 packet receive errors + 0 packets sent + 0 receive buffer errors + 0 send buffer errors +UdpLite: +TcpExt: + 0 packet headers predicted +IpExt: + OutOctets: 840 +MPTcpExt: diff --git a/netstat_server_after b/netstat_server_after new file mode 100644 index 0000000..f23b25f --- /dev/null +++ b/netstat_server_after @@ -0,0 +1,37 @@ +Ip: + Forwarding: 2 + 0 total packets received + 0 forwarded + 0 incoming packets discarded + 0 incoming packets delivered + 0 requests sent out +Icmp: + 0 ICMP messages received + 0 input ICMP message failed + ICMP input histogram: + 0 ICMP messages sent + 0 ICMP messages failed + ICMP output histogram: +Tcp: + 0 active connection openings + 0 passive connection openings + 0 failed connection attempts + 0 connection resets received + 0 connections established + 0 segments received + 0 segments sent out + 0 segments retransmitted + 0 bad segments received + 0 resets sent +Udp: + 0 packets received + 0 packets to unknown port received + 0 packet receive errors + 0 packets sent + 0 receive buffer errors + 0 send buffer errors +UdpLite: +TcpExt: + 0 packet headers predicted +IpExt: +MPTcpExt: diff --git a/netstat_server_before b/netstat_server_before new file mode 100644 index 0000000..f23b25f --- /dev/null +++ b/netstat_server_before @@ -0,0 +1,37 @@ +Ip: + Forwarding: 2 + 0 total packets received + 0 forwarded + 0 incoming packets discarded + 0 incoming packets delivered + 0 requests sent out +Icmp: + 0 ICMP messages received + 0 input ICMP message failed + ICMP input histogram: + 0 ICMP messages sent + 0 ICMP messages failed + ICMP output histogram: +Tcp: + 0 active connection openings + 0 passive connection openings + 0 failed connection attempts + 0 connection resets received + 0 connections established + 0 segments received + 0 segments sent out + 0 segments retransmitted + 0 bad segments received + 0 resets sent +Udp: + 0 packets received + 0 packets to unknown port received + 0 packet receive errors + 0 packets sent + 0 receive buffer errors + 0 send buffer errors +UdpLite: +TcpExt: + 0 packet headers predicted +IpExt: +MPTcpExt: diff --git a/runner.py b/runner.py index 4721558..c3bbb8f 100644 --- a/runner.py +++ b/runner.py @@ -1,127 +1,222 @@ -#!/usr/bin/python - -from core.experiment import Experiment, ExperimentParameter, ExperimentParameter -from core.topo import Topo, TopoParameter +#!/usr/bin/python3 +import logging +import os +import subprocess +import tempfile +import traceback +from pathlib import Path -from mininet_builder import MininetBuilder +import yaml from mininet.clean import cleanup - +from mininet.cli import CLI +from core.experiment import Experiment, ExperimentParameter +from core.topo import Topo, TopoParameter from experiments import EXPERIMENTS +from mininet_builder import MininetBuilder from topos import TOPO_CONFIGS, TOPOS -import logging -import os -import subprocess -import traceback def get_git_revision_short_hash(): # Because we might run Minitopo from elsewhere. curr_dir = os.getcwd() ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) os.chdir(ROOT_DIR) - ret = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode("unicode_escape").strip() + ret = subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"] + ).decode("unicode_escape").strip() os.chdir(curr_dir) return ret + +def _is_yaml(path: str) -> bool: + return Path(path).suffix.lower() in {".yaml", ".yml"} + + +def _yaml_topo_to_legacy_tmpfile(yaml_path: str) -> str: + with open(yaml_path, "r") as f: + y = yaml.safe_load(f) + + t = y["topology"] + left = str(t["subnets"]["left"]) + right = str(t["subnets"]["right"]) + topo_type = str(t["type"]) + + lines = [f"leftSubnet:{left}", f"rightSubnet:{right}"] + + for idx, p in enumerate(t.get("paths", [])): + delay = p["delay_ms"] + queue = p.get("queue_pkts", 100) + bw = p["bw_mbit"] + triplet = f"{delay},{queue},{bw}" + + if "link_type" in p and "id" in p: + key = f"path_{p['link_type']}_{p['id']}" + else: + name = str(p.get("name", f"c2r_{idx}")) # legacy fallback + key = f"path_{name}" + + lines.append(f"{key}:{triplet}") + + lines.append(f"topoType:{topo_type}") + + tmp = tempfile.NamedTemporaryFile( + prefix="mtopo_topo_", suffix=".para", delete=False, mode="w" + ) + tmp.write("\n".join(lines) + "\n") + tmp.flush() + tmp.close() + return tmp.name + + class Runner(object): """ Run an experiment described by `experiment_parameter_file` in the topology described by `topo_parameter_file` in the network environment built by `builder_type`. - All the operations are done when calling the constructor. """ + def __init__(self, builder_type, topo_parameter_file, experiment_parameter_file): logging.info("Minitopo version {}".format(get_git_revision_short_hash())) - self.topo_parameter = TopoParameter(topo_parameter_file) + self._tmp_files = [] + + #Charger/convertir le fichier topo + topo_param_path = topo_parameter_file + if _is_yaml(topo_param_path): + topo_param_path = _yaml_topo_to_legacy_tmpfile(topo_param_path) + self._tmp_files.append(topo_param_path) + logging.info( + f"Converted YAML topo -> legacy: {topo_parameter_file} -> {topo_param_path}" + ) + + #initialiser self.topo_parameter + self.topo_parameter = TopoParameter(topo_param_path) + + #Builder + Topo + Config self.set_builder(builder_type) self.apply_topo() self.apply_topo_config() + + # 3) Démarrer le réseau self.start_topo() - self.run_experiment(experiment_parameter_file) - self.stop_topo() + if experiment_parameter_file: + self.run_experiment(experiment_parameter_file) + self.stop_topo() + else: + # Pas d’XP -> on ouvre la CLI et on ne stoppe qu’à la sortie + self.open_cli() + self.stop_topo() + #Lancer l'expérience si fournie (sinon on garde la CLI Mininet) + if experiment_parameter_file: + self.run_experiment(experiment_parameter_file) + # Si une expérience est lancée, on peut arrêter ensuite + self.stop_topo() + + def __del__(self): + # Meilleure chance de nettoyage des tmp si le process se termine "proprement" + self._cleanup_tmp_files() + + def _cleanup_tmp_files(self): + for p in self._tmp_files: + try: + os.unlink(p) + except Exception: + pass + self._tmp_files.clear() def set_builder(self, builder_type): - """ - Currently the only builder type supported is Mininet... - """ + """Currently the only builder type supported is Mininet...""" if builder_type == Topo.MININET_BUILDER: self.topo_builder = MininetBuilder() else: raise Exception("I can not find the builder {}".format(builder_type)) def apply_topo(self): - """ - Matches the name of the topo and find the corresponding Topo class. - """ + """Matches the name of the topo and find the corresponding Topo class.""" t = self.topo_parameter.get(Topo.TOPO_ATTR) if t in TOPOS: self.topo = TOPOS[t](self.topo_builder, self.topo_parameter) else: raise Exception("Unknown topo: {}".format(t)) - logging.info("Using topo {}".format(self.topo)) def apply_topo_config(self): - """ - Match the name of the topo and find the corresponding TopoConfig class. - """ + """Match the name of the topo and find the corresponding TopoConfig class.""" t = self.topo_parameter.get(Topo.TOPO_ATTR) if t in TOPO_CONFIGS: self.topo_config = TOPO_CONFIGS[t](self.topo, self.topo_parameter) else: raise Exception("Unknown topo config: {}".format(t)) - logging.info("Using topo config {}".format(self.topo_config)) def start_topo(self): - """ - Initialize the topology with its configuration - """ + """Initialize the topology with its configuration""" self.topo.start_network() self.topo_config.configure_network() def run_experiment(self, experiment_parameter_file): - """ - Match the name of the experiement and launch it - """ - # Well, we need to load twice the experiment parameters, is it really annoying? - xp = ExperimentParameter(experiment_parameter_file).get(ExperimentParameter.XP_TYPE) + """Match the name of the experiment and launch it""" + xp = ExperimentParameter(experiment_parameter_file).get( + ExperimentParameter.XP_TYPE + ) if xp in EXPERIMENTS: exp = EXPERIMENTS[xp](experiment_parameter_file, self.topo, self.topo_config) exp.classic_run() else: raise Exception("Unknown experiment {}".format(xp)) + def open_cli(self): + + net = getattr(self.topo_builder, "net", None) + + # fallback au cas où certains Topo exposent .net + if net is None: + net = getattr(self.topo, "net", None) + + if net is None: + raise AttributeError( + "Impossible de trouver l'instance Mininet 'net' " + "(builder/topo). As-tu bien appelé start_network() ?" + ) + + logging.info("Opening Mininet CLI (tape 'exit' pour quitter).") + CLI(net) + def stop_topo(self): - """ - Stop the topology - """ - self.topo.stop_network() + """Stop the topology""" + try: + self.topo.stop_network() + finally: + self._cleanup_tmp_files() -if __name__ == '__main__': +if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( - description="Minitopo, a wrapper of Mininet to run multipath experiments") - - parser.add_argument("--topo_param_file", "-t", required=True, - help="path to the topo parameter file") - parser.add_argument("--experiment_param_file", "-x", - help="path to the experiment parameter file") - + description="Minitopo, a wrapper of Mininet to run multipath experiments" + ) + parser.add_argument( + "--topo_param_file", "-t", required=True, help="path to the topo parameter file" + ) + parser.add_argument( + "--experiment_param_file", + "-x", + help="path to the experiment parameter file (optional)", + ) args = parser.parse_args() - logging.basicConfig(format="%(asctime)-15s [%(levelname)s] %(funcName)s: %(message)s", level=logging.INFO) + logging.basicConfig( + level=logging.INFO, + format="%(asctime)-15s [%(levelname)s] %(name)s:%(funcName)s: %(message)s", + force=True, + ) - # XXX Currently, there is no alternate topo builder... try: Runner(Topo.MININET_BUILDER, args.topo_param_file, args.experiment_param_file) except Exception as e: - logging.fatal("A fatal error occurred: {}".format(e)) + logging.fatal("A fatal error occurred: %s", e) traceback.print_exc() finally: - # Always cleanup Mininet logging.info("cleanup mininet") - cleanup() \ No newline at end of file + cleanup() diff --git a/runner.py.save b/runner.py.save new file mode 100644 index 0000000..0427d60 --- /dev/null +++ b/runner.py.save @@ -0,0 +1,149 @@ +#!/usr/bin/python + +from core.experiment import Experiment, ExperimentParameter, ExperimentParameter +from core.topo import Topo, TopoParameter + +from mininet_builder import MininetBuilder +from mininet.clean import cleanup + +from experiments import EXPERIMENTS +from topos import TOPO_CONFIGS, TOPOS +from pathlib import Path + +import logging +import os +import subprocess +import traceback +import tempfile +import yaml + +def get_git_revision_short_hash(): + # Because we might run Minitopo from elsewhere. + curr_dir = os.getcwd() + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + os.chdir(ROOT_DIR) + ret = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode("unicode_escape").strip() + os.chdir(curr_dir) + return ret + +def _is_yaml(path: str) -> bool: + return Path(path).suffix.lower() in {".yaml", ".yml"} + + + +class Runner(object): + """ + Run an experiment described by `experiment_parameter_file` in the topology + described by `topo_parameter_file` in the network environment built by + `builder_type`. + + All the operations are done when calling the constructor. + """ + def __init__(self, builder_type, topo_parameter_file, experiment_parameter_file): + logging.info("Minitopo version {}".format(get_git_revision_short_hash())) + self._tmp_files = [] + topo_param_path = topo_parameter_file + if _is_yaml(topo_param_path): + topo_param_path = _yaml_topo_to_legacy_tmpfile(topo_param_path) + self._tmp_files.append(topo_param_path) + logging.info(f"Converted YAML topo -> legacy: {topo_parameter_file} -> {topo_param_path}") + + # Create topology parameter object as usual + self.topo_parameter = TopoParameter(topo_param_path) + self.set_builder(builder_type) + self.apply_topo() + self.apply_topo_config() + self.start_topo() + self.run_experiment(experiment_parameter_file) + self.stop_topo() + + def set_builder(self, builder_type): + """ + Currently the only builder type supported is Mininet... + """ + if builder_type == Topo.MININET_BUILDER: + self.topo_builder = MininetBuilder() + else: + raise Exception("I can not find the builder {}".format(builder_type)) + + def apply_topo(self): + """ + Matches the name of the topo and find the corresponding Topo class. + """ + t = self.topo_parameter.get(Topo.TOPO_ATTR) + if t in TOPOS: + self.topo = TOPOS[t](self.topo_builder, self.topo_parameter) + else: + raise Exception("Unknown topo: {}".format(t)) + + logging.info("Using topo {}".format(self.topo)) + + def apply_topo_config(self): + """ + Match the name of the topo and find the corresponding TopoConfig class. + """ + t = self.topo_parameter.get(Topo.TOPO_ATTR) + if t in TOPO_CONFIGS: + self.topo_config = TOPO_CONFIGS[t](self.topo, self.topo_parameter) + else: + raise Exception("Unknown topo config: {}".format(t)) + + logging.info("Using topo config {}".format(self.topo_config)) + + def start_topo(self): + """ + Initialize the topology with its configuration + """ + self.topo.start_network() + self.topo_config.configure_network() + + def run_experiment(self, experiment_parameter_file): + """ + Match the name of the experiement and launch it + """ + # Well, we need to load twice the experiment parameters, is it really annoying? + xp = ExperimentParameter(experiment_parameter_file).get(ExperimentParameter.XP_TYPE) + if xp in EXPERIMENTS: + exp = EXPERIMENTS[xp](experiment_parameter_file, self.topo, self.topo_config) + exp.classic_run() + else: + raise Exception("Unknown experiment {}".format(xp)) + + def stop_topo(self): + """ + Stop the topology + """ + self.topo.stop_network() + for p in getattr(self, "_tmp_files", []): + try: + os.unlink(p) + except: + pass + + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description="Minitopo, a wrapper of Mininet to run multipath experiments") + + parser.add_argument("--topo_param_file", "-t", required=True, + help="path to the topo parameter file") + parser.add_argument("--experiment_param_file", "-x", + help="path to the experiment parameter file") + + args = parser.parse_args() + + logging.basicConfig(format="%(asctime)-15s [%(levelname)s] %(funcName)s: %(message)s", level=logging.INFO) + + # XXX Currently, there is no alternate topo builder... + try: + Runner(Topo.MININET_BUILDER, args.topo_param_file, args.experiment_param_file) + except Exception as e: + logging.fatal("A fatal error occurred: {}".format(e)) + traceback.print_exc() + finally: + # Always cleanup Mininet + logging.info("cleanup mininet") + cleanup() diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..41b019b --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +hello quiche diff --git a/topos/__init__.py b/topos/__init__.py index f5b7452..9c1181e 100644 --- a/topos/__init__.py +++ b/topos/__init__.py @@ -1,7 +1,6 @@ import importlib import pkgutil import os - from core.topo import Topo, TopoConfig pkg_dir = os.path.dirname(__file__) @@ -20,4 +19,4 @@ def _get_all_subclasses(BaseClass, dico): _get_all_subclasses(cls, dico) _get_all_subclasses(TopoConfig, TOPO_CONFIGS) -_get_all_subclasses(Topo, TOPOS) \ No newline at end of file +_get_all_subclasses(Topo, TOPOS) diff --git a/topos/multi_interface.py b/topos/multi_interface.py index 925292b..542e1c3 100644 --- a/topos/multi_interface.py +++ b/topos/multi_interface.py @@ -76,113 +76,185 @@ def __init__(self, topo, param): super(MultiInterfaceConfig, self).__init__(topo, param) def configure_routing(self): - for i, _ in enumerate(self.topo.c2r_links): - cmd = self.add_table_route_command(self.get_client_ip(i), i) - self.topo.command_to(self.client, cmd) + print("\n=== CONFIGURE ROUTING (MultiInterfaceConfig) ===") - cmd = self.add_link_scope_route_command( - self.get_client_subnet(i), - self.get_client_interface(0, i), i) - self.topo.command_to(self.client, cmd) + # routes côté client (une table par lien c2r) + for i, _ in enumerate(self.topo.c2r_links): + print(f"[CLIENT] table={i} ip={self.get_client_ip(i)} subnet={self.get_client_subnet(i)} via {self.get_router_ip_to_client_switch(i)}") + cmd = self.add_table_route_command(self.get_client_ip(i), i) + print(f"[Client] $ {cmd}") + self.topo.command_to(self.client, cmd) - cmd = self.add_table_default_route_command(self.get_router_ip_to_client_switch(i), - i) - self.topo.command_to(self.client, cmd) + cmd = self.add_link_scope_route_command(self.get_client_subnet(i), self.get_client_interface(0, i), i) + print(f"[Client] $ {cmd}") + self.topo.command_to(self.client, cmd) - for i, _ in enumerate(self.topo.r2s_links): - cmd = self.add_table_route_command(self.get_server_ip(i), i) - self.topo.command_to(self.server, cmd) + cmd = self.add_table_default_route_command(self.get_router_ip_to_client_switch(i), i) + print(f"[Client] $ {cmd}") + self.topo.command_to(self.client, cmd) - cmd = self.add_link_scope_route_command( - self.get_server_subnet(i), - self.get_server_interface(0, i), i) - self.topo.command_to(self.server, cmd) + # routes côté serveur (une table par lien r2s) + for i, _ in enumerate(self.topo.r2s_links): + print(f"[SERVER] table={i} ip={self.get_server_ip(i)} subnet={self.get_server_subnet(i)} via {self.get_router_ip_to_server_switch(i)}") + cmd = self.add_table_route_command(self.get_server_ip(i), i) + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) - cmd = self.add_table_default_route_command(self.get_router_ip_to_server_switch(i), - i) - self.topo.command_to(self.server, cmd) + cmd = self.add_link_scope_route_command(self.get_server_subnet(i), self.get_server_interface(0, i), i) + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) - cmd = self.add_global_default_route_command(self.get_router_ip_to_client_switch(0), - self.get_client_interface(0, 0)) - self.topo.command_to(self.client, cmd) + cmd = self.add_table_default_route_command(self.get_router_ip_to_server_switch(i), i) + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) - cmd = self.add_simple_default_route_command(self.get_router_ip_to_server_switch(0)) - self.topo.command_to(self.server, cmd) + # routes par défaut + cmd = self.add_global_default_route_command(self.get_router_ip_to_client_switch(0), self.get_client_interface(0, 0)) + print(f"[Client DEFAULT] $ {cmd}") + self.topo.command_to(self.client, cmd) + + cmd = self.add_simple_default_route_command(self.get_router_ip_to_server_switch(0)) + print(f"[Server DEFAULT] $ {cmd}") + self.topo.command_to(self.server, cmd) + + # Snapshot des routes résultantes + print("\n[STATE] routes résultantes") + print("[Client]\n", self.topo.command_to(self.client, "ip rule; echo; ip route show table main; echo; for i in $(seq 0 4); do ip route show table $i; done")) + print("[Server]\n", self.topo.command_to(self.server, "ip rule; echo; ip route show table main; echo; for i in $(seq 0 4); do ip route show table $i; done")) def configure_interfaces(self): - logging.info("Configure interfaces using MultiInterfaceConfig...") - super(MultiInterfaceConfig, self).configure_interfaces() - self.client = self.topo.get_client(0) - self.server = self.topo.get_server(0) - self.router = self.topo.get_router(0) - netmask = "255.255.255.0" - - for i, _ in enumerate(self.topo.c2r_links): - cmd = self.interface_up_command(self.get_client_interface(0, i), self.get_client_ip(i), netmask) - self.topo.command_to(self.client, cmd) - client_interface_mac = self.client.intf(self.get_client_interface(0, i)).MAC() - self.topo.command_to(self.router, "arp -s {} {}".format(self.get_client_ip(i), client_interface_mac)) - - if self.topo.get_client_to_router_links()[i].backup: - cmd = self.interface_backup_command(self.get_client_interface(0, i)) - self.topo.command_to(self.client, cmd) - - for i, _ in enumerate(self.topo.c2r_links): - cmd = self.interface_up_command(self.get_router_interface_to_client_switch(i), - self.get_router_ip_to_client_switch(i), netmask) - self.topo.command_to(self.router, cmd) - router_interface_mac = self.router.intf(self.get_router_interface_to_client_switch(i)).MAC() - self.topo.command_to(self.client, "arp -s {} {}".format( - self.get_router_ip_to_client_switch(i), router_interface_mac)) - - if len(self.topo.r2s_links) == 0: - # Case no server param is specified - cmd = self.interface_up_command(self.get_router_interface_to_server_switch(0), - self.get_router_ip_to_server_switch(0), netmask) - self.topo.command_to(self.router, cmd) - router_interface_mac = self.router.intf(self.get_router_interface_to_server_switch(0)).MAC() - self.topo.command_to(self.server, "arp -s {} {}".format( - self.get_router_ip_to_server_switch(0), router_interface_mac)) - - cmd = self.interface_up_command(self.get_server_interface(0, 0), self.get_server_ip(0), netmask) - self.topo.command_to(self.server, cmd) - server_interface_mac = self.server.intf(self.get_server_interface(0, 0)).MAC() - self.topo.command_to(self.router, "arp -s {} {}".format( - self.get_server_ip(0), server_interface_mac)) - - for i, _ in enumerate(self.topo.r2s_links): - cmd = self.interface_up_command(self.get_router_interface_to_server_switch(i), - self.get_router_ip_to_server_switch(i), netmask) - self.topo.command_to(self.router, cmd) - router_interface_mac = self.router.intf(self.get_router_interface_to_server_switch(i)).MAC() - self.topo.command_to(self.server, "arp -s {} {}".format( - self.get_router_ip_to_server_switch(i), router_interface_mac)) - - for i, _ in enumerate(self.topo.r2s_links): - cmd = self.interface_up_command(self.get_server_interface(0, i), self.get_server_ip(i), netmask) - self.topo.command_to(self.server, cmd) - server_interface_mac = self.server.intf(self.get_server_interface(0, i)).MAC() - self.topo.command_to(self.router, "arp -s {} {}".format( - self.get_server_ip(i), server_interface_mac)) + logging.info("=== CONFIGURE INTERFACES (MultiInterfaceConfig) ===") + super(MultiInterfaceConfig, self).configure_interfaces() + self.client = self.topo.get_client(0) + self.server = self.topo.get_server(0) + self.router = self.topo.get_router(0) + print("[DEBUG] rightSubnet (param) =", repr(self.param.get("rightSubnet"))) + print("[DEBUG] rightSubnet (topo_parameter) =", repr(getattr(self.topo.topo_parameter, "get", lambda *_: None)("rightSubnet"))) + netmask = "255.255.255.0" + + # Affiche les préfixes lus + print(f"[INFO] leftSubnet={self.param.get('leftSubnet')} rightSubnet={self.param.get('rightSubnet')}") + + # --- C2R : Client <-> Router --- + for i, _ in enumerate(self.topo.c2r_links): + cli_if = self.get_client_interface(0, i) + cli_ip = self.get_client_ip(i) + rtr_if = self.get_router_interface_to_client_switch(i) + rtr_ip = self.get_router_ip_to_client_switch(i) + + print(f"[C2R#{i}] {cli_if}={cli_ip}/24 <-> {rtr_if}={rtr_ip}/24") + + cmd = self.interface_up_command(cli_if, cli_ip, netmask) + print(f"[Client] $ {cmd}") + self.topo.command_to(self.client, cmd) + + client_interface_mac = self.client.intf(cli_if).MAC() + cmd = f"arp -s {cli_ip} {client_interface_mac}" + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + for i, _ in enumerate(self.topo.c2r_links): + rtr_if = self.get_router_interface_to_client_switch(i) + rtr_ip = self.get_router_ip_to_client_switch(i) + cmd = self.interface_up_command(rtr_if, rtr_ip, netmask) + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + router_interface_mac = self.router.intf(rtr_if).MAC() + cmd = f"arp -s {rtr_ip} {router_interface_mac}" + print(f"[Client] $ {cmd}") + self.topo.command_to(self.client, cmd) + + # --- R2S : Router <-> Server --- + if len(self.topo.r2s_links) == 0: + s_if = self.get_server_interface(0, 0) + s_ip = self.get_server_ip(0) + r_if = self.get_router_interface_to_server_switch(0) + r_ip = self.get_router_ip_to_server_switch(0) + + print(f"[R2S(auto)] {r_if}={r_ip}/24 <-> {s_if}={s_ip}/24") + self.topo.command_to(self.router, f"ip -4 addr flush dev {r_if}") + self.topo.command_to(self.server, f"ip -4 addr flush dev {s_if}") + cmd = self.interface_up_command(r_if, r_ip, netmask) + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + router_interface_mac = self.router.intf(r_if).MAC() + cmd = f"arp -s {r_ip} {router_interface_mac}" + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) + + cmd = self.interface_up_command(s_if, s_ip, netmask) + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) + + server_interface_mac = self.server.intf(s_if).MAC() + cmd = f"arp -s {s_ip} {server_interface_mac}" + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + else: + for i, _ in enumerate(self.topo.r2s_links): + s_if = self.get_server_interface(0, i) + s_ip = self.get_server_ip(i) + r_if = self.get_router_interface_to_server_switch(i) + r_ip = self.get_router_ip_to_server_switch(i) + + print(f"[R2S#{i}] {r_if}={r_ip}/24 <-> {s_if}={s_ip}/24") + + print(f"[Router] $ ip -4 addr flush dev {r_if}") + self.topo.command_to(self.router, f"ip -4 addr flush dev {r_if}") + print(f"[Server] $ ip -4 addr flush dev {s_if}") + self.topo.command_to(self.server, f"ip -4 addr flush dev {s_if}") + cmd = self.interface_up_command(r_if, r_ip, netmask) + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + router_interface_mac = self.router.intf(r_if).MAC() + cmd = f"arp -s {r_ip} {router_interface_mac}" + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) + + cmd = self.interface_up_command(s_if, s_ip, netmask) + print(f"[Server] $ {cmd}") + self.topo.command_to(self.server, cmd) + + server_interface_mac = self.server.intf(s_if).MAC() + cmd = f"arp -s {s_ip} {server_interface_mac}" + print(f"[Router] $ {cmd}") + self.topo.command_to(self.router, cmd) + + # Snapshot rapide des IPs réellement posées + print("\n[STATE] ip -br -4 addr") + print("[Client]\n", self.topo.command_to(self.client, "ip -br -4 addr")) + print("[Router]\n", self.topo.command_to(self.router, "ip -br -4 addr")) + print("[Server]\n", self.topo.command_to(self.server, "ip -br -4 addr")) + def get_client_ip(self, interface_index): - return "{}{}.1".format(self.param.get(TopoParameter.LEFT_SUBNET), interface_index) + # ex: leftSubnet: "10.0." -> "10.0..1" + return f"{self.param.get('leftSubnet')}{interface_index}.1" def get_client_subnet(self, interface_index): - return "{}{}.0/24".format(self.param.get(TopoParameter.LEFT_SUBNET), interface_index) + # ex: "10.0..0/24" + return f"{self.param.get('leftSubnet')}{interface_index}.0/24" def get_router_ip_to_client_switch(self, switch_index): - return "{}{}.2".format(self.param.get(TopoParameter.LEFT_SUBNET), switch_index) + # ex: "10.0..2" + return f"{self.param.get('leftSubnet')}{switch_index}.2" def get_router_ip_to_server_switch(self, switch_index): - return "{}{}.2".format(self.param.get(TopoParameter.RIGHT_SUBNET), switch_index) + # ex: rightSubnet: "10.1." -> "10.1..2" + return f"{self.param.get('rightSubnet')}{switch_index}.2" def get_server_ip(self, interface_index=0): - return "{}{}.1".format(self.param.get(TopoParameter.RIGHT_SUBNET), interface_index) + # ex: "10.1..1" (=> Server_0 = 10.1.0.1 si idx=0) + return f"{self.param.get('rightSubnet')}{interface_index}.1" def get_server_subnet(self, interface_index): - return "{}{}.0/24".format(self.param.get(TopoParameter.RIGHT_SUBNET), interface_index) + # ex: "10.1..0/24" + return f"{self.param.get('rightSubnet')}{interface_index}.0/24" def client_interface_count(self): return max(len(self.topo.c2r_links), 1) @@ -200,4 +272,4 @@ def get_router_interface_to_client_switch(self, interface_index): return "{}-eth{}".format(self.topo.get_router_name(0), interface_index) def get_server_interface(self, server_index, interface_index): - return "{}-eth{}".format(self.topo.get_server_name(server_index), interface_index) \ No newline at end of file + return "{}-eth{}".format(self.topo.get_server_name(server_index), interface_index)