Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке тризначний рукопашний?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Three-way handshake** (триетапне рукостискання) - це процес встановлення TCP-з'єднання через три пакети: SYN, SYN-ACK і ACK. ``` Client -> Server: SYN (seq=x) Server -> Client: SYN-ACK (seq=y, ack=x+1) Client -> Server: ACK (ack=y+1) ``` **Головне:** обидві сторони узгоджують sequence numbers до початку передачі даних. Кожне TCP-з'єднання проходить через це автоматично на рівні ядра ОС.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Three-way handshake** (триетапне рукостискання) - це процес встановлення TCP-з'єднання, під час якого клієнт і сервер обмінюються трьома пакетами (SYN, SYN-ACK, ACK), щоб підтвердити готовність обох сторін і узгодити sequence numbers перед передачею даних. ## Теорія ### TL;DR - Як перевірка телефонної лінії: "чути?" (SYN), "чую, говоріть" (SYN-ACK), "прийнято, починаємо" (ACK). Обидві сторони переконуються що канал працює перед тим як щось передавати. - Відбувається автоматично для кожного TCP-з'єднання. HTTP, HTTPS, SSH, підключення до БД, WebSockets - все починається з цього. - Головна мета: узгодити початкові sequence numbers, щоб TCP міг виявляти втрачені або перемішані пакети. - UDP пропускає рукостискання. Швидше, але без гарантії доставки. - ECONNREFUSED = сервер відкинув SYN (порт закритий). ETIMEDOUT = SYN-ACK не прийшов (сервер недоступний або заблокований firewall). ### Швидкий приклад ```javascript const net = require('net'); const socket = net.createConnection({ host: 'example.com', port: 80 }); socket.on('connect', () => { // Рукостискання завершено - SYN -> SYN-ACK -> ACK відбулись в ядрі ОС socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); }); socket.on('error', (err) => { // ECONNREFUSED: сервер живий, порт закритий // ETIMEDOUT: SYN нікуди не дійшов console.log('Connection failed:', err.code); }); ``` Подія `connect` спрацьовує лише після того, як усі три пакети пройшли. Код застосунку не бачить самого рукостискання - ним займається ядро ОС. ### Як це працює крок за кроком Крок 1 (SYN): клієнт обирає випадковий початковий sequence number (ISN) і надсилає SYN-пакет. По суті: "хочу підключитися, мій лічильник байтів починається з цього числа". Крок 2 (SYN-ACK): сервер обирає свій ISN і відповідає. Підтверджує ISN клієнта (додає 1) і одночасно повідомляє свій. Один пакет, два завдання. Крок 3 (ACK): клієнт підтверджує ISN сервера. Стан з'єднання на обох сторонах переходить до ESTABLISHED. Можна передавати дані. Саме тому потрібно три кроки, а не два. Два кроки підтвердили б, що сервер отримав SYN клієнта, але клієнт ніколи б не підтвердив, що отримав sequence number сервера. Третій крок закриває цю прогалину. ``` // Реальний вивід tcpdump під час рукостискання // Клієнт 192.168.1.100:54321 -> Сервер 192.168.1.1:80 // Крок 1: SYN 192.168.1.100.54321 > 192.168.1.1.80: Flags [S], seq 1000000000 // Крок 2: SYN-ACK 192.168.1.1.80 > 192.168.1.100.54321: Flags [S.], seq 2000000000, ack 1000000001 // Крок 3: ACK 192.168.1.100.54321 > 192.168.1.1.80: Flags [.], ack 2000000001 // Починається передача даних 192.168.1.100.54321 > 192.168.1.1.80: Flags [P.] "GET / HTTP/1.1" ``` ### Ключова різниця між TCP і UDP Рукостискання не стосується передачі даних. Воно встановлює спільний контекст: обидві сторони знають, де починається байтовий потік одна одної. Після цього TCP може виявляти втрачені пакети, дублікати і відновлювати порядок. UDP надсилає пакети і забуває про них. Саме тому відеодзвінки використовують UDP (пропущений кадр - не катастрофа), а передача файлів - TCP (кожен байт має прийти по порядку). ### Коли що використовувати - **TCP-з'єднання:** рукостискання відбувається автоматично. Ти не обираєш його. - **UDP (без рукостискання):** DNS, відеостримінг, онлайн-ігри. Там затримка важливіша за гарантію доставки. - **Діагностика зависань:** якщо з'єднання не встановлюється, перевір чи взагалі повертається SYN-ACK. Firewall, який мовчки відкидає SYN-пакети, виглядає так само, як недоступний сервер. - **Безпека:** SYN flood-атаки надсилають мільйони SYN-пакетів без завершення ACK. Сервер заповнює таблицю напіввідкритими з'єднаннями і вичерпує ресурси. ### Типові помилки **Помилка 1: запис даних до готовності з'єднання** ```javascript // Неправильно const socket = net.createConnection({ host: 'example.com', port: 80 }); socket.write('GET / HTTP/1.1\r\n'); // рукостискання ще не завершено // Правильно socket.on('connect', () => { socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); }); ``` **Помилка 2: таймаут надто короткий для повільних мереж** ```javascript // Неправильно: 100ms не вистачить для міжконтинентального з'єднання net.createConnection({ host: 'remote-server.com', port: 443, timeout: 100 }); // Правильно: рукостискання з віддаленим сервером може зайняти 200-500ms net.createConnection({ host: 'remote-server.com', port: 443, timeout: 5000 }); ``` **Помилка 3: ECONNREFUSED і ETIMEDOUT - різні проблеми** ECONNREFUSED означає, що сервер відповів RST: він працює, але порт закритий. ETIMEDOUT означає, що SYN взагалі не отримав відповіді: неправильний хост, firewall або мертвий сервер. Повторна спроба при ECONNREFUSED нічого не змінить. **Помилка 4: припускати, що sequence numbers передбачувані** Старі реалізації TCP використовували послідовні або часові ISN. Зловмисники могли їх вгадати і підробляти пакети. Сучасний TCP рандомізує ISN за RFC 6528. Не будуй логіку на основі передбачення sequence number іншої сторони. **Помилка 5: продакшен без захисту від SYN flood** ```bash # Linux: увімкни SYN cookies на рівні ОС sysctl net.ipv4.tcp_syncookies=1 # У nginx: обмеж кількість з'єднань з одного IP # limit_conn_zone $binary_remote_addr zone=addr:10m; # limit_conn addr 100; ``` SYN cookies дозволяють серверу відповідати на SYN-ACK без збереження стану для кожного напіввідкритого з'єднання. Якщо клієнт завершує рукостискання - стан відновлюється. Якщо ні - нічого не витрачено. Я бачив це неправильно налаштованим на внутрішніх сервісах: один навантажувальний тест зі staging виглядав для продакшен-сервера як SYN flood. ### Де зустрічається в реальних проектах - **Запити браузера:** кожен HTTP GET починається з SYN. Для HTTPS поверх TCP-рукостискання іде ще TLS handshake. - **SSH:** затримка 1-2 секунди при `ssh user@host` - це TCP-рукостискання плюс обмін ключами, а не повільна мережа. - **Підключення до БД:** MySQL, PostgreSQL, MongoDB проходять TCP handshake перед першим запитом. Connection pooling існує щоб не повторювати це на кожен запит. - **WebSockets:** HTTP upgrade до WebSocket відбувається після того, як TCP-з'єднання вже встановлено. - **Load balancers:** nginx і HAProxy стежать за невдалими рукостисканнями і позначають бекенди як недоступні. ### Питання на співбесіді **Q:** Чому три кроки, а не два? **A:** Два кроки підтвердили б, що сервер отримав SYN клієнта, але клієнт ніколи не підтвердив би, що отримав sequence number сервера. Без третього кроку сервер не може знати, чи доходять його пакети. **Q:** Що станеться, якщо фінальний ACK загубиться? **A:** Сервер залишається в SYN_RECV і повторно надсилає SYN-ACK з експоненційним відступом (загалом близько 60 секунд). Клієнт переходить в ESTABLISHED і намагається відправити дані. Сервер отримує дані в неочікуваному стані, надсилає RST, і клієнт отримує "Connection reset by peer". **Q:** Як пов'язані TIME_WAIT і рукостискання? **A:** Безпосередньо не пов'язані. TIME_WAIT виникає при закритті з'єднання, а не при відкритті. Після чотириетапного FIN-обміну сторона, що ініціювала закриття, чекає 2 хвилини (2 x MSL), щоб старі пакети від попереднього сеансу не пошкодили нове з'єднання на тому самому порту. Через це іноді не вдається одразу перезапустити сервер. **Q:** Чи можна надіслати дані всередині SYN-пакета? **A:** Так, з TCP Fast Open (TFO, RFC 7413). Сервер видає cookie при першому з'єднанні. Наступні SYN-пакети несуть цей cookie разом з даними, скорочуючи один round-trip. Chrome і ядро Linux 3.7+ підтримують TFO. **Q:** [Senior] Як виявити SYN flood і що з ним робити? **A:** Стеж за `netstat -an | grep SYN_RECV` - якщо число росте, це підозрілий знак. Увімкни SYN cookies на рівні ОС. Постав nginx або HAProxy з обмеженням швидкості підключень на IP. Для критичних сервісів використовуй DDoS-захист на рівні мережі. У Node.js `server.maxConnections` обмежує прийняті з'єднання, але flood зазвичай потрапляє в чергу ядра до того, як твій процес його побачить. ## Приклади ### Базовий: TCP-сервер на Node.js ```javascript const net = require('net'); const server = net.createServer((socket) => { // Callback викликається лише коли з'єднання досягло стану ESTABLISHED // SYN / SYN-ACK / ACK вже пройшли в ядрі ОС console.log('Client connected:', socket.remoteAddress); socket.write('Connected\n'); socket.on('data', (data) => { socket.write(`Echo: ${data}`); }); socket.on('error', () => socket.destroy()); }); server.listen(3000, () => console.log('Listening on port 3000')); ``` Ти ніколи не пишеш код для обробки окремих пакетів рукостискання. Ядро робить SYN / SYN-ACK / ACK і передає тобі вже готовий socket. ### Середній: діагностика помилок підключення ```javascript const net = require('net'); function connectWithDiagnosis(host, port) { const socket = net.createConnection({ host, port, timeout: 5000 }); socket.on('connect', () => { console.log('Handshake done, connection ready'); }); socket.on('timeout', () => { socket.destroy(); // SYN-ACK не отримано - перевір firewall або доступність хоста console.log('Timeout: no SYN-ACK received'); }); socket.on('error', (err) => { if (err.code === 'ECONNREFUSED') { // SYN дійшов до сервера, але порт закритий console.log('Port closed - check the port number'); } else if (err.code === 'EHOSTUNREACH') { // Немає маршруту - перевір IP або мережу console.log('No route to host'); } else { console.log('Unexpected error:', err.message); } }); } connectWithDiagnosis('example.com', 80); ``` Кожен код помилки вказує на різну фазу невдалого рукостискання. ECONNREFUSED означає, що SYN дійшов. ETIMEDOUT означає, що ні. Ці два факти ведуть в абсолютно різні боки при діагностиці. ### Просунутий: спостереження рукостискання через tcpdump ```bash # Захоплення пакетів рукостискання в реальному часі tcpdump -i any 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0' -n # Приклад виводу: # 10:01:01 client.54321 > server.80: Flags [S], seq 1000000000 # 10:01:01 server.80 > client.54321: Flags [S.], seq 2000000000, ack 1000000001 # 10:01:01 client.54321 > server.80: Flags [.], ack 2000000001 # 10:01:01 client.54321 > server.80: Flags [P.] "GET / HTTP/1.1..." ``` `[S]` = SYN. `[S.]` = SYN-ACK. `[.]` = чистий ACK. `[P.]` = дані (PSH+ACK). Кожне TCP-з'єднання твоєї машини дає рівно таку саму послідовність на початку.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.