В углу здания студенческого клуба есть кофейня, и в углу этой кофейни сидят два студента. Лиз стучит по клавиатуре потрёпанного древнего MacBook, который ей подарил брат перед отъездом в колледж. Слева от неё на диване Тим пишет уравнения в пружинном блокноте. Между ними стоит наполовину пустая кружка с кофе комнатной температуры, из которой Лиз время от времени потягивает напиток, чтобы не заснуть.

На другом конце кофейни бариста оторвался от своего телефона, чтобы окинуть взглядом помещение. Один наушник вставлен в ухо, второй висит, телефон воспроизводит видео с заданием, выданным на лекции по кинематографии. В кофейне, где работают студенты, есть неписанное правило: в ночную смену сотрудники могут пользоваться длинными промежутками между клиентами, чтобы выполнять домашние задания. Кроме Тима и Лиз в помещении по одиночке сидит ещё пара студентов, уже много часов сосредоточенно смотрящих в свои ноутбуки. Вся остальная часть кофейни пуста.

Тим останавливается на половине строки, вырывает лист из блокнота, комкает его и кладёт рядом с небольшой горкой других скомканных листов.

«Чёрт, а сколько времени?», — спрашивает он.

Лиз переводит взгляд на часы на экране ноутбука. «Два с небольшим».

Тим зевает и начинает писать с начала новой страницы, но Лиз его прерывает.

«Тим».

«Что?», — отвечает Тим, преувеличенно демонстрируя своё раздражение от того, что его прервали, когда он только начал писать.

«Что значит „прослушивать порт“?»

«Хм».

«Мне нужно написать веб-сервер для курса net», — Лиз сокращает полное название курса Computer Networks 201, который Тим прошёл в прошлом семестре.

«Ага, помню такое».

«И я слушаю соединения к порту».

«Порт 80», — уверенно отвечает Тим, надеясь прервать разговор, опередив её вопрос.

«На самом деле, мы должны прослушивать 8080, чтобы он мог работать без рута, но я не об этом».

«Ну ладно, тогда о чём?»

«Что значит прослушивание порта?»

«Это значит, что другие процессы могут подключаться к серверу по этому порту», — похоже, Тима этот вопрос сбил с толку.

«Да, это я знаю, но как

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

«Наверно, у операционной системы есть большая таблица портов и слушающих их процессов. Когда ты привязываешься к порту, она помещает указатель на твой сокет в эту таблицу».

«Ага, наверно», — отвечает Лиз нерешительно.

Каждый из них возвращается к своей работе. Спустя какое-то время тишину прерывает победоносное тихое «Да!» Тима, он зачёркивает номер на распечатанном листе бумаги. Он наконец нашёл доказательство, которое с трудом искал для задачи по матанализу.

Лиз воспользовалась возможностью снова привлечь его внимание.

«Смотри, Тим, я запустила два процесса, одновременно привязанные к одному порту».

Она разворачивает два окна с кодом на Python:

# server1.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())

И рядом с ним:

# server2.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', 8080))
print(sock.recv(1024))

Потом она показывает, что обе программы работают в отдельных окнах терминала через shell-подключение к Debian-серверу университета cslab3.

Тим разворачивает ноутбук к себе. Открывает третий терминал, на секунду останавливается, освежая воспоминания в своём усталом мозгу, и вводит netcat 127.0.0.1 8080.

netcat запускается и мгновенно завершается. В другом окне терминала завершается запущенная программа python server1.py, выводя следующее:

(<socket.socket fd=4, family=AddressFamily.AF_INET,
type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080),
raddr=('127.0.0.1', 59558)>, ('127.0.0.1', 59558))


Он изучает код server1.py, рассуждая вслух.

«Итак, сервер выполняет привязку к порту, принимает первый сокет для подключения, а затем выполняет выход. Понятно, значит выведенный на экран кортеж был результатом вызова accept, после чего выполняется выход. Но теперь (он наводит курсор на редактор с кодом server2.py) этот код вообще что-то прослушивает?»

Он снова запускает netcat 127.0.0.1 8080 -v в том же терминале, что и раньше, и в нём выводится следующее:

netcat: connect to 127.0.0.1 port 8080 (tcp) failed: Connection refused

«Видишь», — говорит он — «в твоём коде баг. server2 по-прежнему запущен, но ты не вызываешь listen. На самом деле он ничего не делает с портом 8080».

«Да нет, точно делает», — отвечает Лиз, хватая ноутбук.

Она добавляет -u в конец команды netcat и нажимает Enter. На этот раз она не выдаёт ошибку и не выходит сразу же, а ждёт ввода с клавиатуры. Раздражённая тем, что Тим сразу предположил наличие бага в её коде, она набирает timmy, зная, что это имя его бесит.

Сессия netcat завершается без вывода и одновременно программа python server2.py завершает работу, выведя:

b'timmy\n'

Тим замечает попытку Лиз поддеть его, но игнорирует её, не желая доставлять ей удовольствие. Он тянется к клавиатуре. Лиз поворачивает ноутбук к нему и он вводит man netcat, чтобы открыть документацию по команде netcat, в которой этот инструмент описывается как «швейцарский армейский нож для TCP/IP». Он доходит до флага -u, который в документации кратко описан как «UDP mode».

«Ага», — говорит он, вспомнив. «Понял, server1 слушает по TCP, а server2 слушает по UDP. Наверно, это и означает SOCK_DGRAM. То есть это разные протоколы. Думаю, у операционной системы есть отдельные таблицы для каждого. Кажется, тему UDP в курсе net проходят позже».

«Ага, я прочитала учебник заранее».

«Естественно. Как это у тебя есть время читать лишние темы, но нет времени выполнять задания так, чтобы не пришлось делать их ночью перед сдачей?»

«Могу задать тебе тот же вопрос про Counter Strike», — парирует Лиз.

Тим ворчит.

Они снова начинают работать в тишине, но спустя несколько минут Лиз её прерывает.

«Тим, посмотри-ка. Я могу прослушивать один порт двумя процессами, даже если они оба TCP».

Тим отвлекается от своей работы. На этот раз на экране у Лиз всего одна программа на Python, запущенная в двух терминалах:

# server3.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())

Лиз объясняет: «Видишь, эта команда показывает, что процесс прослушивает порт». Она вводит lsof -i:8080 и нажимает на Enter.

Программа выводит следующее:

> lsof -i:8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 174265 liz 3u IPv4 23850797 0t0 TCP localhost:http-alt (LISTEN)
python3 174337 liz 3u IPv4 23853188 0t0 TCP localhost:http-alt (LISTEN)


«Что произойдёт, если к нему подключиться?», — спрашивает Тим, на этот раз с долей интереса.

«Смотри».

Лиз один раз выполняет netcat localhost 8080, и один из процессов сервера завершается, а второй продолжает работать. При повторном выполнении команды завершается и другой процесс.

Тим начинает изучать код и водит пальцем по экрану, чтобы читать его. Лиз ненавидит заляпанный экран, поэтому говорит «аккуратно!» и отталкивает его руку. «Я не буду касаться», — возражает Тим. Держа руку на подчёркнуто безопасном расстоянии, он указывает на строку с setsockopt и спрашивает: «А это что ещё за магия?»

«Здесь мы задаём опцию сокета, позволяющую многократно использовать порт».

«Хм, это есть в учебнике?»

«Не знаю, нашла это на Stack Overflow».

«Не думал, что можно так использовать порт несколько раз».

«Я тоже», — она остановилась и подумала. «То есть в операционной системе не может быть просто таблица портов к сокетам, это должна быть таблица портов к списку сокетов. И ещё одна для UDP. И, возможно, для других протоколов».

«Ага, вроде логично», — соглашается Тим.

«Хм-м-м», — говорит Лиз, внезапно её голос стал менее уверенным.

«Что?»

«Ой, да ладно», — отвечает она и начинает сосредоточенно что-то печатать.

Тим возвращается к своему заданию, и спустя несколько минут он перечёркивает ещё один вопрос. Он почти закончил, и его поза становиться более расслабленной. Лиз наклоняет к нему ноутбук и говорит: «Зацени». Она показывает ему две программы.

# server4.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.2', 8080))
sock.listen()
print(sock.accept())

# server5.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.3', 8080))
sock.listen()
print(sock.accept())

«Разве они не одинаковые?», — спрашивает Тим после изучения кода.

«Посмотри на IP привязки».

«А, так ты слушаешь один порт, но два разных IP. И это работает?»

«Похоже, да. И я могу подключиться к обоим».

Лиз выполнила netcat 127.0.0.2, а затем netcat 127.0.0.3.

Тим задумался. «Так, посмотрим. У операционной системы должна быть таблица от каждого сочетания порта и IP к сокету. Хотя нет, на самом деле, две: одна для TCP и одна для UDP».

«Ага», — кивнула Лиз. «И это может быть не один, а много сокетов. Но посмотри». Она меняет IP в коде сервера на 0.0.0.0.

# server6.py
import socket

sock socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 8080))
sock.listen()
print(sock.accept())

«Теперь когда я запускаю сервер, привязанный к 127.0.0.2, то получаю следующее», — продолжает она.

Traceback (most recent call last):
File "server5.py", line 4, in <module>
s.bind(('127.0.0.2', 8080))
OSError: [Errno 99] Cannot assign requested address


«Но если я выполню netcat 127.0.0.2 8080, то подключение к серверу будет по адресу 0.0.0.0».

«Ну да, 0.0.0.0 означает „привязаться ко всем локальным IP“, разве в лекции об этом не говорили? И адреса, начинающиеся с 127. — это локальные IP loopback-а, поэтому логично, что они привязаны именно так».

«Ага, но как это работает? Есть примерно 16 миллионов IP, начинающихся с 127.. Операционная система ведь не создаёт большую таблицу со всеми этими адресами?»

«Думаю, нет», — у него не было ответа, поэтому он решил сменить тему. «Ну ладно, а как там дела с твоим HTTP-сервером?», — вопрос риторический, ведь он знает, что Лиз ещё не написала ни одной строки самого задания.

Она отвечает что-то неопределённое, потому что её уже поглотил другой эксперимент.

Прошло ещё немного времени. Завершив свою работу, Тим поглядывает на время на своём телефоне. Он подумывает, не пойти ли домой, к своему неровному матрасу в общежитии. Он решил, что диван кофейни такой же удобный и откинул голову на его высокую спинку.

Полусонный, он смотрит на потолок, и тут Лиз толкает его и говорит: «Тим, посмотри».

Она показывает ему ещё одну программу:

# server7.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())

«Зацени. Это IPv6-сервер».

Тим зевнул и придвинулся. К тому времени утреннее солнце начало светить через окно на диван, на котором они сидели. Два других студента незаметно вышли из кофейни в первые утренние часы и пришёл первый дневной клиент, ожидавший своего кофе на вынос.

«Что это за двоеточия?», — спросил Тим.

«Это краткая запись восьми нулей в IPv6, они означают то же, что и 0.0.0.0 в IPv4».

«То есть этот код приказывает прослушивать все локальные IP IPv6? Так работает IPv6?»

«Ну да, по сути, так».

Она ввела netcat "::1" 8080 -v и объяснила, что ::1 — это loopback-адрес в IPv6.

«То есть типа 127.0.0.1 для обычных IP».

«IPv4. Да, именно. Но посмотри сюда. По данным lsof, я слушаю только по IPv6, видишь?», — Лиз выполнила lsof -i :8080, и команда вывела одну строку

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 455017 liz 3u IPv6 25152485 0t0 TCP *:http-alt (LISTEN)


«Но я могу подключиться к нему через IP IPv4».

netcat 127.0.0.1 8080 -v

«Хм, а наоборот? Можно подключиться к IPv4-серверу с IP IPv6?»

«Неа, смотри».

Она запустила python3 server6.py, а затем netcat "::1" 8080 -v, получив такой результат:

netcat: connect to ::1 port 8080 (tcp) failed: Connection refused

Тим спросил: «А что будет, если попробовать прослушать IPv6 по 8080, когда IPv4-сервер продолжает работать?»

Лиз показала ему, запустив python server7.py.

Traceback (most recent call last):
File "server7.py", line 4, in <module>
s.bind(('::', 8080))
OSError: [Errno 98] Address already in use


«Но посмотри», — сказала она, открыв код ещё одной программы.

# server8.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())

Она показала на строку с setsockopt, объяснив: «Если я добавляю это, то могу слушать по IPv6 и IPv4 через один порт из разных процессов».

Она запустила python server8.py, а затем lsof -i :8080.

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 460409 liz 3u IPv6 25188010 0t0 TCP *:http-alt (LISTEN)
python3 460813 liz 3u IPv4 25191765 0t0 TCP *:http-alt (LISTEN)


Тим подвёл итог тому, что ему показала Лиз. «То есть при прослушивании порта ты на самом деле слушает комбинацию порта, IP-адреса, протокола и версии IP?»

«Да, только если ты не прослушиваешь все локальные IP. И если ты прослушиваешь все IP IPv6 ты одновременно прослушиваешь все IP IPv4, если только специально не откажешься от этого перед вызовом bind».

«Понятно. То есть у операционной системы должна быть какая-то hash map от пары порта и IP к сокету для каждой комбинации TCP или UDP, IPv4 или IPv6».

«К списку сокетов», — поправила его Лиз. «Помнишь, что я могла прослушивать несколько сокетов?»

«А, ну да».

«Но ей нужно ещё и обрабатывать прослушивание всех „домашних“ IP, и иметь возможность находить сокет, прослушивающий IPv6 с IP IPv4».

«Ну ладно, мне надо это сдать», — сказал Тим, показав на кучу листов бумаги. «А ты закончишь свой HTTP-сервер к сроку?»

Лиз пожала плечами: «У меня ещё есть сегодня время».

Тим покачал головой, как недовольный родитель.

Лиз закатила глаза и сказала: «Беги, Тим».

«В то же время на следующей неделе?»

«Ага».

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


  1. z0ic
    14.02.2022 16:47
    +15

    "Какие же вы счастливые" - подумал я, подключая libuv.


  1. Kekushiftkey
    14.02.2022 17:38
    +4

    необычный формат)

    вообще клево то, что тут описаны различные решения, в то время как обычно пишут итоговый результат без описания проб и ошибок


  1. amarao
    14.02.2022 17:39
    +4

    Это они ещё интерфейсы не посчитали (можно слушать на интерфейсе), что становится особо интересно после переименования интерфейсов.

    listen_app -i eth0
    ip link set name foo dev eth0
    ip link add type veth name eth0
    ip link set up dev veth0
    listen_app -i eth0

    Два приложения слушают eth0, но это разные eth0!

    В целом, статья выглядит как попытка "вскочить в вопрос" не прочитав man по сокетам.


    1. MaryRabinovich
      16.02.2022 20:25

      Немного оффтоп: не подскажете ли, что стоит прочесть про сокеты и порты чисто техническое? Что обычно рекомендуют? Достаточно краткое, при этом внятное.

      И без лирических героев.


      1. amarao
        16.02.2022 21:04
        +1

        man 2 socket

        Дальше там:

        SEE ALSO
        
           “An Introductory 4.3BSD Interprocess Communication Tutorial”  and  “BSD
           Interprocess  Communication  Tutorial”,  reprinted in UNIX Programmer's
           Supplementary Documents Volume 1.

        Обоих довольно много для понимания. Ещё есть man 7 socket.


        1. MaryRabinovich
          16.02.2022 21:30

          Спасибо:)

          Попробовала для начала вчитаться в ман 2.

          ... верно ли я понимаю, что речь реально про линукс, а это вот всё на питоне - только к нему оболочка? А что тогда происходит в винде? То же, что в линуксе? Ну, я списала эти вот два первых отрывка (ещё пока читала статью) "сервер 1" и "сервер 2" к себе на виндуДомашюю, на питон. Запустила, банально в Idle. И моя Idle начала слушать не пойми что (в смысле что ожидала ввода, не реагировала на Ctrl+C, хуже того - в процессах на Ctrl+Alt+Del не значилось ничего отдельного, только открытый браузер да тот самый питон).

          Куда же вот это вот всё направилось на винде?


          1. amarao
            17.02.2022 15:15

            Есть ядро (the kernel), которое предоставляет набор системных вызовов для работы с сокетами. Поверх есть libc, которая на 90% враппер поверх ядра, плюс небольшой фэнсервис. Поверх уже python (и его библиотеки), которые этот "фэнсервис" используют.

            Т.е. реальную работу делает ядро, libc приглаживает углы и разницу в версиях а python и его библиотеки используют.

            Теоретически можно себе представить альтернативный сетевой стек в userspace (dpdk, rdma, etc), но это крайняя экзотика.

            Сокет - это абстракция операционной системы по взаимодействию процессов по сети (AF_INET) или локально (AF_UNIX).

            В windows аналогично, хотя точная связь ядро (a kernel) и его библиотек (всякие winapi и т.д.) различается.

            Все разработчики ОС ориентируются на упомянутую "BSD Interprocess Communication Tutorial", хотя и докидывают всякие фичи/несовместимости.

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


  1. Wesha
    14.02.2022 20:46
    +1

    Вот и пришло то время, когда молодёжь стала изучать систему "методом чёрного ящика", то есть заранее не имея представления, как она там, под капотом, работает. Наверно, уже надо сказать спасибо, что хоть начала изучать...

    Всю статью ждал какого-то откровения, которого я не знал. Не дождался, чуда не случилось.


    1. NTDLL
      14.02.2022 20:52
      +7

      Да ну его нафик этот капот. Пока разбираешься что такое сокет, люди давным давно зарабатывают, просто склепав сайтик на cms.


      1. Wesha
        14.02.2022 22:14
        +4

        Пока разбираешься что такое сокет, люди давным давно зарабатывают, просто склепав сайтик на cms

        ... а потом их сайтик взломал Вася-хакер из 5-го "Б", потому что ему было не лень разобраться, что админский интерфейс, оказывается, можно закрыть паролем!


        1. NTDLL
          14.02.2022 23:58

          или что админский интерфейс могли бы вообще не отдавать на запрос


          1. Wesha
            15.02.2022 01:54

            Они до этой строчки в документации не дочитали — некогда было.


        1. spacediver
          15.02.2022 13:21
          +4

          Напротив, мейнстримная CMS с настройками по умолчанию может быть понадёжнее самописного новичком сервера на сокетах?..


          1. dakuan
            16.02.2022 12:29

            Тут палка о двух концах. Для таргетированной атаки - да. Но если в мейнстримной CMS найдут уязвимость, то в зоне риска окажутся даже те, кого вряд ли кто-то стал бы взламывать целеноправленно.


    1. myxo
      15.02.2022 03:03
      +1

      На самом деле умение изучать систему как черный ящик довольно полезное. Не у всего есть хорошая, актуальная документация, как у api сокетов.


      1. Wesha
        15.02.2022 03:10

        Нет, умение изучать систему как чёрный ящик довольно полезное, тут я не спорю. Но умение RTFM тоже не самое последнее по пользе, и обычно приводит к искомому результату несколько быстрее.


        1. calculator212
          15.02.2022 10:14

          Правда это не такой увлекательный путь.


    1. lz961
      15.02.2022 18:22

      Думается, что на каком-то уровне (для каждого специалиста на своём) "капот" всё-же остаётся всегда закрытым.


    1. amarao
      16.02.2022 21:06

      Будем реалистами, любая сложная система сейчас изучается таким образом. Даже если вы знаете RFC, которое описывает поведение системы, даже если вы уверены в вашей модели работы системы, система написана людьми со своим представлением RFC, и может оказаться, что их интерпретация RFC отличается от вашей. Это если есть RFC.

      К большинству же современного софта никакие RFC не предлагаются.


      1. Wesha
        16.02.2022 22:57

        1. amarao
          17.02.2022 15:16

          А я вот не знаю. uucp учитывать или нет?


  1. somurzakov
    14.02.2022 21:25
    +1

    почему статьи с хакер ньюс оказываются здесь? я их на хакер ньюс читал в оригинале ))


    1. mammuthus
      15.02.2022 00:05
      +2

      Вы выпили красную таблетку и ее действие началось


  1. khch
    14.02.2022 23:14
    +1

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


    1. cadovvl
      15.02.2022 10:57
      +10

      Просто программист - понятие обширное. Под разные задачи и специализации.

      Кому-то сокеты программировать, кому-то вычисления на intrinsic писать, кому-то контекстно-зависимую грамматику унарного минуса описывать, кому-то настраивать эффективные индексы в базе данных, кому-то json-ы лопатой ворочать, а кому-то цвета кнопок туда-сюда менять.

      И каждый из этих людей может "скептически" смотреть на "крутых программистов" из других областей.

      А про "дохренадолларов": с больным зубом, я, скорее, заплачу среднему стоматологу, чем гениальному проктологу. Потому что решению моих проблем набор навыков среднего стоматолога отвечает несколько больше...