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: