суббота, 17 апреля 2021 г.

[prog.wtf] Пример творческого подхода некоторых софтописателей к трактовке спецификаций

Есть такой простой как две копейки протокол: SOCKS5. Этот протокол подразумевает последовательный обмен всего лишь несколькими сообщениями. Но именно обмен в режиме "запрос-ответ". Т.е. клиент отсылает первое сообщение, затем ждет, что ответит сервер, потом шлет второе сообщение.

Процедура подключения клиента к SOCKS5 серверу проста.

Клиент подключается и шлет первый PDU, в котором перечисляются методы аутентификации, поддерживаемые клиентом (например: без аутентификации вообще, на базе username/password).

Сервер анализирует этот список, выбирает подходящий для себя метод и отсылает ответное сообщение клиенту. В этом сообщении находится идентификатор выбранного сервером метода или специальное значение, указывающее, что сервер не хочет или не может аутентифицировать клиента.

Получив ответ от сервера клиент должен прислать следующее сообщение, в котором будут находится параметры для выбранного способа аутентификации. Так, если клиент и сервер договорились об использовании метода username/password, то во втором сообщении от клиент должны прийти username и password.

Итак, простая схема, в которой клиент шлет сообщение серверу, сервер отвечает, затем клиент шлет второе сообщение и т.д.

Однако, как оказалось, некоторые софтописатели применяют любопытный хак: они шлют два первых сообщения от клиента сразу. Так, в первом PDU клиент говорит, что он желает использовать только способ аутентификации по username/password. И тут же приклеивает к первому PDU второй, который как раз и содержит username/password.

Если клиент точно знает, что сервер поддерживает аутентификацию по username/password и что у клиента валидные username/password, то такой подход может казаться работающим. Ну, действительно, ведь от сервера в ответ на первое сообщение всегда будет приходить одно и то же. Так зачем заморачиваться на ожидание этого "одного и того же", чтобы лишь затем отсылать второе сообщение? Можно же просто отослать два сообщения подряд и проигнорировать первые два байта из ответного потока сервера. Ведь эти два байта будут иметь заранее определенные значения. А если вдруг сервер почему-то ответил как-то не так, то он все равно порвет соединение когда дойдет до разбора второго PDU.

Только вот тут все начинает зависеть от того, как именно реализован SOCKS5 сервер.

Если этот сервер использует множество мелких операций чтения, скажем:

  • читает первый байт первого PDU, проверяет его значение;
  • затем читает второй байт первого PDU. Это будет количество идентификаторов методов аутентификации;
  • затем читает N байт (где N -- это количество идентификаторов методов аутентификации, каждый идентификатор имеет длину в один байт).

то такой сервер автоматически остановится после разбора байт, относящихся к первому PDU. Отошлет ответ клиенту, затем начнет по байтам читать второй PDU.

И такой тривиальной реализации SOCKS5 сервера будет без разницы, пришло ли от клиента два PDU сразу или же они приходили по отдельности.

Однако, такое побайтовое чтение неэффективно. И плохо масштабируется, если ваш SOCKS5 сервер должен обслуживать десятки тысяч параллельных подключений.

Так что эффективнее читать блоками.

Например, мы выделяем буфер на 257 байт (это максимальный размер первого PDU) и читаем в него не более 257 байт из сокета.

Прочитали сколько-то байт и начали анализировать содержимое буфера:

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

Тут фокус в том, что делать, если мы взяли второй байт и увидели, что в буфере-то у нас содержится несколько больше данных, чем должно было бы быть.

Что это такое?

Очень похоже на какой-то мусор, первый байт которого по случайности совпал с номером версии SOCKS5 протокола.

Можно ли этому мусору доверять?

Думаю, что нет. Мы ждем первое сообщение, которое, согласно протоколу, должно прийти в единственном числе. А второе может у нас оказаться лишь после того, как мы отослали ответ на первое.

Так что, если клиент прислал сразу два PDU не дожидаясь ответа на свой первый PDU, то клиент, имхо, нарушает спецификацию протокола SOCKS5. И должен идти лесом.


К сожалению, среди программистов слишком часто встречаются творческие (скажем так) личности, которые делают то, что не следует делать. А когда им демонстрируешь что они (мягко говоря) поступают неправильно, то в ответ слышишь неубиваемый аргумент: "Так оно же работает!"

Хотя, зачастую, работает только благодаря тому, что на другой стороне код налабала на коленке когда-то не менее творческая личность. Минус на минус в итоге дает плюс. И оно же работает! :)

5 комментариев:

Denys Soroka комментирует...

Внезапно "оптимистичный" способ - послать все недожидаясь ответа, более правильный по ряду причин перечисленных в socks v6 draft

> The client sends as much information upfront as possible, and does not wait for the authentication process to conclude before requesting the creation of a socket.

ну и кроме того - там же явно есть размер пакета, зачем заглядывать за его границу?

eao197 комментирует...

@Denys Soroka

> ну и кроме того - там же явно есть размер пакета, зачем заглядывать за его границу?

Ну вот я смотрю на размер пришедших данных и вижу, что их больше, чем должно быть. Вопрос: откуда мне знать, что данные корректны, а не являются каким-то мусором?

Denys Soroka комментирует...

@eao197

> Вопрос: откуда мне знать, что данные корректны, а не являются каким-то мусором?

Этот вопрос неправильный, правильный вопрос - когда сервер может точно вынести вердикт "мусор/не мусор"? Ответ - после того как отошлет ответ на текущий запрос. И только в этот момент у сервера появляется право на последующую обработку.

eao197 комментирует...

@Denys Soroka

> Этот вопрос неправильный, правильный вопрос - когда сервер может точно вынести вердикт "мусор/не мусор"?

Вопрос хороший. Только вот ваш ответ не убедителен. Точнее, это не ответ, это утверждение. Без аргументации.

Denys Soroka комментирует...

@eao197

> Точнее, это не ответ, это утверждение. Без аргументации.
Да, именно, это аксиома. Она либо принимается и тогда сервер работает согласно RFC. Либо нет - и тогда сервер реализует другой, похожий на сокс-5 протокол.