How it works

The game consists of:

  • a CLI written in Python that is used by the player;
  • a control script (checker.py) that verifies the player’s actions in the background and assigns points;
  • a server in Java (Spring Boot) that:
    • exposes the UI (static web pages);
    • receives data from the control script;
    • saves results in a PostgreSQL database;
    • sends the game state to the UI via WebSocket;
  • a web UI made up of HTML, CSS, and vanilla JavaScript.

Each level of the game requires:

  • an OCI container (Docker);
  • a specific checker.py script for the level that is called by the main control script;
  • a WebComponent that is loaded by the UI.

Help! Why mix so many technologies?
Because we are a LUG and we wanted to create a game that is not only fun to play but also interesting to develop and maintain. By combining different technologies, we can create a project that is rich in challenges and learning opportunities, allowing various members of the association to contribute their expertise and grow together with the project.

The following diagram illustrates the communication between the various components:

The game starts when the user runs the start-game.py script (CLI). The script makes an initial call to the server to check if the username chosen by the user is available, as a given username can only be used once. There is no authentication mechanism between the script and the server. It is assumed that both software run on the same local network.

Se lo username è disponibile, lo script notifica il server che sta per iniziare una nuova partita. Il server inoltra questa informazione alla UI, tramite WebSocket.

The script then starts a container, the image of which depends on the level and language selected by the user. The container is started without a network (--network=none) to limit the user’s creativity.

Three volumes are mounted on the container:

  • the file /home/player/.bashrc, mounted in read-only;
  • the file /opt/linux-bomb/log;
  • the folder /bomb.

The container starts with a non-privileged user player, and the flag --userns=keep-id is used to avoid permission issues within the volumes. It is assumed that the container is started by a user with ID 1000.

The folder /bomb contains the files that represent the state of the bomb and is the one on which the user will need to act. It is initialized by the entrypoint of the base image from the folder /opt/linux-bomb/bomb, defined by the Dockerfiles of each level.

The file .bashrc defines the functionality that allows monitoring the commands executed by the user. This functionality has been implemented using a mechanism in bash that allows specifying a command within an environment variable called PROMPT_COMMAND. The command contained in this variable will be executed at the end of each command executed by the user.

In our case, we could have configured PROMPT_COMMAND to send the command and the error code directly to the server. However, since we preferred to disable the network within the container, our PROMPT_COMMAND simply appends the last executed command and its exit code to the file /opt/linux-bomb/log. This file, being mounted on an external volume during the creation of the container, is also readable from outside the container, allowing for monitoring. As an additional “anti-cheating” mechanism, one could invoke chattr +a on this file, allowing only append operations.

The script checker.py periodically checks the content of the log file, and for each executed command, it invokes a specific checker script for each level, which defines the logic for assigning points based on the files and folders detected in /bomb. The main script sends the result to the server, also applying any penalties for incorrect commands. Again, there is no form of authentication with the server. The server also does not perform any checks but simply trusts what it receives from the checker and stores the result in the database. All operations are then notified to the UI via WebSocket.

The checker script also verifies if the container is still running, ending the game (with the explosion of the bomb) if it detects that the container has been terminated. The script also takes care of terminating the container in case of victory.

The container starts with a bash initiated by the timeout command, which allows the container to be automatically terminated upon reaching the maximum time set for the level.

When the container is terminated, another Python script (end-message.py) is triggered, which calls the APIs to check whether the game has ended in victory or failure, in order to display the appropriate messages to the user.

The container and the end-message.py script are started together within a new shell initiated as the last step of the start-game.py script using the os.execvp() function, which executes a new program, replacing the current process.