Skip to content

Mraoul/hackfortress_scoreboard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hack Fortress Scoreboard

Background & Architecture

Background

The Hack Fortress competition first debuted at Shmoocon 2011 after an initial attempt to debut at Defcon 2010 fell through due to funding issues. It was and remains a hybrid Capture the Flag (CTF) and Gaming competition, combining a jeopardy style capture the flag competition with the Team Fortress 2 (TF2) team-based First Person Shooter (FPS). Although the game format has evolved over the years, at its core remains the collaboration of gamers and hackers, working together to win the game. In its current incarnation, as hackers solve challenges and submit flags, they get both points and coins. Similarly, gamers playing TF2 will get points and generate coins as they play the game and get kills, point captures, etc. The hackers can use the coins generated by their team to help the oppposite respective side of the team -- e.g., coins generated by hackers can help their TF2 team, and coins generated by gamers can be used to help hackers. To facilitate this, the Hack Scoreboard has a store system called the Hackonomy that enables usage of the coins. The game runs as a single-elimination bracket-style tournament, with it being run with eight (8) teams historically. Given the number of teams, the brackets are broken up into QuarterFinals, SemiFinals, and Finals, with each these being assigned a 'set' of puzzles.

Components

The Hack Fortress architecture consists of three main pieces interconnected by a RabbitMQ message queue.

  1. The Hack Scoreboard (this repo)
  2. The Viewer
  3. The TF2 Interface

The Hack Scoreboard

This repo contains the code and setup for the aforementioned Hack Scoreboard. Although, all three pieces are needed, this project contains the majority of the heavy lifting logic for most of the game's functionality, including providing a place for the hackers to get puzzles, solve challenges, access the store, etc.

The Hack Scoreboard actually consists of multiple services, all ruby and utilizing NGINX for serving up the application. One of the services is the main Rails + React application which the Hackers interact with. Another is a 'worker' that utilizes Ruby Kicks (formerly Sneakers) to continuously listen to RabbitMQ for TF2 events. The Rails app was originally written using Rails 3.0 with a JQuery javascript frontend and has been continously updated to where it is now, using Rails 7.1.x and React with Vite for the frontend. The final service is a small Sinatra app that is used for streaming events to the frontend using Server-sent Events via redis as an interconnect. This enables one-way communication to client to enable dynamic updates.

Both the Rails and Sinatra apps sit behind an NGINX proxy which combines both to be served from the same host.

The Viewer

The viewer project (currently unreleased) is a Python Flask + React App which listens to RabbitMQ for game events, from both the hack and tf2 sides and displays the game status. This functionality was originally baked into the The Hack Scoreboard project but was broken out in ~2016 for easier development and management.

The TF2 Interface

The TF2 interface (currently unreleased) consists of Python instrumentation around TF2's API and log parser, enabling event-driven actions and custom modification of the games state based on outside factors.

Pre-reqs

The Hack Scoreboard requires three (3) backend dependencies to function. Other than the previously mentioned RabbitMQ server which is shared between all components, the scoreboard needs a redis server and MySQL database. The redis server is used for caching information and further is utilized for its streaming capabilities by the Sinatra app.

Setup & Installation

Setup and installation is simplified by using Ansible scripts to facilitate most of the installation and configuration of things that are needed for the system to work. Some manual configuration will still be required, though.

Compatibility

The ansible playbooks were written for and tested against Ubuntu 22.04. It may or may not work with other versions of Ubuntu. The code should work in any linux distro, but has only been run/tested in Ubuntu.

Ansible

Run the setup_ansible.sh script as root. This will create a directory /opt/ansible and create a virtual-env /opt/ansible/ansible-env where it will install python dependencies needed by ansible.

After it runs, copy the ansible directory to /opt/ansible/, e.g., /opt/ansible/ansible.

Preflight

Before running any playbooks, check the values in group_vars/all.yml. The development variable, which defaults to false determines if the playbook will setup a system user scoreboard and if it can skip setting up RabbitMQ. When set to true, it will use the user specified as the user to own directories created. If you are running this on a personal machine, set development to true (or set the variable via cli). If this is a shared system, keep it at false, so it runs in production mode. You can then switch user to the scoreboard user via:

sudo su - scoreboard

If you want to run/install the scoreboard from a different directory, change the value of scoreboard_base.

Before running any ansible commands you will need to be in the ansible venv:

cd /opt/ansible
. ansible-env/bin/activate

The preflight playbook sets up system-level components and directories. Run as follows:

cd /opt/ansible/ansible
ansible-playbook preflight.yml

Scoreboard

After running the preflight playbook, copy over or pull the scoreboard (this repo) to the base directory (specified in the all.yml file).

Next open up the scoreboard.yml playbook and update the SQL passwords in the vars section. If this is a development environment, you can leave the passwords as is.

Now run the scoreboard playbook

cd /opt/ansible/ansible
ansible-playbook scoreboard.yml

If all goes well, it will have done most of the steps needed to get up and running. At this point only a few manual steps left.

Manual Configuration

With both Ansible playbooks run, all of the pieces are in place but some things need to be manually configured. These items are probably possible to automate via ansible in the future.

Credentials

Rails manages credentials (and secrets) in an encrypted file. This encrypted file can be saved to a git repo but you will have to create your own for you own installation.

To get rails to create a file run:

EDITOR=vim rails credentials:edit

Replace the value of EDITOR with your editor of choice. This will create and open the file config/credentials.yml.enc in vim for editing.

NOTE: If you are not familiar with vim and use it accidentally, type q! to exit.

It wil also create a config/master.key file which will be ignored by git (entry in .gitignore file).

Once the file is open for editing, take a look a the config/credentials.template.yml file for what values need to be added/updated. The default file created by rails will not have stages, which is needed by this app.

Rails Setup

With credentials setup, you now need to setup the Rails database, to do so run:

rails db:reset

This will setup the MySQL database with the app schema and seed the development database. If you want to run this in production mode (which is faster but does not allow dynamic updates to the code), you can add RAILS_ENV=production to any rails command. E.g.,:

RAILS_ENV=production rails db:reset

Only necessary in production as development will auto-compile, we need to compile the assets and the frontend app:

rails assets:precompile
Restart Services

Finally, you can restart the services

sudo systemctl restart scoreboard

Navigate to http port 80 for the host where you installed the app and you should be greeted by the login prompt. Enter admin as the username and whatever default password you set when creating the credentials file.

Enabling SSL

The scoreboard utilizes NGINX for exposing the service and can be configured to work over SSL if desired. The installed configuration includes a commented out portion that redirects any http/port 80 traffic to https/port 443. After setting up certificates, comment out (or delete) the uncommented server section and then uncomment the commented portion.

Management

Roles

The scoreboard uses roles to manage permissions, putting users into one of three:

  • Admin
  • Judge
  • Contestant

The default admin user is created by the scoreboard and is generally the only organizer role needed.

Admin

The Admin role allows access to all permissions and views. There is a marked difference between the number of pages and knobs available to an Admin versus other roles.

Judge

The Judge role allows a user to access interfaces needed to interact with the game. This includes capabilities such as managing puzzles, submitting challenges on behalf of contestants, etc. Historically all Judges used the same login as it simplifies management and the scoreboard is a short lived service.

Contestant

The Contestant role grants locked down access to the scoreboard, limiting access if a team is currently not playing.

Puzzle Management

Using either an Admin or Judge Role grants access to the Puzzle Catalog which enables the user to manage puzzles Categories and Puzzles within those categories.

To simplify matters we have historically managed puzzes in a spreadsheet, exported to CSV and then used the CSV Upload functionality in the Puzzle Catalog to bulk import puzzles for an event. The following CSV fields are expected:

  • Category - The puzzle category name
  • Name - The puzzle name
  • Set - The puzzle 'set', which should be 1, 2, or 3.
  • Hints - Any hints that may be used by Judges to help contestants
  • Points - The nmerical point value of the puzzle
  • Description - A description of the puzzle that will be presented to the contestant
  • Unlock - The number of minutes at which point the puzzle will be 'unlocked'
  • Solution - The puzzle solution, if left blank, only a Judge can 'submit' a solution
  • Location - A location description, or alternatively download path if Download is defined
  • Author - The author of the puzzle (makes it easier to find out who wrote the puzzle)
  • Download - A modifier that changes how Location is interpreted
    • 'text_only' (default if not set) - handle as regular text
    • 'gcloud' - handle as a file stored in GCP (see config/gcloud.yml for required config)
    • 'local' - handle as a file hosted in the app (see config/localstorage.yml for required config)

Beyond CSV, the scoreboard can export the puzzles in JSON format and provides the capability to upload puzzles from JSON.

Puzzle Sets

As alluded to in the above section, puzzles are organized into 'sets'. This groups puzzles together enabling a set to be associated with a round (see below).

Teams and Users

An above section covered users and roles, but didn't mention how teams work. Basically, if a user is marked as a contestant, they are associated with a team. Generally a contestant user shouldn't be created directly, instead a team should be created (via the Team page). Creating a team will automatically create an associated user and five (5) players associated with the team. To manage the publicly displayed team name, go to the Team page. To manage the password for the team login, go the Users page.

Round Management

By default the scoreboard will create eight (8) rounds, consisting of four (4) quarter finals, two (2) semi finals and a final round. It will further automatically assign puzzle sets to these rounds. The Rounds page enables an admin to manage the game, including setting which teams are playing and actually starting a game.

Automation

The entire Hack Fortress game runs on some level of event based automation. The TF2 side will send messages which include its information about the game such as who's playing and the game duration. At a high level it works like the following:

  1. Hack Scoreboard has a round set to Ready status
  2. TF2 sends a "Game Prep" Message
  3. TF2 sends a "Game Start" message
  4. Hack Scoreboard gets the message and automatically sets the round that was set to Ready to Live

There are additional steps in betwee, but they main involve the Hack Fortress Viewer.

Store Management

The Hackonomy store is managed via the Store Control and Item Catalog pages the latter of which is very similar to the above Puzzle Catalog. Since store items don't need to change as often, the majority of items are managed in db/seeds.rb and seeded when running db:reset and the store control page is mainly used for setting discounts. The Item Catalog allows further changes to be made to the base items.

Inventory

Although the item catalog controls information about the store items, the Inventory controls the stock levels per round. Once you choose a Round from the top drop-down list, it will you show the stock for each item separated by team color (red and blue). Since inventory is set when a round is created, you must update the values here if you want to change the stock for an existing round.

Development

When setup in development mode, Rails will automatically detect most changes to ruby code on the fly so your next call will utilize the latest code. Note that some changes, including in intializers may require a restart (see below).

To support frontend development, edit the /etc/scoreboard/env file and define the RAILS_DEV_IP variable. If you have a hostname for your dev setup, also uncomment and define RAILS_DEV_HOSTNAME. Now restart the rails scoreboard:

sudo systemctl restart scoreboard

Further, Vite, specifically the vite rails integration provides an easy way to automatically reload the React frontend app when it detects a change. Just navigate to the scoreboard directory and run:

vite dev

This will start the vite development server and watch for changes. If you have already loaded the app in your browser, refresh and to have it refetch a debug version of the app.

Appendix

Message Specs

The following is the message spec used for intra-communication. All messages are sent MessagePack'd

NOTE: Some of these message specs may be slightly out of date.

#TF2 Time Messages
#routing key: tf2.event.time
{
    “event”: “game_prep”,
    “game_id”: <unique game id number>,
    “duration”: <duration>, # how many minutes is this game?
    “red_team”: <name of red team>,
    “blue_team”: <name of blue team>,
}

{
    “event”: “game_start”,
    “game_id”: <unique game id number>,
    “timestamp”: <timestamp>,
}

{
    “event”: “game_end”,
    “game_id”: <unique game id number>
}

#TF2 Event Message
#routing key: tf2.event.score
{
    "team": <"1" if red | "2" if blue>,
    “game_id”: <game_id>,
    "event":  <"first_blood"|"domination"|"revenge"|"point_captured"|"kill"|"round_win">,
    “value”: <event value, optional, defaults to internal hard-coded values>
}


#Hack Game/Time Messages
#routing key: hack.event.time
{
    “event”: “game_prepped”,
    “categories”: <list of game categories>
}

{
    “event’: “game_started”
}

#Hack Event
#routing key: hack.event.score
{
    "type": "hack",
    "team": <"1" if red | "2" if blue>,
    “category”: <category of puzzle if event == “hack”>,
    "player": <player name>,
    “name”: <puzzle name>,
    "value": <event value>
}

{
    "type": "bonus",
    “bonus”: <"first_blood"|"domination"|"revenge"|"bonus">,
    "team": <"1" if red | "2" if blue>”,
    "value": <event value>
}


#Hack Effect Request
#routing key: purchase.effect.tf2
{
    "from_team": <"1" if red | "2" if blue>, # Requesting team
    "to_team": <"1" if red | "2" if blue>, # Affected team
    "num_players": <value between 1 - 6>, # Number of player affected
    "value": <effect value>,
    "effect_name": <effect name>, # Pre-negotiated name of effect
                                  # List in purchase.rb in hack scoreboard
                                  # list in <??> in tf2 scoreboard
    "delay": <amount of seconds to delay effect from occurring>
}

#Hack Effect Internal
#routing key: purchase.effect.hack
{
    “effect_name”: <name of effect, e.g., store dos>
    “from_team”: <”1” if red | “2” if blue> # Requesting team
    “to_team”: <”1” if red | “2” if blue> # affected team
}

#Hack Status Effect
#routing key: status.effect
{
    “team”: <”1” if red | “2” if blue> # affected team
    “status_name”: <”cap_block” | “cap_delay”>
    “timed”: “<true|false> # whether this is a timed status or not
    “timer”: <number> # number of seconds this is timed for
    “remove”: <true|false> #whether this is being requested to be removed>
}

About

HackFortress Scoreboard

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published