From 2c6107b2b34b75c731f1182b4479b041ebbe2292 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:48:00 +0200 Subject: [PATCH] build: use quick tunnels for ssh debugging (#48073) * build: use dynamic local tunnels for ssh debugging Co-authored-by: Shelley Vohr * weeee Co-authored-by: Samuel Attard * that'll do Co-authored-by: Samuel Attard * chore: pretty output Co-authored-by: Samuel Attard * build: allow ssh input Co-authored-by: Shelley Vohr --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr Co-authored-by: Samuel Attard --- .github/actions/ssh-debug/action.yml | 4 +- .github/actions/ssh-debug/setup-ssh.sh | 130 +++++++++--------- .github/actions/ssh-debug/ssh-session.sh | 51 +++++-- .../actions/ssh-debug/sshd_config.template | 24 +++- .github/workflows/build.yml | 7 + .../pipeline-electron-build-and-test.yml | 11 +- .../pipeline-segment-electron-build.yml | 14 +- .../pipeline-segment-electron-test.yml | 13 +- 8 files changed, 167 insertions(+), 87 deletions(-) diff --git a/.github/actions/ssh-debug/action.yml b/.github/actions/ssh-debug/action.yml index 96b99671fddd..a8ed77e6c264 100644 --- a/.github/actions/ssh-debug/action.yml +++ b/.github/actions/ssh-debug/action.yml @@ -6,10 +6,10 @@ inputs: required: true default: 'false' timeout: - description: 'SSH session timeout in minutes' + description: 'SSH session timeout in seconds' required: false type: number - default: 60 + default: 3600 runs: using: composite steps: diff --git a/.github/actions/ssh-debug/setup-ssh.sh b/.github/actions/ssh-debug/setup-ssh.sh index 7407dc20955e..13e11f454114 100755 --- a/.github/actions/ssh-debug/setup-ssh.sh +++ b/.github/actions/ssh-debug/setup-ssh.sh @@ -1,44 +1,20 @@ #!/bin/bash -e -get_authorized_keys() { - if [ -z "$AUTHORIZED_USERS" ] || ! echo "$AUTHORIZED_USERS" | grep -q "\b$GITHUB_ACTOR\b"; then - return 1 - fi - - api_response=$(curl -s "https://api.github.com/users/$GITHUB_ACTOR/keys") - - if echo "$api_response" | jq -e 'type == "object" and has("message")' >/dev/null; then - error_msg=$(echo "$api_response" | jq -r '.message') - echo "Error: $error_msg" - return 1 - else - echo "$api_response" | jq -r '.[].key' - fi -} - -authorized_keys=$(get_authorized_keys "$GITHUB_ACTOR") - -if [ -n "$authorized_keys" ]; then - echo "Configured SSH key(s) for user: $GITHUB_ACTOR" -else - echo "Error: User '$GITHUB_ACTOR' is not authorized to access this debug session." - echo "Authorized users: $AUTHORIZED_USERS" - exit 1 -fi - -if [ "$TUNNEL" != "true" ]; then +if [ "${TUNNEL}" != "true" ]; then echo "SSH tunneling is disabled. Set enable-tunnel: true to enable remote access." echo "Local SSH server would be available on localhost:2222 if this were a local environment." exit 0 fi +echo ::group::Configuring Tunnel + echo "SSH tunneling enabled. Setting up remote access..." EXTERNAL_DEPS="curl jq ssh-keygen" for dep in $EXTERNAL_DEPS; do - if ! command -v "$dep" > /dev/null 2>&1; then - echo "Command $dep not installed on the system!" >&2 + if ! command -v "${dep}" > /dev/null 2>&1; then + echo "Command ${dep} not installed on the system!" >&2 exit 1 fi done @@ -48,22 +24,21 @@ cd "$GITHUB_ACTION_PATH" bashrc_path=$(pwd)/bashrc # Source `bashrc` to auto start tmux on SSH login. -if ! grep -q "$bashrc_path" ~/.bash_profile; then +if ! grep -q "${bashrc_path}" ~/.bash_profile; then echo >> ~/.bash_profile # On macOS runner there's no newline at the end of the file - echo "source \"$bashrc_path\"" >> ~/.bash_profile + echo "source \"${bashrc_path}\"" >> ~/.bash_profile fi OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) -if [ "$ARCH" = "x86_64" ]; then +if [ "${ARCH}" = "x86_64" ]; then ARCH="amd64" -elif [ "$ARCH" = "aarch64" ]; then +elif [ "${ARCH}" = "aarch64" ]; then ARCH="arm64" fi -# Install tmux on macOS runners if not present. -if [ "$OS" = "darwin" ] && ! command -v tmux > /dev/null 2>&1; then +if [ "${OS}" = "darwin" ] && ! command -v tmux > /dev/null 2>&1; then echo "Installing tmux..." brew install tmux fi @@ -71,47 +46,80 @@ fi if [ "$OS" = "darwin" ]; then cloudflared_url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${OS}-${ARCH}.tgz" echo "Downloading \`cloudflared\` from <$cloudflared_url>..." - curl --location --silent --output cloudflared.tgz "$cloudflared_url" + curl --location --silent --output cloudflared.tgz "${cloudflared_url}" tar xf cloudflared.tgz rm cloudflared.tgz -else - cloudflared_url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${OS}-${ARCH}" - echo "Downloading \`cloudflared\` from <$cloudflared_url>..." - curl --location --silent --output cloudflared "$cloudflared_url" fi chmod +x cloudflared -echo "Setting up SSH key for authorized user: $GITHUB_ACTOR" -echo "$authorized_keys" > authorized_keys - echo 'Creating SSH server key...' ssh-keygen -q -f ssh_host_rsa_key -N '' echo 'Creating SSH server config...' -sed "s,\$PWD,$PWD,;s,\$USER,$USER," sshd_config.template > sshd_config +sed "s,\$PWD,${PWD},;s,\$USER,${USER}," sshd_config.template > sshd_config echo 'Starting SSH server...' -/usr/sbin/sshd -f sshd_config -D & +sudo /usr/sbin/sshd -f sshd_config -D & sshd_pid=$! -echo 'Starting tmux session...' -(cd "$GITHUB_WORKSPACE" && tmux new-session -d -s debug) +echo "SSH server started successfully (PID: ${sshd_pid})" -#if no cloudflare tunnel token is provided, exit -if [ -z "$CLOUDFLARE_TUNNEL_TOKEN" ]; then - echo "Error: required CLOUDFLARE_TUNNEL_TOKEN not found" +echo 'Starting tmux session...' +(cd "${GITHUB_WORKSPACE}" && tmux new-session -d -s debug) + +mkdir ~/.cloudflared +CLEAN_TUNNEL_CERT=$(printf '%s\n' "${CLOUDFLARE_TUNNEL_CERT}" | tr -d '\r' | sed '/^[[:space:]]*$/d') + +echo "${CLEAN_TUNNEL_CERT}" > ~/.cloudflared/cert.pem + +CLEAN_USER_CA_CERT=$(printf '%s\n' "${CLOUDFLARE_USER_CA_CERT}" | tr -d '\r' | sed '/^[[:space:]]*$/d') + +echo "${CLEAN_USER_CA_CERT}" | sudo tee /etc/ssh/ca.pub > /dev/null +sudo chmod 644 /etc/ssh/ca.pub + +random_suffix=$(openssl rand -hex 5 | cut -c1-10) +tunnel_name="${GITHUB_SHA}-${GITHUB_RUN_ID}-${random_suffix}" +tunnel_url="${tunnel_name}.${CLOUDFLARE_TUNNEL_HOSTNAME}" + +if ./cloudflared tunnel list | grep -q "${tunnel_name}"; then + echo "Deleting existing tunnel: ${tunnel_name}" + ./cloudflared tunnel delete ${tunnel_name} +fi + +echo "Creating new cloudflare tunnel: ${tunnel_name}" +./cloudflared tunnel create ${tunnel_name} + +credentials_file=$(find ~/.cloudflared -name "*.json" | head -n 1) +if [ -z "${credentials_file}" ]; then + echo "Error: Could not find tunnel credentials file" exit 1 fi -echo 'Starting Cloudflare tunnel...' +echo "Found credentials file: ${credentials_file}" -./cloudflared tunnel --no-autoupdate run --token "$CLOUDFLARE_TUNNEL_TOKEN" 2>&1 | tee cloudflared.log | sed -u 's/^/cloudflared: /' & +echo 'Creating tunnel configuration...' +cat > tunnel_config.yml << EOF +tunnel: ${tunnel_name} +credentials-file: ${credentials_file} + +ingress: + - hostname: ${tunnel_url} + service: ssh://localhost:2222 + - service: http_status:404 +EOF + +echo 'Setting up DNS routing for tunnel...' +./cloudflared tunnel route dns ${tunnel_name} ${tunnel_url} + +echo 'Running cloudflare tunnel...' +./cloudflared tunnel --no-autoupdate --config tunnel_config.yml run 2>&1 | tee cloudflared.log | sed -u 's/^/cloudflared: /' & cloudflared_pid=$! -url="$TUNNEL_HOSTNAME" +echo ::endgroup:: + +echo ::notice title=SSH Debug Session Ready::ssh ${tunnel_url} -public_key=$(cut -d' ' -f1,2 < ssh_host_rsa_key.pub) ( echo ' ' @@ -119,22 +127,20 @@ public_key=$(cut -d' ' -f1,2 < ssh_host_rsa_key.pub) echo '🔗 SSH Debug Session Ready!' echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' echo ' ' - echo '📋 Copy and run this command to connect:' + echo '📋 Infra WG can copy and run this command to connect:' echo ' ' - if [ -n "$TUNNEL_HOSTNAME" ]; then - echo "ssh-keygen -R action-ssh-debug && echo 'action-ssh-debug $public_key' >> ~/.ssh/known_hosts && ssh -o ProxyCommand='cloudflared access tcp --hostname $url' runner@action-ssh-debug" - else - echo "ssh-keygen -R action-ssh-debug && echo 'action-ssh-debug $public_key' >> ~/.ssh/known_hosts && ssh -o ProxyCommand='cloudflared access tcp --hostname $url' runner@action-ssh-debug" - fi + echo "ssh ${tunnel_url}" echo ' ' - echo "⏰ Session expires automatically in $TIMEOUT minutes" + echo "⏰ Session expires automatically in ${TIMEOUT} seconds" echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' echo ' ' echo ' ' ) | cat +echo ::group::Starting Background Session echo 'Starting SSH session in background...' -./ssh-session.sh "$sshd_pid" "$cloudflared_pid" $TIMEOUT & +./ssh-session.sh "${sshd_pid}" "${cloudflared_pid}" "${TIMEOUT}" "${tunnel_name}" & echo 'SSH session is running in background. GitHub Action will continue.' echo 'Session will auto-cleanup after timeout or when processes end.' +echo ::endgroup:: diff --git a/.github/actions/ssh-debug/ssh-session.sh b/.github/actions/ssh-debug/ssh-session.sh index 875acf1c66b7..27dfd16b62c4 100755 --- a/.github/actions/ssh-debug/ssh-session.sh +++ b/.github/actions/ssh-debug/ssh-session.sh @@ -2,20 +2,51 @@ SSHD_PID=$1 CLOUDFLARED_PID=$2 -SESSION_TIMEOUT=${3:-3600} +SESSION_TIMEOUT=${3:-10000} +TUNNEL_NAME=$4 + +cleanup() { + # Kill processes. + for pid in "$SLEEP_PID" "$SSHD_PID" "$CLOUDFLARED_PID"; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + fi + done + + # Clean up tunnel. + if [ -n "$TUNNEL_NAME" ]; then + cd "$GITHUB_ACTION_PATH" + ./cloudflared tunnel delete "$TUNNEL_NAME" 2>/dev/null || { + echo "Failed to delete tunnel" + } + fi + + echo "Session ended at $(date)" + exit 0 +} + +# Trap signals to ensure cleanup. +trap cleanup SIGTERM SIGINT SIGQUIT SIGHUP EXIT # Wait for timeout or until processes die. sleep "$SESSION_TIMEOUT" & SLEEP_PID=$! -# Monitor if SSH or cloudflared dies early. -while kill -0 "$SSHD_PID" 2>/dev/null && kill -0 "$CLOUDFLARED_PID" 2>/dev/null && kill -0 "$SLEEP_PID" 2>/dev/null; do - sleep 10 +# Monitor processes +while kill -0 "$SLEEP_PID" 2>/dev/null; do + # Check SSH daemon. + if ! kill -0 "$SSHD_PID" 2>/dev/null; then + echo "SSH daemon died at $(date)" + break + fi + + # Check cloudflared, + if ! kill -0 "$CLOUDFLARED_PID" 2>/dev/null; then + echo "Cloudflared died at $(date)" + break + fi + + sleep 10 done -# Cleanup. -kill "$SLEEP_PID" 2>/dev/null || true -kill "$SSHD_PID" 2>/dev/null || true -kill "$CLOUDFLARED_PID" 2>/dev/null || true - -echo "SSH session ended" +cleanup diff --git a/.github/actions/ssh-debug/sshd_config.template b/.github/actions/ssh-debug/sshd_config.template index 9c7949d8b886..7bc8934b94f3 100644 --- a/.github/actions/ssh-debug/sshd_config.template +++ b/.github/actions/ssh-debug/sshd_config.template @@ -2,8 +2,24 @@ Port 2222 HostKey $PWD/ssh_host_rsa_key PidFile $PWD/sshd.pid -# Only allow single user -AllowUsers $USER +# Connection settings +ClientAliveInterval 30 +ClientAliveCountMax 10 +MaxStartups 10 +LoginGraceTime 120 -# Only allow those keys -AuthorizedKeysFile $PWD/authorized_keys +# Allow TCP forwarding for tunneling +AllowTcpForwarding yes + +# Try to prevent timeouts +TCPKeepAlive yes + +# Security +TrustedUserCAKeys /etc/ssh/ca.pub +PubkeyAuthentication yes +PasswordAuthentication no + +AuthorizedPrincipalsCommand /bin/bash -c "echo '%t %k' | ssh-keygen -L -f - | grep -A1 Principals" +AuthorizedPrincipalsCommandUser nobody + +PubkeyAcceptedKeyTypes ssh-rsa,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c15823e382a..49248c3c106f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,11 @@ on: description: 'Skip lint check' default: false required: false + enable-ssh: + description: 'Enable SSH debugging' + required: false + type: boolean + default: false push: branches: - main @@ -224,6 +229,7 @@ jobs: gn-build-type: testing generate-symbols: false upload-to-storage: '0' + enable-ssh: ${{ inputs.enable-ssh || false }} secrets: inherit macos-arm64: @@ -242,6 +248,7 @@ jobs: gn-build-type: testing generate-symbols: false upload-to-storage: '0' + enable-ssh: ${{ inputs.enable-ssh || false }} secrets: inherit linux-x64: diff --git a/.github/workflows/pipeline-electron-build-and-test.yml b/.github/workflows/pipeline-electron-build-and-test.yml index 6a1a8ecd158b..ee043fb31795 100644 --- a/.github/workflows/pipeline-electron-build-and-test.yml +++ b/.github/workflows/pipeline-electron-build-and-test.yml @@ -54,6 +54,11 @@ on: required: false type: boolean default: false + enable-ssh: + description: 'Enable SSH debugging' + required: false + type: boolean + default: false concurrency: group: electron-build-and-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }} @@ -76,7 +81,8 @@ jobs: gn-build-type: ${{ inputs.gn-build-type }} generate-symbols: ${{ inputs.generate-symbols }} upload-to-storage: ${{ inputs.upload-to-storage }} - is-asan: ${{ inputs.is-asan}} + is-asan: ${{ inputs.is-asan }} + enable-ssh: ${{ inputs.enable-ssh }} secrets: inherit test: uses: ./.github/workflows/pipeline-segment-electron-test.yml @@ -86,5 +92,6 @@ jobs: target-platform: ${{ inputs.target-platform }} test-runs-on: ${{ inputs.test-runs-on }} test-container: ${{ inputs.test-container }} - is-asan: ${{ inputs.is-asan}} + is-asan: ${{ inputs.is-asan }} + enable-ssh: ${{ inputs.enable-ssh }} secrets: inherit diff --git a/.github/workflows/pipeline-segment-electron-build.yml b/.github/workflows/pipeline-segment-electron-build.yml index 7df57eb3ecce..f27f16e5d1de 100644 --- a/.github/workflows/pipeline-segment-electron-build.yml +++ b/.github/workflows/pipeline-segment-electron-build.yml @@ -58,7 +58,11 @@ on: required: false type: boolean default: false - + enable-ssh: + description: 'Enable SSH debugging' + required: false + type: boolean + default: false concurrency: group: electron-build-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.target-variant }}-${{ inputs.is-asan }}-${{ github.ref_protected == true && github.run_id || github.ref }} @@ -96,14 +100,16 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - name: Setup SSH Debugging - if: ${{ inputs.target-platform == 'macos' && env.ACTIONS_STEP_DEBUG == 'true' }} + if: ${{ inputs.target-platform == 'macos' && (inputs.enable-ssh || env.ACTIONS_STEP_DEBUG == 'true') }} uses: ./src/electron/.github/actions/ssh-debug with: tunnel: 'true' env: - CLOUDFLARE_TUNNEL_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_TOKEN }} - TUNNEL_HOSTNAME: ${{ secrets.CLOUDFLARED_SSH_HOSTNAME }} + CLOUDFLARE_TUNNEL_CERT: ${{ secrets.CLOUDFLARE_TUNNEL_CERT }} + CLOUDFLARE_TUNNEL_HOSTNAME: ${{ vars.CLOUDFLARE_TUNNEL_HOSTNAME }} + CLOUDFLARE_USER_CA_CERT: ${{ secrets.CLOUDFLARE_USER_CA_CERT }} AUTHORIZED_USERS: ${{ secrets.SSH_DEBUG_AUTHORIZED_USERS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Free up space (macOS) if: ${{ inputs.target-platform == 'macos' }} uses: ./src/electron/.github/actions/free-space-macos diff --git a/.github/workflows/pipeline-segment-electron-test.yml b/.github/workflows/pipeline-segment-electron-test.yml index 65220fc48e60..7d74d5901df1 100644 --- a/.github/workflows/pipeline-segment-electron-test.yml +++ b/.github/workflows/pipeline-segment-electron-test.yml @@ -25,6 +25,11 @@ on: required: false type: boolean default: false + enable-ssh: + description: 'Enable SSH debugging' + required: false + type: boolean + default: false concurrency: group: electron-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.is-asan }}-${{ github.ref_protected == true && github.run_id || github.ref }} @@ -128,14 +133,16 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - name: Setup SSH Debugging - if: ${{ inputs.target-platform == 'macos' && env.ACTIONS_STEP_DEBUG == 'true' }} + if: ${{ inputs.target-platform == 'macos' && (inputs.enable-ssh || env.ACTIONS_STEP_DEBUG == 'true') }} uses: ./src/electron/.github/actions/ssh-debug with: tunnel: 'true' env: - CLOUDFLARE_TUNNEL_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_TOKEN }} - TUNNEL_HOSTNAME: ${{ secrets.CLOUDFLARED_SSH_HOSTNAME }} + CLOUDFLARE_TUNNEL_CERT: ${{ secrets.CLOUDFLARE_TUNNEL_CERT }} + CLOUDFLARE_TUNNEL_HOSTNAME: ${{ vars.CLOUDFLARE_TUNNEL_HOSTNAME }} + CLOUDFLARE_USER_CA_CERT: ${{ secrets.CLOUDFLARE_USER_CA_CERT }} AUTHORIZED_USERS: ${{ secrets.SSH_DEBUG_AUTHORIZED_USERS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Dependencies uses: ./src/electron/.github/actions/install-dependencies - name: Set Chromium Git Cookie