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. +

+

For more informations, visit mptcp.dev

+
+

MPTCP Connection Status

+
+

You {{ state_message }} using MPTCP!

+
+
+ +

Static files

+

+ You can download files of various sizes form the files directory. +

+ +

cURL

+

+ With cURL you can easily check the status of your connection. Simply use the following command:
+ mptcpize run curl {{ host }} +

+

+ The response is limited to the status message., e.g.:
+ You {{ state_message }} using MPTCP! +

+
+ + diff --git a/lighttpd.conf b/lighttpd.conf new file mode 100644 index 0000000..38eb071 --- /dev/null +++ b/lighttpd.conf @@ -0,0 +1,54 @@ +server.modules += ( "mod_access", + "mod_auth", + "mod_fastcgi", + "mod_rewrite", + "mod_openssl", + "mod_dirlisting") + +server.feature-flags = ( "server.network-mptcp" => "enable" ) +server.use-ipv6 = "enable" + +server.document-root = "/var/www/" + +$SERVER["socket"] == "[::]:80" {} +$SERVER["socket"] == "0.0.0.0:80" {} + +$SERVER["socket"] == "[::]:443" { + ssl.engine = "enable" + ssl.pemfile = "/etc/letsencrypt/live/check.mptcp.dev/fullchain.pem" + ssl.privkey = "/etc/letsencrypt/live/check.mptcp.dev/privkey.pem" +} + +$SERVER["socket"] == "0.0.0.0:443" { + ssl.engine = "enable" + ssl.pemfile = "/etc/letsencrypt/live/check.mptcp.dev/fullchain.pem" + ssl.privkey = "/etc/letsencrypt/live/check.mptcp.dev/privkey.pem" +} + +#static routes must also be referenced in the url.rewrite bellow +#otherwise they will be ignored +$HTTP["url"] =~ "^/\.well-known/acme-challenge/" { + server.document-root = "/var/www" +} + +$HTTP["url"] =~ "^/files/" { + dir-listing.activate = "enable" + server.document-root = "/var/www/" +} + +fastcgi.server = ( + "/app.fcgi" => + (( + "socket" => "/tmp/app-fcgi.sock", + "bin-path" => "/flask_app/app.fcgi", + "check-local" => "disable", + "max-procs" => 2, + )) +) + +url.rewrite-once = ( + "^(/\.well-known/acme-challenge.*)$" => "$1", + "^(/files.*)$" => "$1", + "^/favicon\.ico$" => "/static/favicon.ico", + "^(.*)$" => "/app.fcgi$1", +)