Создание TCP-прокси
Вы можете реализовать все 𝗧𝗖𝗣-взаимодействия, используя встроенный в 𝗚𝗼 пакет 𝗻𝗲𝘁. В предыдущем разделе мы сосредоточились главным образом на его применении с позиции клиента. В этом же разделе задействуем его для создания 𝗧𝗖𝗣-серверов и передачи данных. Изучение этого процесса начнется с создания эхо-сервера — сервера, который просто возвращает запрос обратно клиенту. Затем мы создадим две более универсальные в применении программы: переадресатор 𝗧𝗖𝗣-портов и 𝗡𝗲𝘁𝗰𝗮𝘁-функцию «зияющая дыра в безопасности», применяемую для удаленного выполнения команд.
Использование io.Reader и io.Writer
При создании примеров этого раздела вам потребуется задействовать два значимых типа: 𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿. Они необходимы для всех задач ввода/вывода (𝗜/𝗢) вне зависимости от того, задействуете вы 𝗧𝗖𝗣, 𝗛𝗧𝗧𝗣, файловую систему или любые другие средства. Будучи частью встроенного в 𝗚𝗼 пакета 𝗶𝗼, эти типы являются краеугольным камнем любой передачи данных, как локальной, так и сетевой. В документации они определены так:
Оба типа определяются как интерфейсы, то есть напрямую их создать нельзя. Каждый тип содержит определение одной экспортируемой функции: 𝗥𝗲𝗮𝗱 или 𝗪𝗿𝗶𝘁𝗲. Можно рассматривать эти функции как абстрактные методы, которые должны быть реализованы в типе, чтобы он считался 𝗥𝗲𝗮𝗱𝗲𝗿 или 𝗪𝗿𝗶𝘁𝗲𝗿. Например, следующий искусственный тип выполняет это соглашение и может использоваться там, где приемлем 𝗥𝗲𝗮𝗱𝗲𝗿:
Давайте с помощью них создадим что-нибудь полуготовое: настраиваемый 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿, обертывающий 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Код для этого тоже будет несколько искусственным, так как типы 𝗚𝗼 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 и 𝗼𝘀.𝗦𝘁𝗱𝗼𝘂𝘁 уже действуют как 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Но если не пытаться изобрести колесо, то ничему и не научишься, ведь так?
Ниже показана полная реализация, а далее дано пояснение.
Реализация reader и writer /io-example/main.go
Этот код определяет два пользовательских типа: 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В каждом типе вы определяете конкретную реализацию функции 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и функции 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В этом случае обе функции считывают из 𝘀𝘁𝗱𝗶𝗻 и записывают в 𝘀𝘁𝗱𝗼𝘂𝘁.
Обратите внимание на то, что функции 𝗥𝗲𝗮𝗱 и в 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿, и в 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 возвращают длину данных и все ошибки. Сами эти данные копируются в срез 𝗯𝘆𝘁𝗲, передаваемый этой функции. Это согласуется с начальным определением интерфейса 𝗥𝗲𝗮𝗱𝗲𝗿, приведенным в данном разделе ранее. Функция 𝗺𝗮𝗶𝗻() создает этот срез с названием 𝗶𝗻𝗽𝘂𝘁 и затем использует его в вызовах к 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲).
При пробном запуске программы мы получим следующий вывод:
Копирование данных из 𝗥𝗲𝗮𝗱𝗲𝗿 в 𝗪𝗿𝗶𝘁𝗲𝗿 — это настолько распространенный шаблон, что пакет 𝗶𝗼 содержит специальную функцию 𝗖𝗼𝗽𝘆(), которую можно задействовать для упрощения функции 𝗺𝗮𝗶𝗻(). Вот ее прототип:
Эта удобная функция позволяет реализовывать то же поведение программы, что и ранее, заменив 𝗺𝗮𝗶𝗻() кодом, показанным ниже.
Применение io.Copy /ch-2/copy-example/main.go
Обратите внимание, что явные вызовы 𝗿𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝘄𝗿𝗶𝘁𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([ ] 𝗯𝘆𝘁𝗲) были замещены одним вызовом 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿). Внутренне 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном ридере функцию 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲), в результате чего 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 выполняет считывание из 𝘀𝘁𝗱𝗶𝗻. Далее 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном райтере функцию 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲), что приводит к вызову 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿, записывающего данные в 𝘀𝘁𝗱𝗼𝘂𝘁. По сути, 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) обрабатывает последовательный процесс чтения/записи без лишних деталей.
Этот вводный раздел никак нельзя считать подробным рассмотрением системы 𝗜/𝗢 и интерфейсов в 𝗚𝗼. Многие вспомогательные функции и пользовательские ридеры/райтеры существуют как часть стандартных пакетов 𝗚𝗼. В большинстве случаев эти стандартные пакеты содержат все основные реализации, необходимые для реализации большинства распространенных задач. В следующем разделе мы рассмотрим применение всех этих основ к 𝗧𝗖𝗣-коммуникациям и в итоге применим полученные навыки для разработки реальных рабочих инструментов.
Создание эхо-сервера:
Как и во многих языках, изучение процесса чтения/записи данных с сокета мы начнем с построения эхо-сервера. Для этого будем использовать 𝗻𝗲𝘁.𝗖𝗼𝗻𝗻 — потокоориентированное соединение 𝗚𝗼, с которым вы уже познакомились при создании сканера портов. Как указано в документации для этого типа данных, 𝗖𝗼𝗻𝗻 реализует функции 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲) согласно определению для интерфейсов 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Следовательно, 𝗖𝗼𝗻𝗻 одновременно является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (да, такое возможно). Это вполне логично, так как 𝗧𝗖𝗣-соединения двунаправленные и могут использоваться для отправки (записи) и получения (чтения) данных.
После создания экземпляра conn вы сможете отправлять и получать данные через TCP-сокет. Тем не менее TCP-сервер не может просто создать соединение, его должен установить клиент. В 𝗚𝗼 для начального открытия 𝗧𝗖𝗣-слушателя на конкретном порте можно использовать 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴). После подключения клиента метод 𝗔𝗰𝗰𝗲𝗽𝘁() создает и возвращает объект 𝗖𝗼𝗻𝗻, который вы можете применять для получения и отправки данных.
Ниже показан полноценный пример реализации сервера. Для большей ясности мы добавили в код комментарии. Не стремитесь сразу понять весь код, так как позже мы подробно его поясним.
Базовый эхо-сервер /ch-2/echo-server/main.go
Базовый эхо-сервер начинается с определения функции 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻), которая принимает в качестве параметра экземпляр 𝗖𝗼𝗻𝗻. Он выступает в роли обработчика соединения, выполняя все необходимые операции 𝗜/𝗢. Эта функция повторяется бесконечно, используя буфер для считывания данных из соединения и их записи в него. Данные считываются в переменную 𝗯, после чего записываются обратно в соединение.
Теперь нужно настроить слушатель, который будет вызывать обработчик. Как ранее говорилось, сервер не может сам создать соединение и должен прослушивать подключение клиента. Следовательно, слушатель, определенный как 𝘁𝗰𝗽, привязанный к порту 𝟮𝟬𝟬𝟴𝟬, запускается во всех интерфейсах посредством функции 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴).
Далее бесконечный цикл обеспечивает, чтобы сервер продолжал прослушивание соединений даже после того, как оно было установлено. В этом цикле происходит вызов 𝗹𝗶𝘀𝘁𝗲𝗻𝗲𝗿.𝗔𝗰𝗰𝗲𝗽𝘁() ❻ — функции, блокирующей выполнение при ожидании подключений. Когда клиент подключается, эта функция возвращает экземпляр 𝗖𝗼𝗻𝗻. Напомним, что 𝗖𝗼𝗻𝗻 является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (реализует методы интерфейса 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲)).
После этого экземпляр 𝗖𝗼𝗻𝗻 передается в функцию обработки 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Перед ее вызовом указано ключевое слово 𝗴𝗼, делающее этот вызов многопоточным, в результате чего другие подключения в ожидании завершения функции-обработчика не блокируются. Это может показаться излишним для столь простого сервера, но мы добавили эту функциональность для демонстрации простоты паттерна многопоточности 𝗚𝗼 на случай, если вы еще не до конца его поняли. В данный момент у вас есть два легковесных параллельно выполняющихся потока.
Основной поток зацикливается и блокируется функцией listener.Accept() на время ожидания ею следующего подключения.
Горутина обработки, чье выполнение было передано в функцию echo(net.Conn), возобновляется и обрабатывает данные.
Далее показан пример использования 𝗧𝗲𝗹𝗻𝗲𝘁 в качестве подключающегося клиента:
Сервер производит следующий стандартный вывод:
Революционно, не правда ли? Сервер, возвращающий клиенту в точности то, что клиент ему отправил. Очень полезный и сильный пример!
Создание буферизованного слушателя для улучшения кода:
Пример в коде Базового эхо-сервера работает прекрасно, но он опирается на чисто низкоуровневые вызовы функции, отслеживание буфера и повторяющиеся циклы чтения/записи. Это довольно утомительный и подверженный ошибкам процесс. К счастью, в 𝗚𝗼 есть и другие пакеты, которые могут его упростить и уменьшить сложность кода. Говоря конкретнее, пакет 𝗯𝘂𝗳𝗶𝗼 обертывает 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 для создания буферизованного механизма 𝗜/𝗢. Далее приведена обновленная функция 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) с сопутствующим описанием изменений:
В экземпляре 𝗖𝗼𝗻𝗻 больше не происходит прямого вызова функций 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲). Вместо этого вы инициализируете новые буферизованные 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 через 𝗡𝗲𝘄𝗥𝗲𝗮𝗱𝗲𝗿(𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿) и 𝗡𝗲𝘄𝗪𝗿𝗶𝘁𝗲𝗿(𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿). Оба вызова в качестве параметра получают существующие 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 (помните, что тип 𝗖𝗼𝗻𝗻 реализует необходимые функции, чтобы считаться 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿).
Оба буферизованных экземпляра содержат вспомогательные функции для чтения и сохранения данных. 𝗥𝗲𝗮𝗱𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) получает символ-разграничитель, обозначая, до какой точки выполнять считывание, а 𝗪𝗿𝗶𝘁𝗲𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) записывает строку в сокет. При записи данных вам нужно явно вызывать 𝘄𝗿𝗶𝘁𝗲𝗿.𝗙𝗹𝘂𝘀𝗵() для сброса всех данных внутреннему райтеру (в данном случае экземпляру 𝗖𝗼𝗻𝗻).
Несмотря на то что предыдущий пример упрощает процесс, применяя буферизацию 𝗜/𝗢, вы можете переработать его под использование вспомогательной функции 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿). Напомним, что функция получает в качестве ввода целевой 𝗪𝗿𝗶𝘁𝗲𝗿 и исходный 𝗥𝗲𝗮𝗱𝗲𝗿, просто выполняя копирование из источника в место назначения.
В этом примере вы передаете переменную 𝗰𝗼𝗻𝗻 и как источник, и как место назначения, так как в итоге будете отражать содержимое обратно в установленное соединение:
Вот вы и познакомились с основами системы 𝗜/𝗢, попутно применив ее к 𝗧𝗖𝗣-серверам. Пришло время перейти к более полезным и представляющим для вас интерес примерам.
Проксирование TCP-клиента:
Теперь, когда у вас под ногами есть твердая почва, можете применить полученные навыки для создания простого переадресатора портов для проксирования соединения через промежуточный сервис или хост. Как уже говорилось, это пригождается для обхода ограничивающего контроля исходящего трафика или использования системы с целью обхода сегментации сети. Прежде чем перейти к коду, рассмотрите вымышленную, но вполне реалистичную задачу: Андрей является малоэффективным сотрудником компании 𝗔𝗖𝗠𝗘 𝗜𝗻𝗰., работая на должности бизнес-аналитика и получая приличную зарплату просто потому, что слегка приукрасил данные своего резюме. (Неужели он реально учился в школе Лиги плюща? Андрей, такой обман неэтичен.) Недостаток мотивации Андрея может по силе сравниться разве что с его любовью к кошкам — такой сильной, что он даже установил дома специальные видеокамеры и создал сайт 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲, через который удаленно следил за своими мохнатыми питомцами. Тем не менее здесь была одна сложность: 𝗔𝗖𝗠𝗘 следит за Андреем. Им не нравится, что он круглые сутки передает потоковое видео своих кошек в ультравысоком разрешении 𝟰𝗞, занимая ценный пропускной канал сети. Компания даже заблокировала своим сотрудникам возможность посещать его кошачий сайт.
Но у хитрого Андрея и здесь возник план: «А что, если я настрою переадресатор портов в подконтрольной мне интернет-системе и буду перенаправлять весь трафик с этого хоста на 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲?» На следующий день Андрей отмечается на работе и убеждается в возможности доступа к личному сайту, размещенному на домене 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺. Он пропускает все встречи после обеда и отправляется в кафетерий, где быстро пишет код для своей задачи, подразумевающей перенаправление на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 всего входящего на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 трафика.
Вот код Андрея, который он запускает на сервере 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺:
Начнем с рассмотрения функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Андрей подключается к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 (вспомните, что этот хост недоступен напрямую с его рабочего места). Затем он использует 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в двух разных местах. Первый экземпляр обеспечивает копирование данных из входящего соединения в соединение 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲. Второй же обеспечивает, чтобы считанные из 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 данные записывались обратно в соединение подключающегося клиента. Так как 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) является блокирующей функцией и будет продолжать блокировать выполнение, пока сетевое соединение открыто, Андрей предусмотрительно обертывает первый вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в новую горутину. Это гарантирует продолжение выполнения в функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) и дает возможность выполнить второй вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿).
Прокси-сервер Андрей прослушивает порт 𝟴𝟬 и ретранслирует весь трафик, получаемый через это соединение, на порт 𝟴𝟬 сайта 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 и обратно. Этот безумный и расточительный парень убеждается, что может подключаться к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 через 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 с помощью 𝗰𝘂𝗿𝗹:
Так Андрей успешно реализует коварный замысел. Он прекрасно устроился, получив возможность в оплачиваемое 𝗔𝗖𝗠𝗘 время использовать их же канал связи для наблюдения за жизнью своих питомцев.
Воспроизведение функции Netcat для выполнения команд:
В этом разделе мы воспроизведем одну из наиболее интересных функций 𝗡𝗲𝘁𝗰𝗮𝘁 — «зияющую дыру в безопасности».
𝗡𝗲𝘁𝗰𝗮𝘁 — это как швейцарский армейский нож для 𝗧𝗖𝗣/𝗜𝗣, который представляет собой более гибкую версию 𝗧𝗲𝗹𝗻𝗲𝘁 с поддержкой сценариев. Эта утилита имеет возможность перенаправлять 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 любой произвольной программы через 𝗧𝗖𝗣, позволяя атакующему, например, превратить уязвимость к выполнению одной команды в доступ к оболочке операционной системы. Взгляните:
Эта команда создает прослушивающий сервер на порте 𝟭𝟯𝟯𝟯𝟳. Любой подключающийся, возможно, через 𝗧𝗲𝗹𝗻𝗲𝘁, клиент сможет выполнить любые команды 𝗯𝗮𝘀𝗵 — вот почему данную функцию и называют зияющей дырой в безопасности. 𝗡𝗲𝘁𝗰𝗮𝘁 позволяет при желании включить такую возможность в процессе компиляции программы. (По понятным причинам большинство исполняемых файлов 𝗡𝗲𝘁𝗰𝗮𝘁 в стандартных сборках 𝗟𝗶𝗻𝘂𝘅 ее не включают.) Эта функция настолько потенциально опасна, что мы покажем, как воссоздать ее в 𝗚𝗼.
Для начала загляните в пакет 𝗚𝗼 𝗼𝘀/𝗲𝘅𝗲𝗰. Он будет использоваться для выполнения команд операционной системы. Этот пакет определяет тип 𝗖𝗺𝗱, который содержит необходимые методы и свойства для выполнения команд и управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Вы будете перенаправлять 𝘀𝘁𝗱𝗶𝗻 (𝗥𝗲𝗮𝗱𝗲𝗿) и 𝘀𝘁𝗱𝗼𝘂𝘁 (𝗪𝗿𝗶𝘁𝗲𝗿) в экземпляр 𝗖𝗼𝗻𝗻, представляющий и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿.
При получении нового подключения создать экземпляр 𝗖𝗺𝗱 можно с помощью функции 𝗖𝗼𝗺𝗺𝗮𝗻𝗱(𝗻𝗮𝗺𝗲 𝘀𝘁𝗿𝗶𝗻𝗴, 𝗮𝗿𝗴 ...𝘀𝘁𝗿𝗶𝗻𝗴) из 𝗼𝘀/𝗲𝘅𝗲𝗰. Эта функция получает в качестве параметров команды ОС и любые аргументы. В данном примере нужно жестко закодировать в качестве команды /𝗯𝗶𝗻/𝘀𝗵 и передать в качестве аргумента -𝗶, чтобы перейти в интерактивный режим, из которого можно будет управлять потоками 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 более уверенно:
Эта инструкция создает экземпляр 𝗖𝗺𝗱, но команду еще не выполняет. Здесь для управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 есть два варианта: использовать 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿), как говорилось ранее, или напрямую присвоить 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 экземпляру 𝗖𝗺𝗱. Давайте непосредственно присвоим объект 𝗖𝗼𝗻𝗻 экземплярам 𝗰𝗺𝗱.𝗦𝘁𝗱𝗶𝗻 и 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:
После настройки команды и потоков запустить ее можно с помощью 𝗰𝗺𝗱.𝗥𝘂𝗻():
Такая логика прекрасно работает для систем 𝗟𝗶𝗻𝘂𝘅. Тем не менее при настройке и запуске этой программы под 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 с помощью 𝗰𝗺𝗱.𝗲𝘅𝗲, а не /𝗯𝗶𝗻/𝗯𝗮𝘀𝗵, подключающийся клиент не получает вывод команды из-за специфичной для 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 обработки анонимных каналов. Далее описаны два решения этой проблемы.
Во-первых, можно настроить код для принудительного сброса 𝘀𝘁𝗱𝗼𝘂𝘁. Вместо непосредственного присваивания 𝗖𝗼𝗻𝗻 экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁 нужно реализовать собственный 𝗪𝗿𝗶𝘁𝗲𝗿, который обертывает 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 (буферизованный райтер) и явно вызывает его метод 𝗙𝗹𝘂𝘀𝗵 для принудительного сброса буфера. Пример использования 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 можно найти в разделе «Создание эхо-сервера» ранее в этой главе. Вот определение пользовательского райтера, 𝗙𝗹𝘂𝘀𝗵𝗲𝗿:
Тип 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 реализует функцию 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲), которая записывает данные во внутренний буферизованный райтер, а затем сбрасывает вывод.
С помощью этой реализации пользовательского райтера можно настроить обработчик подключений на создание экземпляра и применение типа 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 для 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:
Продолжение кода выше
Это решение хотя и вполне пригодно, но не очень элегантно. Несмотря на то что рабочий код для нас важнее, чем аккуратный, мы используем эту проблему как возможность рассказать о функции 𝗶𝗼.𝗣𝗶𝗽𝗲(). Она представляет собой синхронный канал в памяти 𝗚𝗼, который можно задействовать для подключения 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿:
Применение 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗣𝗶𝗽𝗲𝗪𝗿𝗶𝘁𝗲𝗿 позволяет избежать необходимости явного сброса райтера и синхронного подключения 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝗧𝗖𝗣-соединения. Опять же понадобится переписать функцию обработчика:
Вызов 𝗶𝗼.𝗣𝗶𝗽𝗲 создает ридер и райтер, подключаемые синхронно, — любые данные, записываемые в райтер (в данном примере 𝘄𝗽), будут считаны ридером (𝗿𝗽). Поэтому сначала происходит присваивание райтера экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁, после чего используется 𝗶𝗼.𝗖𝗼𝗽𝘆(𝗰𝗼𝗻𝗻, 𝗿𝗽) для присоединения 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 к 𝗧𝗖𝗣-соединению. Это делается с помощью горутины, предотвращающей блокирование кода. Любой стандартный вывод команды отправляется райтеру, после чего передается ридеру и далее через 𝗧𝗖𝗣-соединение. Как вам такая элегантность?
Таким образом, мы успешно реализовали «зияющую дыру безопасности» 𝗡𝗲𝘁𝗰𝗮𝘁 с позиции 𝗧𝗖𝗣-слушателя, ожидающего подключения. По тому же принципу можно реализовать эту функцию с позиции подключающегося клиента, перенаправляющего 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝘀𝘁𝗱𝗶𝗻 локального исполняемого файла удаленному слушателю. Детали этого процесса мы оставим вам для самостоятельной реализации, но в общем они будут включать следующее:
установку подключения к удаленному слушателю через net.Dial(network, address string);
инициализацию Cmd через exec.Command(name string, arg ...string);
перенаправление свойств Stdin и Stdout для использования объекта net.Conn;
выполнение команды.
На этом этапе слушатель должен получить подключение. Любые передаваемые клиенту данные должны интерпретироваться на клиенте как 𝘀𝘁𝗱𝗶𝗻, а данные, получаемые слушателем, — как 𝘀𝘁𝗱𝗼𝘂𝘁.
На этом всё, ждите в ближайшее время больше статей.