Недавно я занимался тем, что исследовал какие существуют решения для реализации web-ssh прокси-сервера. Суть задачи заключается в том, чтобы дать пользователям возможность соединяться с произвольным ssh-сервером посредством web-интерфейса. Обычно, решения web-ssh предназначены для соединения с сервером, на котором они развернуты, но в рамках моей задачи мне хотелось, чтобы пользователь мог указать IP, порт, имя и пароль пользователя (или ключ) и выполнить соединение с произвольным сервером. С ходу найти подобного решения мне не удалось.
Вообще-то, конечно, есть Guacamole, но для моей задачи использование этого приложения было слишком затратным как по ресурсам разработки, так и по функциям и их организации, поэтому от Guacamole я отказался.
Однако, для открытого пакета shellinabox я обнаружил решение на блоге на немецком языке, которое я и решил довести до нужного мне уровня. В итоге, получился симпатичный контейнер Docker, который можно найти как на GitHub так и на Dockerhub, который решает все необходимые задачи.
Но, статья не об этом, а о сопутствующем коде на Python, который мне пришлось написать. Дело в том, что мне не нравилось, что если пользователь открыл web ssh и куда-то ушел, то сессия будет висеть бесконечно, что на мой взгляд неприемлемо. Это ведет к следующим отрицательным последствиям:
- ресурсы прокси-сервера терминалов используются неэффективно, поскольку он вынужден обслуживать неиспользуемые сессии;
- открытое в браузере без присмотра окно с ssh навевает чувство тоски и уязвимости.
В общем, я решил, что хочу добиться того, чтобы shellinabox разрывал соединение в том случае, если пользователь несколько минут не пишет ничего на консоль (stdin) и на stdout не поступают данные.
Достаточно продолжительный поиск в Google показал мне, что цели можно добиться с использованием команды timeout и strace. Команда timeout оказалась для меня новой, ее назначение — обеспечить прерывание процесса по достижению некоторого таймаута в том случае, если он сам не завершился.
Команду strace я использую часто, однако, обычно применяю ее для того, чтобы отслеживать причину, по которой какая-то служба или команда не работает как ожидается. В рамках моего поиска на тему того, как осуществить мониторинг активности на каналах stdin, stdout процесса я обнаружил, что strace так же может это обеспечить:
strace -e write=1,2 -e trace=write -p <PID>
В общем, данные команды были тем, что мне было необходимо для осуществления задуманного. Общая схема выглядит так:
- При открытии нового соединения shellinabox запускает python-скрипт;
- Python-скрипт запускает (fork) процесс-демон мониторинга активности процесса, который осуществляет мониторинг своего же PID;
- python-скрипт выполняет (exec) ssh (замена себя на ssh).
Для тех, кто хочет сразу посмотреть как же устроен весь скрипт, отправляю сюда. Для остальных далее по частям.
Кратко, код можно представить следующим образом.
monitor_daemon(inactivity_interval, identity_file)
...
...
os.execv("/usr/bin/ssh", ["/usr/bin/ssh"] + identity_args + ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-p", str(peer_port), "%s@%s" % (peer_login, peer_ip)])
Код простой и самоочевидный. Вся магия находится внутри функции monitor_daemon
:
def monitor_daemon(inactivity_interval, identity_file):
orig_pid = os.getpid()
try:
pid = os.fork()
if pid > 0:
return
except OSError as e:
print("Fork #1 failed: %d (%s)" % (e.errno, e.strerror))
sys.exit(1)
os.chdir("/")
os.setsid()
os.umask(0)
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as e:
print("Fork #2 failed: %d (%s)" % (e.errno, e.strerror))
sys.exit(1)
if identity_file != "":
time.sleep(1)
os.unlink(identity_file)
try:
while True:
proc = subprocess.Popen('timeout %d strace -e write=1,2 -e trace=write -p %d' % (inactivity_interval, orig_pid), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc.poll()
counter = 0
for line in proc.stderr.readlines():
counter += 1
if(counter <= 3):
os.kill(orig_pid, signal.SIGKILL)
sys.exit(0)
except Exception as e:
pass
sys.exit(0)
где нас более всего интересует часть:
while True:
proc = subprocess.Popen('timeout %d strace -e write=1,2 -e trace=write -p %d' % (inactivity_interval, orig_pid), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc.poll()
counter = 0
for line in proc.stderr.readlines():
counter += 1
if(counter <= 3):
os.kill(orig_pid, signal.SIGKILL)
sys.exit(0)
в которой и происходит в цикле запуск мониторинга, который после заданного периода ожидания выполняет завершение процесса ssh, если требуется.
Вот собственно и все. Решение оказалось достаточно простым, но потребовало некоторое время на изучение доступных инструментов.
PS: Я не являюсь профессиональным python-разработчиком, код субоптимален.
PPS: Если у кого-то возникнет желание улучшить код, добро пожаловать в репозиторий, я с удовольствием приму PR-ы.