Введение

Как мы с вами знаем, SSH — надежный и безопасный протокол для удаленного управления системами, который у многих является неотъемлемой частью работы. Однако, что делать, когда стандартные порты SSH заблокированы или закрыты, например, в строго защищенных корпоративных сетях или в облачных средах с жесткой политикой безопасности? Или что делать, если под рукой есть только браузер и нет возможности использовать обычный терминал? Или вам вживую нужно смотреть/управлять сессией?

Одним из таких решений является WebTTY — инструмент, который обеспечивает доступ к терминалу удаленного сервера через веб-браузер, используя технологию WebRTC и веб-технологии для создания безопасного и зашифрованного соединения. Это решение позволяет обойти ограничения, такие как заблокированные стандартные SSH-порты, и предоставляет простой и удобный способ взаимодействия с командной строкой сервера без необходимости открытия дополнительных портов, что особенно полезно в средах с жесткими сетевыми ограничениями или за фаерволами.

В этой статье мы рассмотрим, как WebTTY может быть использован для доступа к SSH-портам через браузер, даже если они закрыты, как его можно настроить и когда его можно использовать. Основана цель данного материала – познакомить вас с таким вариантом подключения и показать, как использовать данный инструмент. Надеюсь, что представленные примеры и объяснения помогут вам оценить его возможности и найти полезные применения в вашей практике.

Основные функции и возможности

  1. Подключение через WebRTC: WebTTY использует WebRTC (Web Real-Time Communication) для установления соединения между клиентом и сервером. Эта технология позволяет передавать данные в реальном времени, включая аудио, видео и текст, непосредственно между браузерами и сервером без необходимости промежуточных серверов или прокси.

  2. Работа через веб-браузер: один из ключевых аспектов WebTTY — возможность работы прямо в веб-браузере. Вам не требуется устанавливать дополнительные клиенты или расширения. Всё, что вам нужно, — это доступ к веб-интерфейсу WebTTY, который доступен по адресу WebTTY.

  3. Шифрование и безопасность: WebTTY обеспечивает безопасность данных через использование шифрования. Сессии защищены с помощью технологии WebRTC, которая включает в себя шифрование передачи данных. В случае одностороннего соединения WebTTY использует сервер 10kb.site для безопасного обмена SDP-описаниями, что дополнительно защищает соединение.

  4. Односторонние и двухсторонние соединения: WebTTY поддерживает два основных режима подключения:

    • Двухстороннее соединение: стандартный режим, в котором устанавливается двусторонняя связь между клиентом и сервером, что позволяет передавать данные в обоих направлениях.

    • Одностороннее соединение: режим, в котором сервер отправляет данные клиенту без ожидания ответа, что может быть полезно для настройки автоматизированных процессов или для временного предоставления доступа.

Как работает WebTTY

Как работает WebTTY
Как работает WebTTY
  1. Инициализация и создание предложения (Offer): на стороне сервера запускается WebTTY, который генерирует предложение (SDP-описание) для установления WebRTC-соединения. Это предложение включает информацию о том, как клиент может подключиться к серверу.

  2. Обмен SDP-описаниями: в случае двухстороннего соединения предложение передается клиенту, который затем отправляет свой ответ (SDP-описание) обратно серверу. При одностороннем соединении предложение отправляется на специальный сервер (10kb.site), где клиент может загрузить ответ на это предложение.

  3. Установка соединения: после успешного обмена SDP-описаниями и установки соединения клиент получает доступ к командной строке сервера через веб-интерфейс. Весь ввод и вывод данных происходит в реальном времени, позволяя пользователю работать с сервером так, как если бы он был подключен напрямую через SSH.

Примеры использования WebTTY

WebTTY позволяет удаленно администрировать серверы за корпоративными фаерволами, когда стандартные SSH-порты закрыты. Это удобно для технической поддержки и отладки систем, так как позволяет быстро подключаться к серверам без необходимости конфигурации VPN или проброса портов. Кроме того, WebTTY обеспечивает передачу данных и команд в реальном времени, что полезно для автоматизации процессов и мониторинга состояния серверов. В образовательных целях WebTTY может быть использован для демонстрации работы в терминале, а также для обеспечения удаленной технической поддержки, упрощая решение проблем.

Как воспользоваться?

Для Debian 12 (на котором проводились тесты) нужно установить golang и собрать бинарник из исходников:

sudo apt install -y golang && \
git clone https://github.com/maxmcd/webtty && \
cd webtty && \
go build && \
cd

После чего можно запустить webtty:

~/webtty/webtty

Работать будем следующим образом для двухстороннего обмена ключами:

Демонстрация работы двухстороннего обмена ключами в webtty за 15 секунд. Слева - терминал машины (к которой подключаемся), справа - браузер.
Демонстрация работы двухстороннего обмена ключами в webtty за 15 секунд. Слева - терминал машины (к которой подключаемся), справа - браузер.

В случае с односторонним обменом, нужно передать лишь один ключ (работает через посредника 10kb.site.

А что если?

А что если развернуть фронт на своем домене? Можно, используем NGINX. Для начала создадим папку для статичных файлов сайта (не забудьте поменять название site-name на ваше здесь и далее):

sudo mkdir -p /var/www/site-name/

Помещаем в нее статичные файлы из репозитория :

sudo wget https://github.com/maxmcd/webtty/archive/refs/heads/gh-pages.zip
sudo unzip gh-pages.zip
sudo rm gh-pages.zip
sudo chmod -R 755 ~/webtty-gh-pages/
sudo mv ~/webtty-gh-pages/* /var/www/site-name/
sudo rm -rf ~/webtty-gh-pages/

Создадим конфигурацию NGINX:

sudo nano /etc/nginx/sites-available/site-name.ru

Со следующим содержимым (не забудьте проверить, что сертификаты доступны по указанным путям)

server {
  listen 80;
  server_name site-name.ru;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  http2 on;
  server_name site-name.ru;

  ssl_certificate /etc/letsencrypt/live/site-name.ru/fullchain.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/site-name.ru/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/site-name.ru/privkey.pem;

  root /var/www/site-name.ru;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

Создадим символическую ссылку:

sudo ln -s /etc/nginx/sites-available/site-name.ru /etc/nginx/sites-enabled/

Проверим конфигурацю и перезапустим NGINX:

sudo nginx -t && \
sudo service nginx reload && \
sudo systemctl restart nginx

Готово, теперь на site-name.ru в браузере будет отображаться окно ожидания подключения (не забудьте создать нужные доменные записи).

Для того, чтобы WebTTY в своем интерфейсе отображал ваше название site-name.ru нужно пересобрать бинарник, отредактировав один из исходных файлов host.go. Изменения нужно внести на 239 строчку:

Заменив это:
"\n[bold]Or in a browser: [reset]https://maxmcd.github.io/webtty/\n\n")

На это:
"\n[bold]Or in a browser: [reset]https://site-name.ru\n\n")

Данные правки ровным счетом влияют только на текст, который выводится во время запуска WebTTY. На работе это не сказывается, так как вся работа идет через STUN сервер от Google: stun.l.google.com:19302. Т.е. вы можете открыть у себя site-name.ru, ваш коллега maxmcd.github.io/webtty/, но все будет работать :)

Как было сказано ранее, WebTTY поддерживает и одностороннее подключение. Но сейчас у сайта посредника 10kb.site истекли сертификаты, что мешает подключению. Да и если и пользоваться таким вариантом, то хотелось бы развернуть свой 10kb.site. Как это сделать:

Для начала нужно изменить .js файл статичного сайта, чтобы он знал новый адрес (меняем старый на ваш):

sudo sed -i 's|https://up.10kb.site/|https://10kb.my-site.ru/|g' /var/www/my-site.ru/app.ac034b8f.js

Снова правим исходники WebTTY в файле ten_kb_site.go и не забываем заново собрать:

Заменив это:
var tenKbUpURL = "https://up.10kb.site/"
var tenKbURL = "https://www.10kb.site/"

На это:
var tenKbUpURL = "https://10kb.my-site.ru/" 
var tenKbURL = "https://10kb.my-site.ru/"

Далее, настроим "серверную" часть, которая есть на оригинальном 10kb.site, но с небольшими изменениями, так как обойдемся без AWS. Для этого нам понадобится Python:

sudo apt update && sudo apt install -y python3 python3-pip && \
sudo pip3 install flask gunicorn --break-system-packages

И директории, где все будет работать:

mkdir -p /var/www/10kb.my-site.ru && \
mkdir -p /var/www/10kb.my-site.ru/uploads && \
sudo chown -R www-data:www-data /var/www/10kb.my-site.ru && \
sudo chown -R www-data:www-data /var/www/10kb.my-site.ru/uploads && \
sudo chmod -R 755 /var/www/10kb.my-site.ru && \
sudo chmod -R 755 /var/www/10kb.my-site.ru/uploads

И создадим index.html, uploader.py, nginx-конфиг и systemd-сервис.

Скрытый текст

index.html

cat > /var/www/10kb.my-site.ru/index.html << EOF
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>10kb.site</title>
    <style>
      body {
        margin: 50px;
      }
      input {
        font-family: monospace;
        font-size: 12px;
      }
      a {
        text-decoration: none;
        color: black;
        border-bottom: 1px solid black;
        display: inline-block;
        line-height: 0.85;
      }
    </style>
  </head>
  <body>
    <pre style="font-size: 20px">
<a href="/">10kb.site</a> 
    </pre>
    <pre>
10kb.site is a write-only public text server.
You can upload any text you want at any file
path, as long as it's less than 10kb.


Files can never be changed or updated (except this one).

Try it out:

Uploading file to https://10kb.my-site.ru/<input id="path" type="text" value="random-filename" /> 
with body: <input id="body" type="text" />
<input id="submit" type="submit" onclick="window.onsubmit()">
<a id="out"></a>

    </pre>

    <script>
      let gbid = (d) => document.getElementById(d);
      let path = gbid("path");
      let body = gbid("body");
      let out = gbid("out");
      let submit = gbid("submit");
      path.value = Math.random().toString(35).substring(2);
      body.value = "Type something here";
      window.onsubmit = () => {
        submit.value = "loading...";
        fetch("/" + path.value, {
          method: "POST",
          body: body.value,
        })
          .then((resp) => resp.json())
          .then((resp) => {
            out.href = resp.url;
            out.innerText = resp.url; // Используем только URL из ответа
            submit.remove();
          });
      };
    </script>
  </body>
</html>
EOF

uploader.py

cat > /var/www/10kb.my-site.ru/uploader.py << EOF
from flask import Flask, request, jsonify
import os
import datetime
import mimetypes

app = Flask(__name__)
UPLOAD_FOLDER = '/var/www/10kb.my-site.ru/uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

@app.route('/', methods=['GET'])
def index():
    return '10kb.site is a write-only public text server.\nFiles are deleted after 1 day.'

@app.route('/<path:filename>', methods=['POST'])
def upload_file(filename):
    if filename.startswith('.'):
        return jsonify({"error": "No paths that start with a ."}), 422
    if not request.data:
        return jsonify({"error": "No body"}), 422
    if len(request.data) > 1e4:
        return jsonify({"error": "File size exceeds limit"}), 422

    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if os.path.exists(file_path):
        return jsonify({"error": "File already exists"}), 409

    with open(file_path, 'wb') as f:
        f.write(request.data)

    # Set file to be deleted after 1 day
    future_time = (datetime.datetime.now() + datetime.timedelta(days=1)).timestamp()
    os.utime(file_path, (int(future_time), int(future_time)))

    return jsonify({"url": f"https://10kb.my-site.ru/{filename}"}), 201

@app.route('/<path:filename>', methods=['GET'])
def get_file(filename):
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(file_path):
        return '', 404

    mime_type, _ = mimetypes.guess_type(file_path)
    with open(file_path, 'rb') as f:
        content = f.read()

    return content, 200, {'Content-Type': mime_type or 'text/plain'}

if __name__ == '__main__':
    app.run(port=5000, debug=False, use_reloader=False)
EOF

Конфиг для NGINX (не забудьте создать нужные доменные записи):

nano /etc/nginx/sites-available/10kb.my-site.ru
server {
  listen 80;
  server_name 10kb.my-site.ru;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  http2 on;

  ssl_certificate /etc/letsencrypt/live/my-site.ru/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/my-site.ru/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/my-site.ru/fullchain.pem;

  root /var/www/10kb.my-site.ru;
  index index.html;

  location / {
    try_files $uri $uri/ @flask;
  }

  location @flask {
    proxy_pass http://127.0.0.1:5000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
sudo ln -s /etc/nginx/sites-available/10kb.my-site.ru /etc/nginx/sites-enabled/ && \
sudo nginx -t && \
sudo service nginx reload && \
sudo systemctl restart nginx

Systemd-сервис (будем запускать uploader.py через gunicorn):

cat > /etc/systemd/system/10kb.my-site.ru-uploader.service << EOF
[Unit]
Description=10kb.my-site.ru-uploader Gunicorn Service
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/10kb.my-site.ru
ExecStart=/usr/local/bin/gunicorn -w 4 -b 127.0.0.1:5000 uploader:app
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload &&
sudo systemctl start 10kb.my-site.ru-uploader &&
sudo systemctl enable 10kb.my-site.ru-uploader &&
sudo systemctl status 10kb.my-site.ru-uploader

В результате получим One-way Connection, через собственный 10kb.site (10kb.my-site.ru уже):

Демонстрация работы одностороннего обмена в webtty -o
Демонстрация работы одностороннего обмена в webtty -o

Расшаренную терминальную сессию можно свернуть, продолжая пользоваться вашим текущим терминалом и шаря доступ через WebTTY одновременно. Для этого понадобится утилита screen иди tmux.

Скрытый текст

Работа в фоне:
screen webtty (пройти процедуру создания пары)
Отделиться от screen можно, нажав Ctrl-a, затем d.
Вернуться к сессии можно, используя: screen -r

Либо
tmux new-session -s mysession
webtty
Когда процесс будет запущен, вы можете отделиться от сессии, нажав Ctrl-b, а затем d.
Вернуться к сессии можно, используя: tmux attach-session -t mysession

Как-то так. Надеюсь, что этот инструмент окажется полезным и найдет свое применение в вашей работе. По крайней мере вы узнали про такую возможность, если до этого о ней не слышали :)

P.S.

Да, вам в любом случае нужно как-то передать ключи для подключения с двусторонним обменом, что не всегда удобно/возможно. (В таком случае можно сформировать и получить к себе ключ на одностороннее подключение в случае какой-то ошибки в системе для оперативно подключения, например).

Да, дефолтный STUN сервер и порт подключения тоже может быть заблокирован.

Комментарии (8)


  1. xata6
    02.09.2024 16:33
    +6

    Получается Вы гений. Создали бекдор на нормально закрытый фаервол


  1. mihmig
    02.09.2024 16:33

    Зависло после
    Answer received, connecting...

    Есть возможность включить отладочные сообщения?


    1. vlad_gatsenko Автор
      02.09.2024 16:33

      Не работает в самом начале статьи, где запуск через стандартный скомпилированный бинарник? Или в другом месте?


  1. Kil1J0y
    02.09.2024 16:33
    +1

    А чем это хуже https прокси или wstunnel? Есть сервер есть haproxy или ngnix, wstunnel может работать через прокси сервер по sni, вижу в этом методе лишь дополнительные уязвимости.


    1. vlad_gatsenko Автор
      02.09.2024 16:33

      А можно подробнее про уязвимости?


  1. d-sh
    02.09.2024 16:33
    +1

    Ставим Meshcentral себе на сервер и рулим через веб и гуем и консолью.


  1. OmSoft
    02.09.2024 16:33
    +2

    Рекомендую посмотреть в сторону https://github.com/butlerx/wetty. Несколько проще.


  1. Adeloyd
    02.09.2024 16:33

    Если это корпоративная сеть, в которой все перекрыто, то p2p не получится и все пойдет через сторонний сервер. По безопасности так себе решение.