Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cbe3bd4
added mezzsh scripts
sauravbanna Apr 11, 2026
7d42717
add files
sauravbanna Apr 14, 2026
30e0cd9
use tailscale instead
Thunderbots Apr 14, 2026
bbfceb5
use auth key instead
Thunderbots Apr 18, 2026
b625696
remove uneeded env + add key to pc automatically
Thunderbots Apr 18, 2026
97f2cdc
removed ssh copy id doesn't work
Thunderbots Apr 18, 2026
17ba258
fix user home
sauravbanna Apr 18, 2026
8f2e4dc
fixed user home in keystore + arg number bug
Thunderbots Apr 18, 2026
f26fefc
fixed permissions issues with ssh script
Thunderbots Apr 20, 2026
19b2b38
made keyfile read only for user
Thunderbots Apr 20, 2026
5da44d2
fix color issue
Thunderbots Apr 20, 2026
d4703a0
update how env vars are sent
Thunderbots Apr 20, 2026
8a4ba50
fixed how users are detected + named
Thunderbots Apr 20, 2026
94e47b8
fixed connection warn dialog
Thunderbots Apr 20, 2026
d8bba65
move into dirs, fix file paths
Thunderbots Apr 20, 2026
b6cecf5
remove comment + unneeded echo
Thunderbots Apr 20, 2026
6cb0fbe
remove comments
Thunderbots Apr 20, 2026
e67fa4a
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Apr 20, 2026
b7ae3a3
add comments
Thunderbots Apr 20, 2026
310a7e0
Merge branch 'sauravbanna/mezzsh' of https://github.com/UBC-Thunderbo…
Thunderbots Apr 20, 2026
7ef0e0d
fix server script path + made it copy to usr bin
Thunderbots Apr 25, 2026
5fda9cf
remove relative paths
sauravbanna Apr 29, 2026
b00018f
Merge branch 'sauravbanna/mezzsh' of github.com:UBC-Thunderbots/Softw…
sauravbanna Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions scripts/mezzsh/mezzsh_connect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash

# Connects to the Mezz PC
# Receives safety check results from the server, and warns
# the user if other users are currently connected remotely or using in person
# Allows user to force a connection

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)

PC_NAME="thunderbots"
TAILSCALE_HOSTNAME="$PC_NAME"

bash "$SCRIPT_DIR/utils/check_tailscale.sh"

# Get the Tailscale IP of the Main PC
TARGET_IP=$(tailscale ip -4 $TAILSCALE_HOSTNAME)

if [ -z "$TARGET_IP" ]; then
echo "Could not find Main PC on Tailscale. Are you logged in?"
exit 1
fi

# flag used to force a connection despite warnings
FORCE_FLAG="NORMAL"
SSH_TARGET="thunderbots@$TARGET_IP"

# first, check server status (if other users are using the PC)
RESPONSE=$(SSH_CHECK_MODE=1 ssh -o SendEnv=SSH_CHECK_MODE -t $SSH_TARGET "check_status" 2>&1)

RED='\e[1;31m'
NC='\e[0m'

# someone is using IRL
if [[ "$RESPONSE" == *"STATUS_BUSY_LOCAL"* ]]; then
echo -e "⚠️ ${RED}WARNING${NC}: Someone is physically logged into the PC onsite."
read -p "Do you want to force the connection? (y/n): " choice

# exits if user does not want to force
[[ "$choice" == [yY] ]] && FORCE_FLAG="FORCE" || exit 1

# someone is connected remotely
elif [[ "$RESPONSE" == *"STATUS_BUSY_REMOTE"* ]]; then
echo -e "⚠️ ${RED}WARNING${NC}: Other SSH users are connected:"
echo "$RESPONSE" | grep "List"
read -p "Do you want to force the connection? (y/n): " choice

# exits if user does not want to force
[[ "$choice" == [yY] ]] && FORCE_FLAG="FORCE" || exit 1
fi

# if the user wanted to force the connection
if [ "$FORCE_FLAG" == "FORCE" ]; then
echo "Force connecting..."
FORCE_CONNECT=1 ssh -o SendEnv=FORCE_CONNECT -t $SSH_TARGET
else
ssh -t $SSH_TARGET
fi
66 changes: 66 additions & 0 deletions scripts/mezzsh/mezzsh_keygen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash

# Generates a private / public key pair for connecting via ssh
# Adds the Mezz PC's IP to the ssh config file, and sets the new key as the identity to use
# And sets the key file's permissions correctly
# Each key also has a username attached to it for easier identification

PC_NAME="thunderbots"
ALIAS="mezzsh"

GREEN='\e[1;32m'
NC='\e[0m'

# get the actual current user despite running in sudo
USER_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)

# Check for root privileges
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)"
exit 1
fi

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)

bash "$SCRIPT_DIR/utils/check_tailscale.sh"

# Get the Tailscale IP of the Main PC
TARGET_IP=$(tailscale ip -4 $PC_NAME)

if [ -z "$TARGET_IP" ]; then
echo "Could not find Main PC on Tailscale. Are you logged in?"
exit 1
fi

read -p "Enter a username for yourself. Please make it somewhat recognizable, preferably just your first name: " USERNAME

KEY_NAME="id_rsa_$ALIAS"
KEY_PATH="/.ssh/$KEY_NAME"
USER_KEY_PATH="$USER_HOME/.ssh/$KEY_NAME"

echo -e "\n--- Generating SSH Key Pair ---"

# username is appended as a comment to the key
ssh-keygen -t rsa -b 4096 -f "$USER_KEY_PATH" -C "$USERNAME" -N ""

# ssh is particular about permissions on the key file
# specifically, the private key file should be owned and should only be readable by the current user
sudo chown $SUDO_USER:$SUDO_USER $USER_KEY_PATH
sudo chmod 400 $USER_KEY_PATH
sudo chmod 400 "$USER_KEY_PATH.pub"

echo -e "\n--- Configuring SSH Alias ---"
cat <<EOF >> "$USER_HOME/.ssh/config"

Host $TARGET_IP
User $PC_NAME
IdentityFile ~$KEY_PATH
IdentitiesOnly yes
SendEnv SSH_CHECK_MODE FORCE_CONNECT
EOF

echo -e "\n--- PUBLIC KEY ---"
cat "${KEY_PATH}.pub"
echo "--------------------------------------------"

echo -e "${GREEN}Success!${NC} Please provide the whole public key above (at "$KEY_PATH.pub") to a software lead to finish setup\n"
35 changes: 35 additions & 0 deletions scripts/mezzsh/server/mezzsh_keystore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash

# Adds a single public key to the Mezz PC's authorized_keys file
# along with other necessary config options

# the script is run as sudo, but we want to modify the current user's authorized_keys
# defaults to the "thunderbots" user
USER_HOME=$(tmp=$(getent passwd "$SUDO_USER" | cut -d: -f6); echo "${tmp:-/home/thunderbots}")

AUTH_KEYS="$USER_HOME/.ssh/authorized_keys"
SERVER_SCRIPT="/home/thunderbots/Software/scripts/mezzsh/server/mezzsh_server.sh"

if [ "$#" -ne 1 ]; then
echo "Usage: sudo ./mezzsh_keystore.sh \"PUBLIC_KEY_STRING\""
exit 1
fi

PUB_KEY="$1"

echo "--- Registering New Remote User ---"

# Add to authorized_keys with a command restriction
ENTRY="$PUB_KEY
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing quote?

command=\"$SERVER_SCRIPT\"
environment=\"SSH_CHECK_MODE=1 FORCE_CONNECT=0\""

if grep -q "$PUB_KEY" "$AUTH_KEYS"; then
echo "Error: This public key is already registered."
else
echo "$ENTRY" >> "$AUTH_KEYS"
chmod 600 "$AUTH_KEYS"
echo "Key added to authorized_keys."
fi

echo "Registration complete."
51 changes: 51 additions & 0 deletions scripts/mezzsh/server/mezzsh_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash

# Script to intercept all incoming ssh connections to the Mezz PC
# Checks for both IRL users and any connected remote users who are currently using the PC
# and warns the new connection accordingly
# If the new connection wants to force, opens the connection
# and also warns any IRL users if present

# The `who` command returns all logged in users (IRL and remote)
# Finds IRL users by looking for the physical monitor (tty2)
LOCAL_USER=$(who | grep -E '(tty2)')

# Finds remote users currently connected
# for each one, finds their username, to make identifying them easier
# excludes the current user, otherwise server will always seem busy
REMOTE_USERS_LIST=$(bash /home/thunderbots/Software/scripts/mezzsh/utils/get_connected_users.sh)

# if the client requested a check, return the user info from above
if [ "$SSH_CHECK_MODE" == "1" ]; then
# local user check takes priority
if [ ! -z "$LOCAL_USER" ]; then
echo "STATUS_BUSY_LOCAL"
exit 0
elif [ ! -z "$REMOTE_USERS_LIST" ]; then
echo "STATUS_BUSY_REMOTE"
echo "List of Users: $REMOTE_USERS_LIST"
exit 0
fi
exit 0
fi

# If the user actually wanted to connect, make sure:
# 1. no other users (IRL or remote) are using the PC
# OR
# 2. the force flag is provided
if ([ ! -z "$LOCAL_USER" ] || [ ! -z "$REMOTE_USERS" ]) && [ "$FORCE_CONNECT" != "1" ]; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ([ ! -z "$LOCAL_USER" ] || [ ! -z "$REMOTE_USERS" ]) && [ "$FORCE_CONNECT" != "1" ]; then
if ([ ! -z "$LOCAL_USER" ] || [ ! -z "$REMOTE_USERS_LIST" ]) && [ "$FORCE_CONNECT" != "1" ]; then

echo "The Mezz Computer is in use! Please use the connect script to see who is currently using it."

# we technically hae already have an active connection at this point
# just no shell is provided
# close the connection after 1 min
sleep 60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to wait for 60s here.

echo "Connection timed out."
exit 1
fi

# Trigger the visual warning dialog if someone is using the PC IRL
bash /home/thunderbots/Software/scripts/mezzsh/utils/connection_warn.sh &
Comment on lines +47 to +48
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when connection is successful, this script will still run even if there is no local user?


# if we get here, start a normal shell
exec $SHELL
72 changes: 72 additions & 0 deletions scripts/mezzsh/server/mezzsh_setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash

# Sets up the Mezz PC to accept new SSH connections
# Uses Tailscale as a VPN to get around connections being blocked at the router level
# Sets up Tailscale, ssh, and links the server script to intercept new connections

SCRIPT_PATH="/home/thunderbots/Software/scripts/mezzsh/server/mezzsh_server.sh"
SERVER_SCRIPT="/usr/local/bin/mezzsh_server.sh"
TIMEOUT_SECONDS=3600 # 1 hour
SSHD_CONFIG="/etc/ssh/sshd_config"
TARGET_USER="thunderbots"

# Check for root privileges
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)"
exit 1
fi

echo "--- Initializing On-Site PC SSH Security Setup ---"

echo "[1/5] Installing Dependencies"
sudo apt update
sudo apt install openssh-server
sudo apt install zenity

# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --hostname=$TARGET_USER --operator=$USER

echo "[2/5] Configuring SSH service to start on boot..."
systemctl enable --now ssh
systemctl start ssh

echo "[3/5] Modifying sshd_config for custom Environment variables, and links script to perform safety checks on new connections......"

# Backup original config
cp $SSHD_CONFIG "${SSHD_CONFIG}.bak"

# This step uses a match block to modify these ssh settings for only 1 user
# Clean up any previous global ForceCommand we might have added
sed -i '/Match User $TARGET_USER/,/AcceptEnv SSH_CHECK_MODE FORCE_CONNECT/d' $SSHD_CONFIG
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# copies the script from repo to script location
cp $SCRIPT_PATH $SERVER_SCRIPT

# Append the Match block to the end of the file
cat <<EOF >> $SSHD_CONFIG
Match User $TARGET_USER
ForceCommand $SERVER_SCRIPT
AcceptEnv SSH_CHECK_MODE FORCE_CONNECT
EOF

# Script has to be executable for it to be triggered on new connections
sudo chmod +x $SERVER_SCRIPT

echo "[4/5] Setting 1-hour shell timeout..."

USER_HOME=$(eval echo "~$TARGET_USER")
TIMEOUT_BLOCK='if [ -n "$SSH_TTY" ]; then export TMOUT='$TIMEOUT_SECONDS' && readonly TMOUT; fi'

if [ -d "$USER_HOME" ]; then
# Remove old TMOUT lines if they exist and append new one
sed -i '/TMOUT/d' "$USER_HOME/.bashrc"
echo $TIMEOUT_BLOCK >> "$USER_HOME/.bashrc"
chown $TARGET_USER:$TARGET_USER "$USER_HOME/.bashrc"
echo "Success: SSH timeout set for $TARGET_USER."
else
echo "ERROR: Home directory for $TARGET_USER not found. Timeout not set."
fi

echo "[5/5] Restarting SSH service to apply changes..."
systemctl restart ssh
25 changes: 25 additions & 0 deletions scripts/mezzsh/utils/check_tailscale.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

# Checks if Tailscale is installed, the daemon is up and running, and we are logged in (connected) to the VPN

# Check if tailscale is installed
if ! command -v tailscale &> /dev/null; then
echo "Tailscale not found. Installing..."
curl -fsSL https://tailscale.com/install.sh | sh
fi

# Check if the Tailscale daemon (tailscaled) is even running
if ! systemctl is-active --quiet tailscaled; then
echo "tailscaled is not running. Starting service..."
sudo systemctl start tailscaled
fi

# Check if the node is authenticated and connected to the tailnet
# 'tailscale status' returns 0 if connected, non-zero otherwise
if ! tailscale status >/dev/null 2>&1; then
echo "Tailscale is down or unauthenticated. Running 'up'..."

read -p "Enter the auth key. Please contact a software lead if you don't have one: " AUTH_KEY

sudo tailscale up --auth-key=$AUTH_KEY
fi
28 changes: 28 additions & 0 deletions scripts/mezzsh/utils/connection_warn.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash

# Script to trigger a dialog on the Mezz PC monitor that someone
# has just connected remotely

# find the username of the person logged in physically
# we look for someone attached to the main display (:0)
LOCAL_USER=$(who | grep -m 1 "(tty2)" | awk '{print $1}')

# if no one is logged in locally, just exit
if [ -z "$LOCAL_USER" ]; then
exit 0
fi

# get the User ID of the local user
LOCAL_UID=$(id -u "$LOCAL_USER")

# Trigger the dialog
# these environment variables must be set to trigger the dialog
# from a background script (i.e our SSH server script)
# specifically on our wayland setup
sudo -u "$LOCAL_USER" DISPLAY=tty2 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$LOCAL_UID/bus \
XDG_RUNTIME_DIR="/run/user/$LOCAL_UID" \
WAYLAND_DISPLAY="wayland-0" \
GDK_BACKEND="wayland" \
zenity --warning --title="Remote Connection" \
--text="A new has just connected remotely (via SSH)." --timeout=10 &
39 changes: 39 additions & 0 deletions scripts/mezzsh/utils/get_connected_users.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

# Gets the names of all currently connected SSH users
# Each user has their name attached to their SSH public key in authorized_keys
# So we find the key each current connection used to connect, and then the corresponding name

# Get keys and comments from authorized_keys
KEYS_FILE="/home/thunderbots/.ssh/authorized_keys"
declare -A key_to_names

while read -r line; do
# Skip the line if it doesn't start with "ssh"
# to skip over the extra flags attached to each key on separate lines
[[ ! $line =~ ^ssh ]] && continue

# the line consists of <ssh key> <username>

# Get fingerprint for the ssh key
fingerprint=$(echo "$line" | ssh-keygen -l -f - | awk '{print $2}')

# get the username part of the line
username=$(echo "$line" | awk '{print $NF}')

# map fingerprint to username
key_to_names["$fingerprint"]="$username"
done < "$KEYS_FILE"

# Find currently active SSH PIDs and match them to log entries
pgrep -u "root" sshd | tail -n +2 | while read -r pid; do
# Look for the 'Accepted publickey' log entry for this specific PID
# there should be a log entry with the text "sshd[<pid>]
# log entry contains the fingerprint within the text "SHA256:<fingerprint>
fingerprint_match=$(grep "sshd\[$pid\]" /var/log/auth.log | grep "Accepted publickey" | grep -oE "SHA256:[^ ]+")

if [ -n "$fingerprint_match" ]; then
username=${key_to_names[$fingerprint_match]}
echo "${username}"
fi
done
Loading