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