From 6c726a67e749d31cb9032287811d0f93b976bffb Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Wed, 8 Oct 2025 15:43:12 +0200 Subject: [PATCH 01/10] Update README.rst --- README.rst | 78 +++++++++++++++++------------------------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index 8c2f20f..9fae2e2 100644 --- a/README.rst +++ b/README.rst @@ -1,63 +1,33 @@ -What ? -====== +# Mininet Runner – Multi-path Experiment Framework (Step-by-Step Guide) -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 easily launch +network topologies with **multiple paths** and run **custom experiments** +between hosts (e.g., TCP, UDP, QUIC, or others). +> **Default command:** +> ```bash +> sudo python3 runner.py -t -x +> ``` +> *(the old `./mpPerf` command is deprecated)* -Usage -===== +--- -.. code-block:: console +## 0) Goal of this mini-tutorial - ./mpPerf -t topo -x xp +The framework can be used for many network experiments. +In this tutorial, we’ll specifically demonstrate **how to set up a Multipath QUIC +connection** between a client and a server through a router, capture the traffic, +and prepare data for **fingerprinting**. -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 -============= +## 1) System requirements -1. Get the CLI --------------- +- Ubuntu / WSL with `sudo` privileges +- Python ≥ 3.8 +- Mininet, `iproute2`, `ethtool`, `tcpdump`, and `tshark` (Wireshark CLI) -.. code-block:: console - - ./mpPerf -t conf/topo/simple_para - -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 - -``topoType`` just specifies that we want to have multiple interfaces, one for -each path. - -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`` - -2. Simple experiment --------------------- - -.. code-block:: console - - ./mpPerf -t conf/topo/simple_para -x conf/xp/4_nc - -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. +Quick install: +```bash +sudo apt-get update +sudo apt-get install -y mininet iproute2 ethtool tcpdump tshark python3-pip From 69c83f96a3302bd6014adf29b54056a5327b65e3 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Wed, 8 Oct 2025 16:14:22 +0200 Subject: [PATCH 02/10] Update README.rst --- README.rst | 66 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 9fae2e2..3f6fd4b 100644 --- a/README.rst +++ b/README.rst @@ -1,33 +1,53 @@ -# Mininet Runner – Multi-path Experiment Framework (Step-by-Step Guide) +What ? +====== -This repository provides a **Python runner** built on **Mininet** to easily launch -network topologies with **multiple paths** and run **custom experiments** -between hosts (e.g., TCP, UDP, QUIC, or others). +This repository provides a **Python runner** built on `Mininet `_ +to create and control simple network topologies with multiple paths. -> **Default command:** -> ```bash -> sudo python3 runner.py -t -x -> ``` -> *(the old `./mpPerf` command is deprecated)* +It can be used for a wide range of networking experiments (TCP, UDP, QUIC, etc.). +In our case, we focus mainly on **Multipath QUIC** tests between two hosts. ---- +Each experiment is defined by a *topology file* (``.para``) and an *experiment file* (``.xp``) +which describe the network parameters and the commands to execute on each node. + +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: -## 0) Goal of this mini-tutorial +.. code-block:: console -The framework can be used for many network experiments. -In this tutorial, we’ll specifically demonstrate **how to set up a Multipath QUIC -connection** between a client and a server through a router, capture the traffic, -and prepare data for **fingerprinting**. + sudo apt-get update + sudo apt-get install -y mininet iproute2 ethtool tcpdump tshark python3-pip --- -## 1) System requirements +Optional – QUIC experiments (using *quiche*) +-------------------------------------------- + +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 + + git clone --recursive https://github.com/cloudflare/quiche.git + cd quiche + cargo build --release --bin http3-server --bin http3-client + +After compilation, you can use the following binaries in your experiment files: -- Ubuntu / WSL with `sudo` privileges -- Python ≥ 3.8 -- Mininet, `iproute2`, `ethtool`, `tcpdump`, and `tshark` (Wireshark CLI) +- ``target/release/http3-server`` +- ``target/release/http3-client`` -Quick install: -```bash -sudo apt-get update -sudo apt-get install -y mininet iproute2 ethtool tcpdump tshark python3-pip +These will be executed automatically by ``runner.py`` when defined in the +corresponding ``.xp`` file. From 784c4e5dffd48ab4a5595e73660668ffcd54e4a5 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Wed, 8 Oct 2025 16:24:14 +0200 Subject: [PATCH 03/10] Update README.rst --- README.rst | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3f6fd4b..826d92e 100644 --- a/README.rst +++ b/README.rst @@ -49,5 +49,89 @@ After compilation, you can use the following binaries in your experiment files: - ``target/release/http3-server`` - ``target/release/http3-client`` -These will be executed automatically by ``runner.py`` when defined in the -corresponding ``.xp`` file. +Example: Running a Multipath QUIC Experiment +============================================= + +This section describes a complete example showing how to establish +a **Multipath QUIC** connection using this framework. + +1. Launch the topology +---------------------- + +Start a multi-interface topology (in our case ``topo_2``) **without any experiment** +to access the Mininet CLI: + +.. code-block:: console + + sudo python3 runner.py -t config/topo/topo_2 + +This opens the interactive Mininet CLI with the nodes already connected +(client, router, server). + +--- + +2. Start the QUIC server +------------------------ + +On the server node, run the ``quiche`` server binary with the appropriate +certificate and key: + +.. code-block:: console + + Server_0 bash -lc 'nohup /home/achraf/quiche/target/release/quiche-server \ + --cert /home/achraf/quiche/certs/cert.pem \ + --key /home/achraf/quiche/certs/key.pem \ + --listen 0.0.0.0:4433 \ + --root /home/achraf/quiche/quiche/examples \ + > /tmp/qserver.log 2>&1 &' + +This launches the QUIC server listening on UDP port **4433**. + +--- + +3. Start the clients (forced IP binding) +--------------------------------------- + +From the client node, start **two clients simultaneously**. +Each one is hardcoded to use a different source IP address, +to force two distinct QUIC paths. + +.. code-block:: console + + Client_0 bash -lc 'LOCAL_BIND=10.0.0.1 /home/achraf/quiche/target/release/quiche-client \ + https://10.1.0.1:4433/ --no-verify & \ + LOCAL_BIND=10.0.1.1 /home/achraf/quiche/target/release/quiche-client \ + https://10.1.0.1:4433/ --no-verify & wait' + +If everything works, the clients should output: + +.. code-block:: none + + Bonjour, vous êtes bien connecté au serveur QUIC multipath + +This confirms both paths successfully connect to the same QUIC server. + +--- + +4. Capture traffic on the router +-------------------------------- + +On the router node, start a packet capture to observe both flows: + +.. code-block:: console + + Router_0 tcpdump -ni any udp port 4433 -c 40 -vvv > /tmp/capture.log 2>&1 & + +This will capture 40 packets of QUIC traffic on port 4433 +from all interfaces. + +--- + +5. Observe the multipath behavior +--------------------------------- + +After re-running both clients, you should see **two distinct QUIC flows** +in the capture logs or in Wireshark, corresponding to the two interfaces. + +This demonstrates a working **Multipath QUIC** connection using ``quiche`` +and the Mininet runner framework. From 747bc674108c1a830d88d6917fa97ff2af2f0ac9 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Mon, 13 Oct 2025 09:19:09 +0200 Subject: [PATCH 04/10] Update README.rst --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 826d92e..5e2e07a 100644 --- a/README.rst +++ b/README.rst @@ -46,8 +46,8 @@ Build the client and server binaries with: After compilation, you can use the following binaries in your experiment files: -- ``target/release/http3-server`` -- ``target/release/http3-client`` +- ``target/release/quiche-server`` +- ``target/release/quiche-client`` Example: Running a Multipath QUIC Experiment ============================================= @@ -131,7 +131,7 @@ from all interfaces. --------------------------------- After re-running both clients, you should see **two distinct QUIC flows** -in the capture logs or in Wireshark, corresponding to the two interfaces. +in the capture logs, corresponding to the two interfaces. This demonstrates a working **Multipath QUIC** connection using ``quiche`` and the Mininet runner framework. From 8181c0de6013fdd856a17029329daeafa88220ec Mon Sep 17 00:00:00 2001 From: achraf Date: Wed, 12 Nov 2025 10:50:15 +0100 Subject: [PATCH 05/10] WIP: YAML support + runner refactor --- config/topo/topo_2.yaml | 22 ++++ config/topo/topo_quic | 6 + config/xp/pquic | 7 ++ config/xp/quic | 9 ++ config/xp/quic1 | 9 ++ config/xp/quic_mpath.yaml | 17 +++ config/xp/quic_test | 25 ++++ demo_ticket_store.bin | 0 experiments/__init__.py | 32 +++-- experiments/pquic.py | 7 +- experiments/quic.py | 94 ++++++++++----- experiments/quic1.py | 87 ++++++++++++++ experiments/quic_mpath.py | 69 +++++++++++ netstat_client_after | 41 +++++++ netstat_client_before | 41 +++++++ netstat_server_after | 37 ++++++ netstat_server_before | 37 ++++++ runner.py | 26 +++- runner.py.save | 149 +++++++++++++++++++++++ runner1.py | 222 ++++++++++++++++++++++++++++++++++ test.txt | 1 + topos/__init__.py | 3 +- topos/multi_interface.py | 244 ++++++++++++++++++++++++-------------- 23 files changed, 1049 insertions(+), 136 deletions(-) create mode 100644 config/topo/topo_2.yaml create mode 100644 config/topo/topo_quic create mode 100644 config/xp/pquic create mode 100644 config/xp/quic create mode 100644 config/xp/quic1 create mode 100644 config/xp/quic_mpath.yaml create mode 100644 config/xp/quic_test create mode 100644 demo_ticket_store.bin create mode 100644 experiments/quic1.py create mode 100644 experiments/quic_mpath.py create mode 100644 netstat_client_after create mode 100644 netstat_client_before create mode 100644 netstat_server_after create mode 100644 netstat_server_before create mode 100644 runner.py.save create mode 100644 runner1.py create mode 100644 test.txt 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_quic b/config/topo/topo_quic new file mode 100644 index 0000000..bda0c14 --- /dev/null +++ b/config/topo/topo_quic @@ -0,0 +1,6 @@ +leftSubnet:10.0. +rightSubnet:10.1. +path_c2r_0:100,10,100 +path_c2r_1:100,25,100 +path_r2s_0:1000,5,100 +topoType:MultiIf 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..0427d60 100644 --- a/runner.py +++ b/runner.py @@ -8,11 +8,14 @@ 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. @@ -23,6 +26,11 @@ def get_git_revision_short_hash(): 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 @@ -33,7 +41,15 @@ class Runner(object): """ 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 = [] + 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() @@ -98,6 +114,12 @@ 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__': @@ -124,4 +146,4 @@ def stop_topo(self): 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/runner1.py b/runner1.py new file mode 100644 index 0000000..c3bbb8f --- /dev/null +++ b/runner1.py @@ -0,0 +1,222 @@ +#!/usr/bin/python3 +import logging +import os +import subprocess +import tempfile +import traceback +from pathlib import Path + +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 + + +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"} + + +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._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() + 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...""" + 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 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""" + try: + self.topo.stop_network() + finally: + self._cleanup_tmp_files() + + +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 (optional)", + ) + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)-15s [%(levelname)s] %(name)s:%(funcName)s: %(message)s", + force=True, + ) + + try: + Runner(Topo.MININET_BUILDER, args.topo_param_file, args.experiment_param_file) + except Exception as e: + logging.fatal("A fatal error occurred: %s", e) + traceback.print_exc() + finally: + 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) From 5acdd2b1ff6b623a4a0bef5f5891973e345167dc Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Wed, 12 Nov 2025 11:11:07 +0100 Subject: [PATCH 06/10] Update README.rst --- README.rst | 80 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 5e2e07a..a440e7a 100644 --- a/README.rst +++ b/README.rst @@ -49,11 +49,11 @@ After compilation, you can use the following binaries in your experiment files: - ``target/release/quiche-server`` - ``target/release/quiche-client`` -Example: Running a Multipath QUIC Experiment +Example: Running a QUIC Experiment ============================================= This section describes a complete example showing how to establish -a **Multipath QUIC** connection using this framework. +a **SP QUIC** connection using this framework. 1. Launch the topology ---------------------- @@ -127,11 +127,75 @@ from all interfaces. --- -5. Observe the multipath behavior ---------------------------------- +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 has been updated to automatically detect and parse YAML files +using the ``--topo_param_file`` option. + +Example command: + +.. code-block:: console + + sudo python3 runner.py -t config/topo/topo_2.yaml + +When a YAML file is provided: + +- The runner loads network parameters via the ``yaml`` Python module. +- The configuration format mirrors the structure of the legacy ``.para`` files. +- Backward compatibility with existing ``.para`` files is preserved. -After re-running both clients, you should see **two distinct QUIC flows** -in the capture logs, corresponding to the two interfaces. -This demonstrates a working **Multipath QUIC** connection using ``quiche`` -and the Mininet runner framework. From b418847c7051942e6c460f83b0c52930c898fe80 Mon Sep 17 00:00:00 2001 From: achraf Date: Wed, 12 Nov 2025 11:20:58 +0100 Subject: [PATCH 07/10] final sync: replace remote with local version --- config/topo/topo_1 | 5 - config/topo/topo_1.yaml | 19 ++++ config/topo/topo_2 | 6 -- config/topo/topo_3 | 5 - config/topo/topo_3.yaml | 19 ++++ config/topo/topo_4 | 8 -- config/topo/topo_4.yaml | 37 +++++++ config/topo/topo_5 | 8 -- config/topo/topo_5.yaml | 37 +++++++ config/topo/topo_cong | 5 - config/topo/topo_quic | 6 -- runner.py | 197 ++++++++++++++++++++++++----------- runner1.py | 222 ---------------------------------------- 13 files changed, 247 insertions(+), 327 deletions(-) delete mode 100644 config/topo/topo_1 create mode 100644 config/topo/topo_1.yaml delete mode 100644 config/topo/topo_2 delete mode 100644 config/topo/topo_3 create mode 100644 config/topo/topo_3.yaml delete mode 100644 config/topo/topo_4 create mode 100644 config/topo/topo_4.yaml delete mode 100644 config/topo/topo_5 create mode 100644 config/topo/topo_5.yaml delete mode 100644 config/topo/topo_cong delete mode 100644 config/topo/topo_quic delete mode 100644 runner1.py 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_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/topo/topo_quic b/config/topo/topo_quic deleted file mode 100644 index bda0c14..0000000 --- a/config/topo/topo_quic +++ /dev/null @@ -1,6 +0,0 @@ -leftSubnet:10.0. -rightSubnet:10.1. -path_c2r_0:100,10,100 -path_c2r_1:100,25,100 -path_r2s_0:1000,5,100 -topoType:MultiIf diff --git a/runner.py b/runner.py index 0427d60..c3bbb8f 100644 --- a/runner.py +++ b/runner.py @@ -1,149 +1,222 @@ -#!/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 - +#!/usr/bin/python3 import logging import os import subprocess -import traceback import tempfile +import traceback +from pathlib import Path + 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 + 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._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}") + 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 + #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 stop_topo(self): - """ - Stop the topology - """ - self.topo.stop_network() - for p in getattr(self, "_tmp_files", []): - try: - os.unlink(p) - except: - pass + 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() ?" + ) -if __name__ == '__main__': - import argparse + logging.info("Opening Mininet CLI (tape 'exit' pour quitter).") + CLI(net) - parser = argparse.ArgumentParser( - description="Minitopo, a wrapper of Mininet to run multipath experiments") + def stop_topo(self): + """Stop the topology""" + try: + self.topo.stop_network() + finally: + self._cleanup_tmp_files() - 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") +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 (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() diff --git a/runner1.py b/runner1.py deleted file mode 100644 index c3bbb8f..0000000 --- a/runner1.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/python3 -import logging -import os -import subprocess -import tempfile -import traceback -from pathlib import Path - -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 - - -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"} - - -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._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() - 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...""" - 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 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""" - try: - self.topo.stop_network() - finally: - self._cleanup_tmp_files() - - -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 (optional)", - ) - args = parser.parse_args() - - logging.basicConfig( - level=logging.INFO, - format="%(asctime)-15s [%(levelname)s] %(name)s:%(funcName)s: %(message)s", - force=True, - ) - - try: - Runner(Topo.MININET_BUILDER, args.topo_param_file, args.experiment_param_file) - except Exception as e: - logging.fatal("A fatal error occurred: %s", e) - traceback.print_exc() - finally: - logging.info("cleanup mininet") - cleanup() From 90b98ec76d00daf6507c617def6b03ed74114439 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Sun, 16 Nov 2025 19:56:59 +0100 Subject: [PATCH 08/10] Update README.rst --- README.rst | 94 +++++++++++++++++------------------------------------- 1 file changed, 29 insertions(+), 65 deletions(-) diff --git a/README.rst b/README.rst index a440e7a..414ff33 100644 --- a/README.rst +++ b/README.rst @@ -5,10 +5,7 @@ This repository provides a **Python runner** built on `Mininet /tmp/qserver.log 2>&1 &' + sudo python3 runner.py -t config/topo/topo_2 -This launches the QUIC server listening on UDP port **4433**. +This opens the interactive Mininet CLI with the nodes already connected +(client, router, server). --- -3. Start the clients (forced IP binding) ---------------------------------------- +Simple Example +-------------- -From the client node, start **two clients simultaneously**. -Each one is hardcoded to use a different source IP address, -to force two distinct QUIC paths. +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 - Client_0 bash -lc 'LOCAL_BIND=10.0.0.1 /home/achraf/quiche/target/release/quiche-client \ - https://10.1.0.1:4433/ --no-verify & \ - LOCAL_BIND=10.0.1.1 /home/achraf/quiche/target/release/quiche-client \ - https://10.1.0.1:4433/ --no-verify & wait' - -If everything works, the clients should output: - -.. code-block:: none - - Bonjour, vous êtes bien connecté au serveur QUIC multipath - -This confirms both paths successfully connect to the same QUIC server. + sudo python3 runner.py -t config/topo/topo_2.yaml ---- +This starts the network with three nodes: -4. Capture traffic on the router --------------------------------- +- ``Client_0`` (two interfaces) +- ``Router_0`` +- ``Server_0`` -On the router node, start a packet capture to observe both flows: +Inside the Mininet CLI, you can run simple connectivity tests, for example: .. code-block:: console - Router_0 tcpdump -ni any udp port 4433 -c 40 -vvv > /tmp/capture.log 2>&1 & + mininet> Client_0 ping -c 3 Server_0 -This will capture 40 packets of QUIC traffic on port 4433 -from all interfaces. +This confirms that the topology is functional without requiring a full QUIC +experiment. More advanced experiments (QUIC, congestion control, multipath, etc.) +should be handled in dedicated experiment files rather than inside the README. ---- YAML Support (New) ================== @@ -183,19 +160,6 @@ Legacy vs YAML Example Runner Update ------------- -The ``runner.py`` script has been updated to automatically detect and parse YAML files -using the ``--topo_param_file`` option. - -Example command: - -.. code-block:: console - - sudo python3 runner.py -t config/topo/topo_2.yaml - -When a YAML file is provided: - -- The runner loads network parameters via the ``yaml`` Python module. -- The configuration format mirrors the structure of the legacy ``.para`` files. -- Backward compatibility with existing ``.para`` files is preserved. +The ``runner.py`` script automatically detects and parse YAML files From 631941534ee307c7d295040fe40961da137990c4 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Sun, 16 Nov 2025 19:58:10 +0100 Subject: [PATCH 09/10] Update README.rst --- README.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 414ff33..2393fa2 100644 --- a/README.rst +++ b/README.rst @@ -99,9 +99,8 @@ Inside the Mininet CLI, you can run simple connectivity tests, for example: mininet> Client_0 ping -c 3 Server_0 -This confirms that the topology is functional without requiring a full QUIC -experiment. More advanced experiments (QUIC, congestion control, multipath, etc.) -should be handled in dedicated experiment files rather than inside the README. +That confirms if the topology is functional without requiring a full QUIC +experiment. YAML Support (New) From 36891eacbe95b3e171a4b563af040e9098d8c585 Mon Sep 17 00:00:00 2001 From: Hraf-Jr Date: Sun, 16 Nov 2025 23:39:48 +0100 Subject: [PATCH 10/10] Update README.rst --- README.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 2393fa2..61be446 100644 --- a/README.rst +++ b/README.rst @@ -46,12 +46,9 @@ After compilation, you can use the following binaries in your experiment files: - ``target/release/quiche-server`` - ``target/release/quiche-client`` -Example: Running a QUIC Experiment +Simple Example ============================================= -This section describes a complete example showing how to establish -a **Single Path QUIC** connection using this framework. - 1. The topology ---------------------- @@ -77,9 +74,6 @@ This opens the interactive Mininet CLI with the nodes already connected --- -Simple Example --------------- - To verify that the runner and topology are working correctly, you can start the topology and interact with it using the Mininet CLI: