From fb0c2a9898d59c1bea324954713c16f53d98d48e Mon Sep 17 00:00:00 2001
From: Maxime Dourov <44191284+mux99@users.noreply.github.com>
Date: Thu, 16 May 2024 11:49:14 +0200
Subject: [PATCH] MPTCP Connection Check (#1)
This is a simple website to check if MPTCP is being used.
It uses:
- lighttpd to serve HTTP(S) requests
- certbot to generate the SSL certificate
- Flask to handle the requests and check with 'ss' if MPTCP is used
- Docker for the "packaging"
Notes:
- lighttpd is launched with mptcpize, because the version from Ubuntu
doesn't support MPTCP [1]
- lighttpd always adds a slash to all listed files, a known issue [2]
Link: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_feature-flagsDetails#Options [1]
Link: https://redmine.lighttpd.net/boards/2/topics/11479 [2]
---
.github/workflows/docker-image.yml | 32 ++++++
.gitignore | 6 ++
Dockerfile | 25 +++++
README.md | 107 ++++++++++++++++++++
flask_app/app.fcgi | 7 ++
flask_app/app.py | 54 +++++++++++
flask_app/requirements.txt | 2 +
flask_app/static/404.html | 1 +
flask_app/static/MPTCP_logo.svg | 137 ++++++++++++++++++++++++++
flask_app/static/styles.css | 151 +++++++++++++++++++++++++++++
flask_app/templates/index.html | 40 ++++++++
lighttpd.conf | 54 +++++++++++
12 files changed, 616 insertions(+)
create mode 100644 .github/workflows/docker-image.yml
create mode 100644 .gitignore
create mode 100644 Dockerfile
create mode 100644 README.md
create mode 100755 flask_app/app.fcgi
create mode 100755 flask_app/app.py
create mode 100644 flask_app/requirements.txt
create mode 100644 flask_app/static/404.html
create mode 100644 flask_app/static/MPTCP_logo.svg
create mode 100644 flask_app/static/styles.css
create mode 100644 flask_app/templates/index.html
create mode 100644 lighttpd.conf
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
new file mode 100644
index 0000000..78c91bd
--- /dev/null
+++ b/.github/workflows/docker-image.yml
@@ -0,0 +1,32 @@
+name: dockerhub
+
+on:
+ push:
+ branches:
+ - 'main'
+ workflow_dispatch:
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ -
+ name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ -
+ name: Build and push
+ id: docker_build
+ uses: docker/build-push-action@v5
+ with:
+ push: true
+ tags: mptcp/mptcp-check:${{ github.ref_name }}
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e94100
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+
+flask_app/__pycache__/
+
+web/flask_app/__pycache__/
+
+certbot/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..cc55330
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+FROM ubuntu:24.04
+
+# Update package lists and install necessary dependencies
+RUN apt-get update && apt-get dist-upgrade -y && apt-get install -y \
+ python3 \
+ python3-pip \
+ python3-venv \
+ openssl \
+ lighttpd \
+ iproute2 \
+ mptcpize \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+RUN python3 -m venv /app/venv
+ENV PATH="/app/venv/bin:$PATH"
+
+# Setup flask app
+COPY flask_app/ /flask_app/
+RUN /app/venv/bin/pip3 install --no-cache-dir -r /flask_app/requirements.txt
+RUN chmod +x /flask_app/app.*
+
+EXPOSE 80 443
+
+# mptcpize will not be needed with lighttpd > 1.4.76 and the option "server.network-mptcp"
+CMD ["mptcpize", "run", "/usr/sbin/lighttpd", "-D", "-f", "/lighttpd.conf"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..066a162
--- /dev/null
+++ b/README.md
@@ -0,0 +1,107 @@
+# mptcp-check
+A website to check the validity of a mptcp connection
+
+The website is build as a Flask app behind a lighttpd server.
+Everything is package in a docker container.
+
+## installing
+The first step is to create a directory that will be used for shared files.
+``` bash
+mkdir /var/docker/mptcp-check
+cd /var/docker/mptcp-check
+```
+
+The second is to create or download the `lighttpd.conf` file.
+``` bash
+curl "https://raw.githubusercontent.com/multipath-tcp/mptcp-check/main/lighttpd.conf" > lighttpd.conf
+```
+
+You will then need to create the two following scripts. I would recommend the following. Do not forget to set `email` and `url`.
+
+`start.sh`:
+``` bash
+#!/bin/bash
+email= #the email used by certbot
+url= #your domain name
+path=/var/docker/mptcp-check
+
+sed -i "s#YOUR_URL#${url}#g" ${path}/lighttpd.conf
+
+docker run --rm --name certbot-init \
+ -v "${path}/cert:/etc/letsencrypt:rw" \
+ -p 80:80 \
+ certbot/certbot certonly \
+ --non-interactive --agree-tos \
+ --email ${email} \
+ -d ${url} \
+ --standalone
+
+docker run --name mptcp-check \
+ -v "${path}/www:/var/www/:ro" \
+ -v "${path}/cert/:/etc/letsencrypt/:ro" \
+ -v "${path}/lighttpd.conf:/lighttpd.conf:ro" \
+ -p 80:80 -p 443:443 \
+ --restart unless-stopped \
+ --network host \
+ --detach \
+ mptcp/mptcp-check:main
+```
+
+------------------------------------
+`renew.sh`
+``` bash
+#!/bin/bash
+email= #the email used by certbot
+url= #your domain name
+path=/var/docker/mptcp-check
+cert="certbot/conf/live/${url}/fullchain.pem"
+
+cert_before=$(sha256sum "${cert}")
+
+docker run --name certbot-renew \
+ -v "${path}/www:/var/www/:rw" \
+ -v "${path}/cert/:/etc/letsencrypt/:rw" \
+ --detach \
+ certbot/certbot \
+ certonly --non-interactive --agree-tos --webroot \
+ --webroot-path /var/www/ \
+ --email ${email} \
+ -d ${url}
+
+cert_after=$(sha256sum "${cert}")
+
+if [ "${cert_before}" != "${cert_after}" ]; then
+ docker restart mptcp-check
+fi
+```
+When the second one has been created, is needs to be added to the crontab and
+run frequently.
+
+To create the crontab:
+```
+crontab -e
+```
+write `23 2 * * * root /path/to/renew.sh` on a new line
+
+## static files
+The server will serve all files and folders in the `/var/www/files` directory.
+You can use the following command to create dummy random files of various sizes.
+```bash
+mkdir -p /var/docker/mptcp-check/www/files && cd $_
+for i in 1M 10M 100M 1000M; do
+ head -c "${i}" /dev/urandom > "${i}"
+done
+```
+
+## updating
+run the following commands
+```
+docker pull mptcp/mptcp-check:main
+
+docker stop mptcp-check
+docker rm mptcp-check
+```
+then run the `start.sh` script again.
+
+## update needs
+Currently, the apt version of lighttpd is 1.4.63. When the 1.4.76 or newer version is available, `mptcpize run` can be removed from the `Dockerfile` file, and the option `"server.network-mptcp"` can be set in the config.
\ No newline at end of file
diff --git a/flask_app/app.fcgi b/flask_app/app.fcgi
new file mode 100755
index 0000000..35219e0
--- /dev/null
+++ b/flask_app/app.fcgi
@@ -0,0 +1,7 @@
+#!/app/venv/bin/python3
+
+from flup.server.fcgi import WSGIServer
+from app import *
+
+if __name__ == '__main__':
+ WSGIServer(app).run()
diff --git a/flask_app/app.py b/flask_app/app.py
new file mode 100755
index 0000000..eaebd2b
--- /dev/null
+++ b/flask_app/app.py
@@ -0,0 +1,54 @@
+#!/app/venv/bin/python3
+
+from flask import Flask, request, render_template, send_from_directory
+from subprocess import check_output
+import os
+
+app = Flask(__name__)
+
+@app.errorhandler(404)
+def page_not_found(error):
+ # Render a custom 404 template
+ return render_template('404.html'), 404
+
+@app.route("/")
+def mptcp_status_page():
+ """
+ Flask route to display MPTCP connection status.
+
+ Retrieves the visitor's IP and port, checks for MPTCP data in the connections dictionary,
+ and renders the webpage with the connection status.
+
+ Returns:
+ Rendered webpage with connection status and MPTCP version if established.
+ """
+
+ addr = request.remote_addr
+ port = request.environ.get('REMOTE_PORT')
+ user = request.environ.get('HTTP_USER_AGENT')
+ host = request.host_url
+
+ #ipv6 compatibility
+ if ":" in addr:
+ addr = f"{addr}:{port}"
+
+ try:
+ conn = check_output(["ss", "-MnH", "dst", f"{addr}", "dport", f"{port}"]).decode("ascii")
+ if conn.startswith("ESTAB"):
+ state_message = 'are'
+ state_class = 'success'
+ else:
+ state_message = 'are not'
+ state_class = 'fail'
+ except Exception as e:
+ state_message = '[error: ' + str(e) + ']'
+ state_class = 'error'
+
+ if user.startswith("curl"):
+ return "You " + state_message + " using MPTCP.\n"
+
+ return render_template('index.html', state_message=state_message, state_class=state_class, host=host)
+
+if __name__ == "__main__":
+ app.run(host="::", port=80, debug=True)
+
diff --git a/flask_app/requirements.txt b/flask_app/requirements.txt
new file mode 100644
index 0000000..3859b1c
--- /dev/null
+++ b/flask_app/requirements.txt
@@ -0,0 +1,2 @@
+Flask==3.0.2
+flup==1.0.3
\ No newline at end of file
diff --git a/flask_app/static/404.html b/flask_app/static/404.html
new file mode 100644
index 0000000..1496369
--- /dev/null
+++ b/flask_app/static/404.html
@@ -0,0 +1 @@
+this is a 404 page
\ No newline at end of file
diff --git a/flask_app/static/MPTCP_logo.svg b/flask_app/static/MPTCP_logo.svg
new file mode 100644
index 0000000..6a45f96
--- /dev/null
+++ b/flask_app/static/MPTCP_logo.svg
@@ -0,0 +1,137 @@
+
+
+
+
diff --git a/flask_app/static/styles.css b/flask_app/static/styles.css
new file mode 100644
index 0000000..3d9cd66
--- /dev/null
+++ b/flask_app/static/styles.css
@@ -0,0 +1,151 @@
+:root {
+ --background1: #f4f4f4;
+ --background2: #fff;
+}
+a {color: #7253ed;}
+a:hover {
+ text-decoration-color: rgba(114,83,237,0.45);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background1: #27262b;
+ --background2: #31343f;
+ color: #e6e1e8;
+ }
+
+ a {color: #2c84fa;}
+ a:hover {
+ text-decoration-color: rgba(44,132,250,0.45);
+ }
+}
+
+
+body {
+ font-family: system-ui,-apple-system,"Segoe UI",roboto,"Helvetica Neue",arial,sans-serif,"Segoe UI Emoji";
+ margin: 0;
+ padding: 0;
+ background-color: var(--background1);
+
+ line-height: 1.6;
+ font-size: inherit;
+ overflow-wrap: break-word;
+}
+
+ul {
+ width: 98%;
+ background-color: var(--background2);
+ border-radius: 1rem;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ padding: 5px;
+}
+
+ul a {
+ display: block;
+ width: 98%;
+ border-bottom: 1px solid #ddd;
+ height: 2rem;
+ line-height: 2rem;
+ margin-bottom: 2px;
+ padding-left: 0.5rem;
+}
+
+ul a:last-child {
+ border-bottom: none;
+}
+
+#logo {
+ position: fixed;
+ top: 1rem;
+ left: 1rem;
+ background-color: var(--background2);
+ padding: 1rem;
+ border-radius: 1rem;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ width: 5rem;
+ height: 5rem;
+}
+
+#logo img {
+ width: 5rem;
+}
+
+main {
+ max-width: 50%;
+ margin: 0 auto;
+ padding-bottom: 20rem;
+}
+
+.container {
+ max-width: 600px;
+ margin: 50px auto;
+ padding: 20px;
+ background-color: var(--background2);
+ border-radius: 10px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+}
+
+h1, h2, h3 {
+ text-align: center;
+}
+
+p {
+ text-align: justify;
+}
+
+code {
+ color: #544492;
+ padding: 0.2em 0.15em;
+ font-weight: 400;
+ background-color: #f5f6fa;
+ border: 1px solid #eeebee;
+ border-radius: 4px;
+}
+
+#connection-status {
+ text-align: center;
+ margin-top: 30px;
+ font-size: 18px;
+ padding: 20px;
+ border-radius: 5px;
+}
+
+#connection-status p {
+ text-align: center;
+}
+
+.error {
+ background-color: #f0ad4e;
+}
+
+.success {
+ background-color: #5cb85c;
+ color: white;
+}
+
+.fail {
+ background-color: #d9534f;
+ color: white;
+}
+
+p {
+ margin: 0;
+}
+
+@media only screen and (max-width: 600px) {
+ main {
+ max-width: 90%;
+ }
+
+ #logo {
+ top: auto;
+ bottom: 1rem;
+ width: 2.5rem;
+ height: 2.5rem;
+ }
+
+ #logo img {
+ width: 2.5rem;
+ }
+
+}
\ No newline at end of file
diff --git a/flask_app/templates/index.html b/flask_app/templates/index.html
new file mode 100644
index 0000000..221b774
--- /dev/null
+++ b/flask_app/templates/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ MPTCP Connection Check
+
+
+
+
+
+
MPTCP Connection Check
+
+ Multipath TCP or MPTCP is an extension to the standard TCP and is described in RFC 8684. It allows a device to make use of multiple interfaces at once to send and receive TCP packets over a single MPTCP connection. MPTCP can aggregate the bandwidth of multiple interfaces or prefer the one with the lowest latency, it also allows a fail-over if one path is down, and the traffic is seamlessly reinjected on other paths.
+