TCP/IP: моделі OSI та TCP/IP, TCP-пакети, Linux sockets і порти

Автор |  29/06/2025
 

Складно в одному пості описати те, про що написані тисячі книжок на тисячу сторінок, але сьогодні спробуємо швиденько розглянути основи того, як відбувається комунікація між хостами в мережі.

Спочатку згадаємо про моделі OSI та TCP/IP, потім про структуру пакетів, встановлення підключень, і в кінці – заглянемо “під капот” Linux – подивимось на сокети і Linux TCP stack.

В основному увага буде саме TCP, бо це те, з чим ми найчастіше маємо справу.

What is: The TCP/IP

Отже, TCP/IP включає в себе два основних поняття:

  • по-перше, це стек протоколів (стандартів, наборів правил)  комунікації – TCP (Transmisstion Control Protocol) Та IP (Internet Protocol): вони описують те, яким чином встановлюються з’єднання між хостами та сервісами в Інтернеті. Ці протоколи стандартизовані і описані у відповідних RFC (TCP – RFC: 793, IP – RFC: 791)
  • крім того, TCP/IP – це модель комунікації, яка включає в себе кілька рівнів – подібно до моделі OSI (і який теж описаний в Informational RFC – RFC 1180)

Загальна модель OSI

Модель OSI (Open Systems Interconnection model), розроблена та стандартизована ISO (International Organization for Standardization) у 1984 році – див. ISO/IEC 35.100.

Колись писав про це у схожому пості – What is: модель OSI, але то було давно, і без детального опису TCP/IP.

Отже, основна ідея моделі: будь-яке з’єднання в мережі проходить через кілька рівнів комунікації, де кожен рівень відповідає лише за свою задачу і не має доступу до логіки роботи інших рівнів – це називається Layer isolation.

При передачі даних між рівнями використовується принцип Encapsulation – кожен рівень “загортає” пакет даних у власну “обгортку” – додає власні заголовки до пакету, не змінюючи вміст попереднього рівня.

Якщо дуже спрощено, то процес можна відобразити так:

  • коли наш браузер формує якийсь HTTP-запит – це відбувається на самому верхньому рівні, Application layer
  • цей запит передається нижче, до Transport layer, де дані від браузера інкапсулюються (encapsulation): до даних з Application layer додаються заголовки з Transport layer – TCP headers
  • потім, ще нижче – на Network layer додається IP-заголовок, що вказує адресу відправника й одержувача
  • і нарешті, на Link layer формується Ethernet frame, який і передається через Physical layer

На приймаючій стороні (наприклад, EC2 інстанс з NGINX) все відбувається у зворотньому напрямку – decapsulation: кожен рівень знімає свою обгортку й передає payload вищому рівню.

Непогана діаграма є в OSI Model Explained (хоча тут HTTP header відображено навпроти Session layer – але це відбувається на Application layer):

PDU (Protocol Data Unit)

Крім того, коли ми говоримо “пакет” маючи на увазі дані – то технічно на кожному рівні моделі OSI ці дані називаються по-різному, а загальна назва для них – PDU (Protocol Data Unit):

Protocols layer classification based on OSI Reference Model

Note: хоча на цій схемі теж є неточності, наприклад SQL – це мова запитів на Application layer

Тобто:

  • Application, Presentation, Session рівні – це просто Data
  • Transport layer (рівень 4) – це Segment (в TCP) або Datagram (UDP)
  • Network layer – Packet (наприклад, IP-пакет, в якому може бути TCP-сегмент)
  • Data Link layer – оперує Frames
  • Physical layer – це вже біти, 0 та 1

Процес передачі даних від браузера до web-серверу з TLS

Давайте детальніше глянемо на те, як відбувається процес передачі даних:

  • Layer 7 – Application layer:
    • браузеру треба відправити дані – payload
    • він оперує на Application layer, і формується HTTP-запит з HTTP headers (аутентифікація, кешування, сам запит до потрібного ресурсу – URI, та куди запит йде – URL)
    • тут встановлюється HTTP-сесія – через cookies, JWT-токен, параметри URL тощо
    • сформовані дані передаються до Presentation Layer
  • Layer 6 – Presentation Layer:
    • якщо використовується шифрування, то тут підключаться бібліотеки SSL/TLS і шифрують дані
    • додаються TLS headers
    • при потребі виконується перетворення даних, наприклад кодування символів (ASCII, UTF-8)
  • Layer 5 – Session Layer:
    • тут відбувається процес TLS handshake – встановлення методів і ключів шифрування (класний матеріал на цю тему – The Illustrated TLS 1.2 Connection, і колись писав я – What is: SSL/TLS в деталях)
    • створюється TLS-сесія між клієнтом та сервером
  • Layer 4 – Transport Layer:
    • тут формується TCP-сегмент (або datagram для UDP, втім тут ми розглядаємо браузер та HTTP, тому TCP): до даних від вищих рівнів (наприклад, браузера) додаються TCP headers – Source port, Destination port, порядковий Sequence number – номер пакету (див Multiplexing and Demultiplexing in Transport Layer)
    • задаються TCP flags – ACK, etc
    • розмір TCP-сегменту обмежується MSS (Maximum Segment Size) – будемо дивитись далі
  • Layer 3 – Network Layer:
    • тут до TCP-сегменту додаються IP headers – звідки пакет відправлений (source IP), куди він йде (destination IP) і формується IP пакет
    • додаються дані про TTL (Time To Live) пакту, checksum пакету для перевірки отримувачем – чи пакет дійшов неушкодженим
    • розмір IP-адреси – 32 біти в IPv4, та 128 біт в IPv6
    • від IP-адреси залежить тип передачі даних – unicast (одному адресату), multicast (кільком адресатам), там broadcast – всім хостам в заданій мережі
    • на цьому ж рівні працює ICMP для обміну інформацією стан мережі і про помилки
      • на Network layer ICMP packets можуть створюватися автоматично при проблемах на рівні маршрутизації
      • але можуть формуватись і на Application layer, наприклад утилітами ping або traceroute
  • Layer 2 – Data Link Layer:
    • Ethernet, Wi-Fi – драйвери мережевої карти формують frame, додаючи MAC-адресу відправника і отримувача і власну перевірку цілісності пакету – CRC (cyclic redundancy check)
    • MAC адреси визначаються за допомогою протоколу ARP (Adress Resolution Protocol)
  • Layer 1 – Physical Layer:
    • фізичне з’єднання, електричні або оптичні сигнали

DNS в моделі OSI

Окремо давайте розглянемо питання про DNS:

  • Layer 7 – Application layer:
    • браузеру потрібно відправити запит на “google.com”
    • браузер виконує функцію getaddrinfo() або gethostbyname() (застаріла) з бібліотеки glibc
      • glibc перевіряє параметри /etc/nsswitch.conf:
        • при необхідності виконати зовнішній DNS-запит (наприклад, якщо нема запису в /etc/hosts) – перевіряє параметри в /etc/resolv/conf
        • формується DNS-запит, відкривається UPD- або TCP-сокет
  • Layer 4 – Transport Layer:
    • на транспортному рівні до PDU (сегменту TCP або датаграми UDP)  додається заголовок з destination port 53 (зазвичай UDP, але може бути TCP – якщо відповідь більша за 512 байт або включено режим DNS-over-TCP, DoT – див. RFC 7766)
  • Layer 3 – Network Layer:
    • до пакету додаються IP headers – куди саме відправити запит
  • Layer 2 – Data Link Layer та Layer 1 – Physical Layer: передача даних

Модель OSI ISO vs модель TCP/IP

Модель TCP/IP (або Internet Protocol Suite) була розроблена у 1970-х, ще до появи OSI і лягла  в основу Інтернету (і його попередника – ARPANET).

Моделі OSI ISO та TCP/IP створені для уніфікації зв’язку між пристроями, але мають ключові відмінності:

  • OSI описує 7 рівнів, тоді як TCP/IP – 4
    • Application Layer в TCP/IP включає в себе Application, Presentation та Session layers моделі OSI
    • а Network Access Layer в TCP/IP включає в себе Data Link та Physical layers моделі OSI (іноді називають Link Layer)
  • модель OSI це більш “академічна модель”, яка використовується для пояснення принципів роботи мережі, а TCP/IP – “прикладна модель”, на якій власне побудована комунікація в Інтернеті

Головна різниця в тому, що:

  • модель TCP/IP створювалась для опису вже існуючих протоколів (TCP, IP, FTP, SMTP тощо), які використовувались в ARPANET – тобто спочатку технологія, а потім її опис у вигляді моделі.
  • натомість модель OSI – це більше теоретична моделі, яку спочатку описали (“як це має бути”), і потім вже під цю модель почали додавати нові протоколи.

Непогана ілюстрація в рівнях моделей є в A Refresher Course on OSI & TCP/IP:

TCP headers та payload

Окей – з загальною схему передачі даних розібрались, давайте подивимось детальніше на те, що і як саме передається в TCP/IP.

Оскільки ми вже згадували про TCP headers – то давайте почнемо з них.

TCP header має однакову структуру незалежно від того, чи він передається всередині IPv4 або IPv6. Його мінімальний розмір 20 байт, а максимальний, за рахунок використання поля Options – 60 байт.

IPv4 headers має змінну довжину – від 20 до 60 байт, а от заголовки IPv6 фіксовані в 40 байт.

MTU, MSS та TCP Payload

Максимальний розмір даних, які можна передати в одному IP-пакеті та TCP-сегменті, і залежить від розміру Ethernet frame, MTU (Maximum Transmission Unit), який по дефолту заданий в 1500 байт:

$ ifconfig wlan0 | grep -i MTU
wlan0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500

Від цих 1500 байт віднімається місце під TCP та IP заголовки, і в результаті це дає нам MSS (Maximum Segment Size) – максимальний розмір корисних даних у TCP-сегменті:

MSS = (MTU) - (IP header) - (TCP header)
    = 1500 - 20 - 20
    = 1460 байт

MSS оголошується під час TCP-handshake через TCP option MSS в пакеті SYN, і обидві сторони узгоджують значення, що дозволяє відправнику враховувати цей розмір, аби уникнути IP-фрагментації.

Якщо TCP payload перевищує MSS – то стек TCP виконує сегментацію, тобто розбиває цей потік на кілька окремих TCP-сегментів.

Наприклад, браузер відправляє POST-запит розміром 3000 байт – тоді TCP поділить цей запит на:

  • сегмент 1 з розміром даних 1460 байт
  • сегмент 2 з розміром даних 1460 байт
  • сегмент 3 з 80 байтами

У випадку з TCP-сегментації – кожен пакет матиме власні заголовки, а його Sequence Number буде вказувати на позицію першого байта даних цього сегмента в загальному потоці даних. При цьому розмір payload кожного сегмента не перевищуватиме MSS.

IP-фрагментація – виняткова ситуація, який трапляється тільки якщо TCP-сегмент (або інший IP-пакет) вже перевищив MTU, і в ідеалі не має відбуватись.

Структура TCP headers

Отже, після отримання даних від Application layer формується TCP-сегмент, до якого додається набір TCP headers:

Note:  далі все ж буду використовувати слово “флаги”, а не “прапорці”, бо в контексті TCP якось більш коректно звучить

Тут:

  • Source port: поле 16 біт в якому вказується порт відправника
  • Destination port: поле 16 біт в якому вказується порт призначення
  • Sequence number: поле 32 біти, яке вказує на перший байт даних (payload) кожного TCP-сегменту
  • Acknowledgment number: поле 32 біти, яке передається отримувачем для запиту наступного TCP-сегменту – це буде Sequence Number + 1
  • DO (data offset): поле 4 біти яке вказує де закінчується TCP header і починаються дані (payload)
  • RSV (reserved field): 3 биті, не використовується, і завжди пусте
  • Flags: 9 біт, також називаються “control bits” – використовуються для передачі флагів, які контролюють встановлення підключення, передачу даних та закриття підключення
    • URG: urgent pointer – якщо флаг заданий, то сегмент має термінові дані, які на стороні операційної системи отримувача можуть бути передані окремим системним викликом минуючи загальний TCP-буфер (див. TCP – Urgent pointer field)
      • не впливає на маршрутизацію чи доставку пакета мережею і використовується тільки локально стеком ядра операційної системи отримувача
    • ACK: acknowledgment підтвердження отримання сегменту
    • PSH: push function – передати дані негайно, не очікуючи наповнення TCP-буферу
    • RST: reset – примусове закриття з’єднання при помилках
    • SYN: початок з’єднання (в TCP 3-way handshake, далі подивимось), задає Initial Sequence Number – див. трохи ниже
    • FIN: нормальне закриття з’єднання, відправляється як клієнтом, так і сервером
  • Window: 16 біт, вказується максимальна кількість байт, які відправник (і клієнт, і сервер) може відправити без очікування наступного ACK від серверу (контроль TCP-буферу ядра на стороні серверу)
  • Checksum: 16 біт, контрольна сума TCP-заголовка + даних, використовується для перевірки, чи не пошкоджено сегмент під час передачі
  • Urgent pointer: 16 біт, якщо заданий URG, то тут вказується де завершуються термінові дані
  • Options: 0 – 320 біт, використовується для передачі MSS, timestamps тощо

Sequence Number

Доволі цікава тема, яку коротко, мабуть, є сенс проговорити окремо.

TCP – це потоковий протокол, який передає послідовність байт, а не окремі повідомлення.

Під час передачі даних в TCP-сесії:

  • клієнт відправляє SYN з Initial Sequence Number, який спочатку задається у вигляді рандомного числа, наприклад 100000
  • сервер відповідає SYN-ACK, підтверджуючи отримання запиту на з’єднання (SYN)
    • в полі Acknowledgment Number вказує 100001
  • далі, при початку передачі першого сегменту даних – клієнт в першому сегменті вкаже Sequence Number 100001, а в наступному – 101461 (при MSS 1460 байт)

Тобто, кожен наступний сегмент збільшує Sequence Number на довжину payload.

Перегляд TCP headers в Wireshark

Самий простий спосіб – з wireshark (або wireshark-qt).

Запускаємо від root, вибираємо інтерфейс:

Наприклад, подивитись трафік до RTFM – знаходимо IP:

$ dig rtfm.co.ua +short
104.26.3.188
104.26.2.188
172.67.68.115

Задаємо фільтр:

ip.addr==104.26.3.188 || ip.addr==104.26.2.188 || ip.addr==172.67.68.115

І отримуємо дані:

TCP connection

Трохи розібрались з TCP headers – тепер давайте глянемо на те, як встановлюється TCP-з’єднання.

Отже, TCP – це протокол, орієнтований на з’єднання (тобто потребує встановлення сесії до початку передачі даних), який забезпечує доставку даних, контроль потоку та виявлення помилок при передачі.

На відміну від UDP – з TCP ми або гарантовано передаємо дані, або буде виявлена помилка і з’єднання буде розірване.

TCP handshake

Як і в TLS, встановлення TCP-з’єднання відбувається за стандартним процесом – “3-way handshake“.

По TLS – див. What is: SSL/TLS в деталях.

TCP handshake складається з трьох етапів (власне, тому і назва “3-way handshake”):

  1. SYN: клієнт відправляє пакет з флагом SYN, вказуючи свій Initial Sequence Number
  2. SYN-ACK: сервер відповідає пакетом з флагами SYN та ACK, цим він:
    1. підтверджує отримання SYN від клієнта (встановлюючи поле Acknowledgment Number)
    2. відправляє свій власний Initial Sequence Number
  3. ACK: клієнт відправляє ACK, чим підтверджує отримання SYN-ACK від серверу

На цьому сесія вважається встановленою, і починається передача даних.

Закриття сесії – “4-way FIN handshake“:

  1. FIN: клієнт повідомляє сервер (або навпаки), що закінчив передачу, і готовий до закриття сесії
  2. ACK від серверу: сервер підтверджує отримання FIN
  3. FIN від серверу: сервер повідомляє, що теж готовий закрити сесію
  4. ACK від клієнта: клієнт відповідає фінальним ACK, підтверджуючи отримання FIN від сервера

Після цього з’єднання повністю закривається.

Аналіз сесії з Wireshark

Можна писати відразу в Wireshark, можемо спочатку створити файл, а потім його вже аналізувати.

Запускаємо tcpdump:

$ sudo tcpdump host 104.26.3.188 or host 104.26.2.188 or host 172.67.68.115 -w tcp.pcap

В іншому вікні виконуємо запит до RTFM:

$ curl https://rtfm.co.ua

Відкриваємо сформований дамп у Wireshark:

$ sudo wireshark tcp.pcap

І отримуємо всі пакети, які були передані:

Власне тут ми першими і бачимо 3-way handshake – початок з’єднання:

  • SYN (клієнт 50556 => сервер 443):
    • Seq=0, Len=0
    • клієнт (192.168.0.116) відкриває з’єднання маючи локальний порт 50556 до серверу RTFM на до 172.67.68.115 і порт 443
  • SYN, ACK (443 → 50556):
    • Seq=0 Ack=1, Len=0
    • відповідь від серверу 172.67.68.115 , що він підтвердив отримання SYN від клієнту 192.168.0.116 (флаг `ACK), і задає свій флаг SYN` з Initial Sequence Number, встановлюючи початкову точку для свого потоку даних
  • ACK (50556 → 443):
    • Seq=1 Ack=1, Len=0
    • Клієнт підтверджує SYN від сервера

Четвертим вже починається передача даних – Len-1388 (насправді, це вже початок TLS handshake – наступним, п’ятим пакетом бачимо TLSv1.3).

Seq=0 – це як раз Sequence Number, про який говорили вище.

Просто Wireshark його відображає в зручній для нас формі, але ми можемо побачити його реальне значення:

Len=0 в перших трьох пакетах нуль, бо це тільки встановлення з’єднання, ще до передачі даних, і пакети містять тільки TCP-заголовки для встановлення з’єднання, без даних.

Ack=N – підтвердження отримання пакету.

Тобто:

  1. Seq=0:
    • клієнт відправляє Inital Sequence Number, який Wireshark нам відображає як 0
  2. Seq=0 Ack=1 Len=0:
    • Seq=0 – сервер теж задає свій  Inital Sequence Number
    • Ack=1 – сервер інкрементить Seq від клієнта на +1
  3. Seq=1 Ack=1 Len=0:
    • Seq=1 – тепер клієнт збільшує свій Sequence Number
    • Ack=1 – клієнт підтверджує отримання SYN від серверу

TCP та ядро Linux

В ядрі операційної системи для передачі даних за TCP-протоколом реалізована своя система – TCP stack.

Вона відповідає за:

  • відкриття та завершення TCP-сесій
  • контроль доставки (ACK, SEQ)
  • повторну передачу втрачених пакетів
  • розпізнавання флагів (SYN, FIN, RST тощо)
  • збирання даних з кількох сегментів у правильному порядку

По факту, це набір функцій в ядрі, які опрацьовують TCP-пакети.

А сам TCP-стек – це частина мережевого стеку ядра, разом з обробкою Ethernet, ARP, IP, UDP та інших.

Див. документацію на  kernel_flow.

Буфери ядра – це основне, з чим можна зіткнутись при налаштуванні чи тюнингу ядра:

  • rmem_*: receive buffer (для вхідного трафіку)
  • wmem_*: write buffer (для вихідного трафіку)

Дефолтні значення задаються в /proc/sys/net/ipv4/tcp_rmem та /proc/sys/net/ipv4/tcp_wmem відповідно.

А перевизначити їх можна з sysctl:

$ sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
$ sudo sysctl -w net.ipv4.tcp_wmem="4096 65536 6291456"

Ядро також підтримує автоматичне масштабування буферів – файл /proc/sys/net/ipv4/tcp_moderate_rcvbuf:

$ cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf
1

1 – включено, 0 – виключено.

Мінімальний Maximum segment size (MSS) задається у файлі /proc/sys/net/ipv4/tcp_min_snd_mss:

$ cat /proc/sys/net/ipv4/tcp_min_snd_mss
48

48 байт == 384 біти.

Мінімальний розмір задається для запобіганню відправки надто маленьких TCP-сегментів, які викликатимуть зайві накладні витрати та зниження продуктивності.

Процес отримання TCP-пакету ядром

Як в системі виглядає процес отримання даних?

Див. kernel_flow.

Якщо спрощено, то:

  1. Layer 2: Link layer
    1. Network Interface Card отримує Ethernet frame з TCP-пакетом
    2. ядро системи викликає драйвер карти, а драйвер викликає функцію netif_receive_skb() і передає весь отриманий кадр (в структурі skbSocket Buffer) на обробку мережевій підсистемі ядра
  2. Layer 3: Network layer
    1. пакет передається до ip_rcv() (IPv4) або ipv6_rcv() (IPv6), де перевіряється заголовок IP для визначення протоколу
    2. якщо Protocol = 6 (TCP) – пакет передається в tcp_v4_rcv()
  3. Layer 4: Transport layer
    1. функція tcp_v4_rcv() перевіряє контрольну суму, знаходить відповідний локальний сокет (порт), обробляє SEQ/ACK/FIN/RST/SYN флаги, і додає payload у receive buffer сокета, прив’язаного до відповідного порту (наприклад, listen(80) для веб-сервера)
    2. після передачі даних до веб-серверу – ядро формує ACK-пакет у відповідь
    3. дані передаються у внутрішній receive buffer сокета, і звідти вже передаються в userspace до веб-серверу (якщо ми про браузер-сервер)

Якщо заглиблюватись – то можна взяти утиліти по типу Systemtap для відстеження системних викликів.

Сокети та TCP-порти в Linux

Для роботи з TCP в Linux є концепція сокетів (sockets) – це такі собі ендпоінти, які прив’язуються до пари IP:PORT.

Непоганий текст з діаграмами – TCP handling in Linux.

Власне сокет – це абстракція, яка дозволяє програмам читати/писати в мережу, як через звичайний файл, і по суті і є файловим дескриптором спеціального типу: операційна система сприймає їх як пайп (pipe), через який можна передавати дані.

Сокети можуть бути або локальними, аби мережевими:

  • AF_INET для IPv4 та AF_INET6 для IPv6
  • AF_UNIX або AF_LOCAL – для локальної роботи

AF_* в імені – це “Address Family“, бо маємо не тільки TCP/UDP-сокети, але й AF_UNIX – локальні, AF_BLUETOOTH – Bluetooth, AF_NETLINK – Netlink і т.д.

При створенні сокету задається його тип:

socket(AF_INET, SOCK_STREAM, 0);  // TCP
socket(AF_INET, SOCK_DGRAM, 0);   // UDP

А потім з функцією bind() виконується прив’язка до IP та порту.

C programming: UNIX Socket

Можна звісно робити на якомусь Python, але програмування на C покаже нам більше деталей.

Приклад роботи сокетів писав в C: сокеты и пример модели client-server (2017 рік, боже…).

Простий приклад локального сокету:

// Create a UNIX domain socket file at /tmp/mysocket.sock

#include <sys/socket.h>   // import socket(), bind() functions
#include <sys/un.h>       // import C struct sockaddr_un
#include <unistd.h>       // import close() function
#include <stdio.h>        // import input/output functions like print()
#include <string.h>       // import strings/memory functions like strlen()

// def main C function
int main() {

   // define a variable with the 'int' type
   // it will store the socket's file descriptor ID returned by the socket() function
   // a file descriptor is just an integer index into the per-process open file table
   // the actual 'file' struct exists in kernel space, user space only sees the integer
    int sockfd;

    // define a variable named 'addr' with the 'struct sockaddr_un' type
    // this structure is used to specify socket address for AF_UNIX sockets
    struct sockaddr_un addr;

    // Step 1: create socket 
    // socket(domain, type, protocol)
    // AF_UNIX: UNIX domain socket
    // SOCK_STREAM: stream-oriented (like TCP)
    // '0': protocal, set to 0 as AF_UNIX + SOCK_STREAM have no protocal
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    // Step 2: set up address structure
    memset(&addr, 0, sizeof(addr));              // zero out the memory for safety
    addr.sun_family = AF_UNIX;                   // set socket family (UNIX domain)
    strcpy(addr.sun_path, "/tmp/mysocket.sock"); // set path for the socket file in the 'addr' structure

    // Step 3: remove old socket file if it exists
    // unlink removes a file; important to avoid "Address already in use" error
    unlink("/tmp/mysocket.sock");

    // Step 4: bind
    // 
    // bind the socket to a local address (path in the filesystem) - bind(sockfd, addr, size_of_addr)
    // 'sockfd': socket file descriptor returned by socket()
    // '&addr': pointer to the sockaddr_un struct that contains:
    // - family (AF_UNIX)
    // - path (filesystem path to the socket file)
    // '(struct sockaddr*)': cast required because bind() expects a generic sockaddr*
    // 'sizeof(addr)': size of the sockaddr_un structure
    //
    // after this call, the socket is associated with a specific name (path),
    // so other processes can connect to it via this path.
    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        return 1;
    }

    printf("UNIX socket created at /tmp/mysocket.sock\n");

    // Step 5: keep socket alive for inspection
    sleep(60);

    // Step 6: cleanup
    close(sockfd);                 // close the socket file descriptor
    unlink("/tmp/mysocket.sock");  // remove the socket file from filesystem
    return 0;
}

Збираємо з gcc:

$ gcc unix_socket.c -o unix_socket

Запускаємо:

$ ./unix_socket 
UNIX socket created at /tmp/mysocket.sock

І маємо відкритий сокет:

$ file /tmp/mysocket.sock
/tmp/mysocket.sock: socket

$ ls -l /tmp/mysocket.sock
srwxr-xr-x 1 setevoy setevoy 0 Jun 29 10:30 /tmp/mysocket.sock

В ls -l бачимо флаг “s” на початку – показує, що це тип socket.

Власне саме так і створюються сокети в Linux, які ми можемо бачити для якихось локальних демонів, наприклад:

$ sudo find / -type s 2>/dev/null
...
/run/docker.sock
/run/dbus/system_bus_socket
...
/run/dhcpcd/sock
...

C programming: AF_INET Socket

AF_INET та AF_INET6 створюються і працюють аналогічно до локальних UNIX-сокетів – тільки замість “адреси” у вигляді імені локального файлу використовують пару IP:PORT.

Також їх називають “BSD sockets” або “Berkeley sockets”, бо вперше вони були реалізовані Berkeley Software Distribution (BSD) Unix у 1983 році.

Код в принципі схожий на створення UNIX-сокету:

#include <stdio.h>              // for printf(), perror()
#include <stdlib.h>             // for exit()
#include <string.h>             // for memset()
#include <unistd.h>             // for close()
#include <sys/types.h>          // for socket types
#include <sys/socket.h>         // for socket(), bind()
#include <netinet/in.h>         // for sockaddr_in
#include <arpa/inet.h>          // for inet_addr()

int main() {
    // will store the socket's file descriptor (int)
    int sockfd;  

    // Step 1: create a new socket
    // AF_INET    = IPv4 address family
    // SOCK_STREAM = TCP (reliable byte stream)
    // 0          = default protocol (IPPROTO_TCP for AF_INET)
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // define the address to bind the socket to
    struct sockaddr_in server_addr;

    // Step 2: fill the 'server_addr' structure with zeros to avoid undefined or leftover data
    memset(&server_addr, 0, sizeof(server_addr));

    // AF_INET for IPv4 
    server_addr.sin_family = AF_INET;
    // port number
    server_addr.sin_port = htons(8080);
    // bind to th 'localhost' (127.0.0.1)
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // Step 3: bind the socket to the given IP address and port
    // this makes the socket listen for incoming connections on the '127.0.0.1:8080'
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);  // close socket before exiting
        exit(EXIT_FAILURE);
    }

    // Step 4: listen for incoming connections
    // the socket is now ready to accept connections
    // function: int listen(int sockfd, int backlog);
    // 'sockfd' is the socket file descriptor
    // 'backlog' is the maximum number of pending connections
    // if the backlog is exceeded, new connections will be refused
    // here we set it to 5, meaning up to 5 connections can be queued
    if (listen(sockfd, 5) < 0) {
        perror("listen");
        return 1;
    }

    printf("Socket successfully created and bound to 127.0.0.1:8080\n");
    printf("Press Enter to close the socket...\n");
    
    // keep the socket open for inspection
    getchar();

    // Step 5: close the socket after use
    close(sockfd);
    return 0;
}

Але тут:

  • задаємо тип AF_INET
  • задаємо TCP-порт
  • задаємо IP, на якому слухати
  • з системним викликом listen() переводимо сокет в стан очікування з’єднань

Збираємо:

$ gcc inet_socket.c -o inet_socket

Запускаємо:

 $ ./inet_socket 
Socket successfully created and bound to 127.0.0.1:8080

Перевіряємо:

$ netstat -anp | grep 8080
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      2724448/./inet_sock 

TCP ports

TCP-порт – це просто число від 0 до 65535, які дозволяють мати різні сервіси і підключення при одному IP-адресі, і використовується виключно для адресації сокету по парі IP:PORT.

Діапазон портів поділений на три частини:

  • 0 – 1023: well-known ports (SSH: 22, HTTP: 80…)
  • 1024 – 49151: Registered ports (можуть використовуватись додатками)
  • 49152 – 65535: Ephemeral ports (системою для клієнтів)

Тобто з’єднання формується з пари <client_IP>:<client_port> => <server_IP>:<server_port>, і пара IP:PORT задається при створенні сокету (через виклик bind()).

Коли ядро отримує TCP-пакет, то викликає tcp_v4_rcv(), яка в свою чергу по IP-адресі:порт шукає відповідний сокет (через виклик inet_lookup() або __inet_lookup_established()), і якщо сокет знайдено – то через нього передається payload пакету.

Якщо сокет не знайдено, або сервіс повернув помилку – то ядро може повернути RST у відповідь, або просто дропнути пакет (залежно від ситуації).

Корисні посилання