Ответ 1
Основная цель проекта TCP состоит в том, чтобы обеспечить надежную передачу данных в условиях потери пакетов, переупорядочения пакетов и, в данном случае, дублирования пакетов.
Совершенно очевидно, как сетевой стек TCP/IP справляется со всем этим, пока соединение установлено, но есть крайний случай, который происходит сразу после закрытия соединения. Что произойдет, если пакет, отправленный в самом конце разговора, будет продублирован и задержан, так что пакеты с 4-сторонним отключением попадут в приемник раньше, чем пакет с задержкой? Стек покорно закрывает соединение. Затем позже, задержанный дубликат пакета обнаруживается. Что должен делать стек?
Что еще более важно, что делать, если программа с открытыми сокетами на заданном IP-адресе + комбинированный порт TCP закрывает свои сокеты, а затем ненадолго появляется программа и хочет прослушивать тот же IP-адрес и номер порта TCP? (Типичный случай: программа убивается и быстро перезапускается.)
Есть несколько вариантов:
-
Запретить повторное использование этой комбинации IP/порт как минимум в 2 раза больше максимального времени, в течение которого пакет может находиться в полете. В TCP это обычно называется задержкой 2 × MSL. Иногда вы также видите 2 × RTT, что примерно эквивалентно.
Это поведение по умолчанию для всех распространенных стеков TCP/IP. 2 × MSL обычно составляет от 30 до 120 секунд, и он отображается в выводе
netstat
как периодTIME_WAIT
. По истечении этого времени стек предполагает, что любые мошеннические пакеты были отброшены в пути из-за истекших TTL, так что сокет покидаетTIME_WAIT
, что позволяет повторно использовать эту комбинацию IP/порт. -
Разрешить новой программе выполнить повторную привязку к этому списку IP/портов. В стеках с интерфейсами BSD-сокетов - по существу, во всех Unixes и Unix-подобных системах, а также в Windows через Winsock - вы должны запросить это поведение, установив параметр
SO_REUSEADDR
помощьюsetsockopt()
прежде чем вызыватьbind()
.
SO_REUSEADDR
чаще всего устанавливается в программах сетевого сервера, поскольку обычным способом использования является изменение конфигурации, а затем потребуется перезапустить эту программу, чтобы изменения вступили в силу. Без SO_REUSEADDR
вызов bind()
в новом экземпляре перезапущенной программы завершится неудачей, если были соединения, открытые к предыдущему экземпляру, когда вы его уничтожили. Эти соединения будут удерживать порт TCP в TIME_WAIT
течение 30-120 секунд, поэтому вы попадаете в случай 1 выше.
Риск установки SO_REUSEADDR
заключается в том, что он создает неоднозначность: метаданные в заголовках пакетов TCP недостаточно уникальны, чтобы стек мог достоверно определить, является ли пакет устаревшим, и поэтому должен быть отброшен, а не доставлен в новый сокет слушателя, поскольку это было явно предназначено для уже мертвого слушателя.
Если вы не видите, что это правда, здесь все стек TCP/IP прослушивающей машины должен работать с каждым соединением, чтобы принять это решение:
-
Локальный IP: не уникально для каждого соединения. Фактически, в нашем определении проблемы здесь говорится, что мы намеренно повторно используем локальный IP.
-
Локальный порт TCP: тоже самое.
-
Удаленный IP-адрес: машина, вызывающая неоднозначность, может повторно подключиться, что не поможет устранить неоднозначность правильного назначения пакета.
-
Удаленный порт. В хорошо функционирующих сетевых стеках удаленный порт исходящего соединения используется не быстро, а только в 16 битах, поэтому у вас есть 30–120 секунд, чтобы заставить стек пройти несколько десятков тысяч из вариантов и повторно использовать порт. Компьютеры могли работать так быстро еще в 1960-х годах.
Если ваш ответ на этот вопрос заключается в том, что удаленный стек должен выполнить что-то вроде
TIME_WAIT
на своей стороне, чтобы запретить повторное использование порта TCP, то это решение предполагает, что удаленный хост является безопасным. Злоумышленник может повторно использовать этот удаленный порт.Я полагаю, что стек слушателя мог бы выбрать строго запрещать соединения только из TCP 4-кортежа, чтобы во время
TIME_WAIT
данному удаленному хосту было запрещено повторное соединение с тем же удаленным эфемерным портом, но я не знаю ни одного стека TCP с этим конкретным уточнением. -
Локальные и удаленные порядковые номера TCP: они также недостаточно уникальны, чтобы новая удаленная программа не могла получить те же значения.
Если бы мы сегодня перепроектировали TCP, я думаю, что мы бы интегрировали TLS или что-то в этом роде в качестве необязательной функции, одним из эффектов которой является невозможность такого рода непреднамеренного и злонамеренного перехвата соединения, но это требует добавления больших полей. (128 бит и выше), что было совсем не практично в 1981 году, когда был опубликован документ для текущей версии TCP (RFC 793).
Без такого усиления неоднозначность, созданная путем повторной привязки во время TIME_WAIT
означает, что вы можете либо a) иметь устаревшие данные, предназначенные для старого слушателя, неправильно доставлять в сокет, принадлежащий новому слушателю, тем самым либо нарушая протокол слушателя, либо неправильно вводя устаревшие данные в связи; или b) новые данные для нового сокета слушателя, ошибочно назначенные старому сокету слушателя и, таким образом, непреднамеренно отброшенные.
Безопасная вещь, чтобы сделать, это подождать период TIME_WAIT
.
В конечном итоге все сводится к выбору затрат: переждать период TIME_WAIT
или взять на себя риск нежелательной потери данных или непреднамеренного ввода данных.
Многие серверные программы берут на себя этот риск, решая, что лучше сразу же выполнить резервное копирование сервера, чтобы не пропустить больше входящих соединений, чем необходимо.
Это не универсальный выбор. Многие программы - даже серверные программы, требующие перезапуска, чтобы применить изменение настроек - вместо этого предпочитают оставить SO_REUSEADDR
покое. Программист может знать об этих рисках и предпочитает оставить дефолт в покое, или они могут не знать о проблемах, но получают выгоду от мудрого дефолта.
Некоторые сетевые программы предлагают пользователю выбор из вариантов конфигурации, снимая с себя ответственность перед конечным пользователем или системным администратором.