Перейти до вмісту

NAT і DHCP

Ви підключаєте ноутбук до Wi-Fi, і за секунду в нього є адреса, шлюз і DNS-сервер. Ви нічого не вводили. Пристрій, якого ще хвилину тому не існувало в мережі, став її повноправним учасником. Звідки він узяв адресу, якщо для відправлення чогось у мережу вона потрібна, а без мережі адресу нікому спитати?

Або інше. У вашій квартирі десяток пристроїв, а провайдер видав одну адресу. Усі десять відкривають сайти одночасно, і відповіді приходять кожному на свій. Як маршрутизатор розбирає, яка відповідь чия, якщо зовні всі десять виглядають однаково?

Буває й так: ви запустили сервер на ноутбуці, а друг із іншого міста не може зайти, хоч усе працює. Або навпаки: у вас усе налаштовано, а для деяких користувачів дзвінок у відеочаті ніяк не з’єднується, хоча в кожного окремо інтернет чудовий.

Усе це теми одного модуля. DHCP видає адреси. NAT дозволяє багатьом пристроям ділити одну публічну адресу. Разом вони складають те, що на побутовому рівні називають «роутером».

Передумови. Адресація, приватні діапазони й широкомовлення (модуль 6), таблиця маршрутів і пересилання (модуль 7), ARP (модуль 5). Про порти досить уявлення з модуля 1; стани TCP, які тут згадуються, докладно розібрано в модулі 11. Про файрвол докладно в модулі 16, тут лише те, що потрібно для розмови про NAT.

Щоб надіслати пакет, потрібна адреса відправника. Щоб отримати адресу від сервера, потрібно з ним поговорити. DHCP (Dynamic Host Configuration Protocol) розв’язує це так: клієнт шле повідомлення, у якому адреса відправника нульова (0.0.0.0), а адреса призначення широкомовна (255.255.255.255). Кадр піде на ff:ff:ff:ff:ff:ff, усі пристрої сегмента його отримають, а DHCP-сервер, якщо він є, відповість. Працює це поверх UDP: клієнт слухає порт 68, сервер порт 67.

Обмін DHCP: Discover, Offer, Request, AckКлієнтадреси ще немаєDHCP-сервер192.168.1.11. Discoverє тут DHCP-сервер?0.0.0.0 → 255.255.255.2552. Offerвізьми 192.168.1.116 на 12 годин192.168.1.1 → клієнт3. Requestберу саме цю адресу від цього сервера0.0.0.0 → 255.255.255.2554. Ackпідтверджую; ось опції: маска, шлюз, DNS192.168.1.1 → клієнтRequest теж широкомовний: інші сервери мають дізнатися, що їхню пропозицію відхилено.
Перший і третій кроки клієнт шле широкомовно, бо адреси в нього ще немає. Другий і четвертий сервер адресує конкретному клієнтові. Схема відома як DORA за першими літерами повідомлень.
  1. Discover. Клієнт шукає сервер: «хто тут видає адреси?» Разом із цим він перелічує, які параметри хотів би отримати (маску, шлюз, DNS тощо).

  2. Offer. Сервер відповідає: «можу дати 192.168.1.116 на 12 годин, ось решта параметрів». Якщо в сегменті кілька DHCP-серверів, клієнт може отримати кілька пропозицій.

  3. Request. Клієнт обирає одну пропозицію й повідомляє про це, вказуючи адресу сервера. Це повідомлення теж широкомовне, щоб інші сервери дізналися про відмову і звільнили зарезервовані для нього адреси.

  4. Acknowledgement. Сервер підтверджує. Лише тепер клієнт має право налаштувати адресу на інтерфейсі. Якщо сервер передумав (наприклад, адресу вже віддали комусь іншому), він надсилає відмову (NAK), і клієнт починає спочатку.

DHCP видає адресу в оренду (lease) на певний термін, інакше адреси кінчалися б на тому, що колись було підключено й забуто. Щоб зберегти адресу, клієнт мусить оренду продовжувати.

У пакеті Ack разом із терміном оренди приходять два таймери. Після T1, за замовчуванням половини терміну, клієнт намагається продовжити оренду, звертаючись до свого сервера напряму. Якщо сервер мовчить, то після T2, за замовчуванням 87,5% терміну, клієнт шле запит широкомовно, сподіваючись, що відповість будь-який сервер. Якщо оренда спливла, а відповіді немає, клієнт зобов’язаний покинути адресу й почати Discover заново.

Якщо DHCP-сервер упав на годину, зв’язок одразу не зникне. Пристрої продовжують користуватися тим, що мають, і проблеми почнуться лише для нових пристроїв і для тих, чия оренда закінчиться в цей час.

Адреса найменша частина відповіді. Решта параметрів зветься опціями (options), і кожна має номер. Ось ті, що трапляються постійно:

Опція Номер Що задає
Маска підмережі 1 розмір мережі
Маршрутизатор 3 шлюз за замовчуванням
DNS-сервери 6 куди слати DNS-запити
Доменне ім’я 15 суфікс для коротких імен
Час оренди 51 на скільки видана адреса
Тип повідомлення 53 Discover, Offer тощо
Ідентифікатор сервера 54 хто видав
Безкласові статичні маршрути 121 додаткові маршрути клієнта

Опції роблять DHCP чимось більшим за видавача адрес. Через нього можна розіслати маршрути (121), дати мережевому завантажувачеві ім’я файлу (опції для PXE), вказати сервер часу. Фактично DHCP-сервер є «сервером налаштувань» для клієнтів.

Relay: коли сервер не в тому сегменті

Section titled “Relay: коли сервер не в тому сегменті”

Широкомовний Discover не проходить маршрутизатор, тож сервер у сусідній підмережі його не отримає. Тримати по DHCP-серверу в кожному сегменті незручно. Для цього є relay agent: маршрутизатор слухає широкомовні DHCP-запити, переписує їх у звичайні (unicast) пакети до сервера й записує в поле giaddr власну адресу на цьому інтерфейсі. Сервер по giaddr бачить, з якої підмережі прийшов запит, і видає адресу саме з неї. Відповідь іде назад до relay, який доставляє її клієнтові. Один сервер може обслуговувати десятки підмереж.

Адрес IPv4 близько чотирьох мільярдів, а пристроїв набагато більше. Для внутрішніх мереж виділено приватні діапазони, які ніколи не маршрутизуються в публічному інтернеті: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918). У двох різних компаніях можуть бути однакові адреси 10.0.0.5, і нікому це не заважає, бо назовні вони не виходять.

Але якщо пристрій зі своєю приватною адресою захоче відкрити сайт, то пакет піде з src=192.168.1.116. Сервер відповість на цю адресу, а вона в інтернеті нічия: жоден маршрутизатор не знає, куди її везти. Потрібна підміна: на межі мережі переписувати адресу відправника на публічну. Така підміна називається NAT (Network Address Translation).

PAT: коли хостів більше, ніж адрес

Section titled “PAT: коли хостів більше, ніж адрес”

Проста заміна адреса на адресу розв’язала б задачу, якби публічних адрес було стільки ж, скільки внутрішніх. Але їх стільки немає, і на практиці NAT переписує ще й порт відправника. Цей варіант називають PAT (Port Address Translation), або NAT із перевантаженням. Слово «NAT» у розмові майже завжди означає саме його.

Трансляція адрес і портів на шлюзі: таблиця відповідностіЛокальна мережаШлюз з NATЗовнішня мережахост A192.168.1.116:40000хост B192.168.1.50:40000NATпублічна адреса 203.0.113.1Сервер 203.0.113.10:80:40000:40001таблиця conntrack: приватна сторона ↔ публічна сторона192.168.1.116:40000↔203.0.113.1:40000192.168.1.50:40000↔203.0.113.1:40001→ 203.0.113.10:80→ 203.0.113.10:80Вихідний пакет: джерело переписуєтьсяВідповідь: призначення переписується назад за таблицею
Обидва хости обрали один і той самий вихідний порт 40000. Назовні вони виходять під однією адресою, але шлюз дає другому хосту інший порт, і за таблицею відповідь повертається потрібному.

Хост 192.168.1.116 відкриває з’єднання з вихідного порту 40000 до 203.0.113.10:80. Шлюз бачить вихідний пакет, записує в таблицю «приватна адреса й порт ↔ публічна адреса й порт», переписує адресу відправника на свою публічну, а порт залишає, якщо він вільний. Коли інший хост спробує використати той самий порт до того самого сервера, шлюз змінить порт на інший. Сервер бачить два різні з’єднання з однієї адреси: від 203.0.113.1:40000 і від 203.0.113.1:40001. Відповіді прийдуть на ці адреси, а шлюз за таблицею переписує призначення назад і відправляє потрібному хосту.

NAT має стан. Запис у таблиці з’являється, коли зсередини надійшов перший пакет потоку, і зникає за тайм-аутом. Без запису відповідь не має куди повертатися: шлюз не знає, кому її віддати.

Ззовні не можна просто так зайти досередини. Якщо записів у таблиці немає, вхідний пакет на публічну адресу шлюзу звернений до самого шлюзу, а не до жодного хоста за ним. Ефект корисний, але побічний; про нього нижче.

У Linux NAT виконує підсистема netfilter, керована через nftables (детальніше в модулі 16).

SNAT (source NAT) змінює адресу відправника: так роблять вихідні з’єднання. Виконується вона у хуку postrouting, коли рішення про маршрут уже ухвалено. Правило пишуть або як snat to 203.0.113.1 з явною адресою, або як masquerade, і відрізняються вони тим, звідки береться нова адреса. masquerade бере адресу інтерфейсу, через який пакет виходить, щоразу наново, а коли інтерфейс падає, забуває пов’язані з ним записи. Для домашнього маршрутизатора, де публічна адреса приходить із DHCP і міняється, саме це й потрібно. Якщо адреса стала, snat to трохи ефективніший.

DNAT (destination NAT) змінює адресу призначення: так роблять проброс портів (port forwarding), коли пакет, що прийшов на публічну адресу, треба відправити всередину. Виконується в хуку prerouting, до маршрутизації, тож після заміни пакет маршрутизується вже за новою адресою.

Обидва напрямки використовують одну й ту саму таблицю. Коли відповідь повертається, netfilter автоматично виконує зворотну підміну: окремих правил для відповіді писати не треба.

Таблицю, про яку йшлося вище, веде модуль conntrack (connection tracking) у ядрі Linux. Він відстежує кожен потік, що проходить через машину, а не лише NAT-овані. Без цього NAT був би неможливий, а stateful-файрвол (модуль 16) теж.

Кожен запис містить два кортежі: як пакет виглядає в одному напрямку й як у зворотному. Різниця між ними і є трансляція. Якщо кортежі однакові з точністю до розвороту, NAT для потоку немає.

Для TCP conntrack відстежує стани, схожі на стани TCP (модуль 11): SYN_SENT, SYN_RECV, ESTABLISHED, FIN_WAIT, LAST_ACK, TIME_WAIT, CLOSE. Для UDP і ICMP, де з’єднання немає, стан приблизний: перший пакет створює запис, відповідь підтверджує його, а тайм-аут обрізає тишу. У правилах nftables стан виражається спрощеніше, через ct state:

Стан Що означає
new перший пакет потоку, який ще не бачили
established потік, у якому пакети вже йшли в обидва боки
related новий потік, породжений наявним (наприклад, ICMP-помилка до з’єднання, яке ми вели)
invalid пакет, що не вписується в жоден потік

Записи живуть обмежений час. Типові значення в Linux (перевіряйте на своїй системі у /proc/sys/net/netfilter/):

Параметр Значення в нашому прогоні
nf_conntrack_tcp_timeout_established 432000 с (п’ять діб)
nf_conntrack_tcp_timeout_time_wait 120 с
nf_conntrack_udp_timeout 30 с
nf_conntrack_udp_timeout_stream 120 с (для UDP-потоку, що вже чув відповідь)
nf_conntrack_max 262144 записи (залежить від пам’яті)

П’ять діб для ESTABLISHED виглядають дивно, але TCP-з’єднання може мовчати годинами, залишаючись живим, і якщо шлюз забуде запис, то перший же наступний пакет опиниться без пари. Для UDP, де жодного з’єднання немає, тайм-аут навмисно короткий: 30 секунд тиші, і запис зникає. Тому застосунки, яким треба тримати UDP-«з’єднання» за NAT відкритим (VoIP, ігри, VPN), надсилають keepalive кожні 15–25 секунд.

Максимум записів має практичні наслідки. Коли таблиця повна, ядро відкидає нові потоки й пише про це в журнал (nf_conntrack: table full, dropping packet). Зазвичай це видно на навантажених шлюзах, серверах із тисячами короткоживучих з’єднань і у відповідь на сканування або SYN flood.

Кількість записів на одного клієнта обмежена не лише максимумом таблиці. Вихідних портів 65535, але унікальним має бути весь кортеж: адреси, порти й протокол. Тому ті самі публічна адреса й порт можуть одночасно служити для з’єднань до різних серверів. Вичерпуються порти лише тоді, коли багато клієнтів тримають тисячі одночасних з’єднань до одного й того самого сервера та порту. Для провайдерських NAT ця межа реальна.

Найпоширеніша помилка: «мене захищає NAT, бо ззовні мене не видно». Так здається, бо з побутового роутера прямо з інтернету справді не зайти. Та причина тут інша.

Вхідний пакет до приватної адреси не доходить не тому, що NAT його перехоплює, а тому, що в публічному інтернеті немає маршруту до 192.168.1.116. Його відкинули б маршрутизатори провайдера. Але якщо зловмисник сидить по той бік шлюзу (у тому самому сегменті провайдера, або провайдер сам погодився маршрутизувати приватну мережу до вас), то нічого, крім налаштувань пересилання, між ним і вашим хостом не стоїть. Далі в розділі про Linux це показано руками.

Захист виконує правило фільтрації з відстеженням стану: приймати пакети з зовнішнього боку, лише якщо вони належать з’єднанню, яке почалося зсередини. Побутові роутери вмикають обидва механізми разом, тому їх легко сприйняти за один, а от на налаштованому вручну шлюзі їх треба вмикати окремо.

Ви пробросили порт 8080 на сервер у локальній мережі. Ззовні 203.0.113.1:8080 працює. Та коли з ноутбука в тій самій мережі ви відкриваєте ту саму адресу, з’єднання зависає.

Причина в шляху пакета. Ноутбук (192.168.1.50) шле SYN на публічну адресу шлюзу. Шлюз робить DNAT і пересилає пакет серверу (192.168.1.116) в тій самій мережі, з якої він прийшов, не змінюючи адресу відправника. Сервер бачить запит від 192.168.1.50, який знаходиться в нього в локальній мережі, і відповідає напряму, минаючи шлюз. Для ноутбука ж відповідь приходить не від 203.0.113.1:8080, куди він слав SYN, а від 192.168.1.116:8000. Це не те з’єднання, яке він відкривав, і він відповідає на неї скиданням (RST).

Лікується це тим, що шлюз робить для таких потоків ще й SNAT: підміняє відправника на власну адресу, і тоді сервер відповідає шлюзу, а той повертає відповідь ноутбуку з правильною адресою. Це називається hairpin NAT (NAT-шпилька, бо пакет іде до шлюзу й повертається тією самою дорогою) або NAT loopback. Інший і часто кращий вихід: щоб у локальній мережі DNS повертав для цього імені внутрішню адресу, і пакет взагалі не ходив до шлюзу.

Адрес IPv4 на всіх не вистачило, тож провайдери самі почали ставити NAT між своїми клієнтами й інтернетом. Це CGNAT (carrier-grade NAT): ваш домашній роутер отримує від провайдера приватну адресу, і на стороні провайдера трафік транслюється ще раз. NAT у NAT. Для проміжку між абонентом і провайдером виділено окремий діапазон 100.64.0.0/10 (RFC 6598). Він не належить ні до приватних діапазонів з RFC 1918, ні до публічних адрес, тож не зіткнеться з вашою домашньою мережею. Якщо ip addr на вашому роутері показує WAN-адресу з цього діапазону, ви за CGNAT.

Наслідки відчутні:

  • Проброс портів неможливий. Порт, який ви відкриваєте на домашньому роутері, нічого не змінює: назовні сидить ще один NAT, яким ви не керуєте.
  • Одна публічна адреса на сотні абонентів. Якщо хтось із них потрапив у чорний список за спам чи DDoS, постраждають усі. Сайти, що обмежують запити за адресою, бачать усіх цих людей як одного користувача.
  • Порти розподілені між абонентами. Кожному дістається обмежений діапазон портів, і програма, що відкриває багато з’єднань, впирається в нього.

Два пристрої за різними NAT хочуть з’єднатися напряму (дзвінок, гра, передача файлу). Але жодному з них не можна просто надіслати пакет ззовні: записів у таблицях ще немає.

Спершу кожен пристрій дізнається свою зовнішню адресу й порт через публічний сервер (протокол STUN: запит вгору, у відповіді «я бачу тебе як 203.0.113.1:40000»). Потім обидва обмінюються цією інформацією через посередника, який має публічну адресу (сигнальний сервер). Нарешті обидва одночасно надсилають один одному UDP-пакет на зовнішню адресу партнера. Перший пакет кожного в першій мить відкидається другим NAT, бо запису ще немає, але він створює запис у таблиці свого NAT, через який зустрічний пакет уже пройде. Це називається UDP hole punching, а весь набір прийомів разом зветься ICE.

Працює не завжди. Деякі NAT, серед них багато CGNAT, для кожного нового призначення виділяють новий зовнішній порт (симетричний NAT), і тоді порт, який бачив STUN-сервер, не збігається з портом, яким користуватиметься партнер. Тоді лишається єдиний вихід: пересилати весь трафік через публічний ретранслятор (TURN-сервер), що коштує грошей і додає затримку. Частина дзвінків у відеочатах з’єднується саме через ретранслятор, і помилки конфігурації тут немає.

Усе нижче запущено в просторах імен: m9cli і m9cl2 (клієнти в локальній мережі 192.168.1.0/24), m9gw (шлюз) і m9srv (сервер з адресою 203.0.113.10). Назовні в шлюзу інтерфейс m9-gw з адресою 203.0.113.1. Спочатку клієнт m9cli з’єднаний зі шлюзом прямою парою veth (на боці шлюзу m9-gl), а для досліду з hairpin додано міст m9br у m9gw і другого клієнта m9cl2. Вивід скорочено. Так само побудована лабораторна B2.

dnsmasq, крім DNS, уміє роздавати адреси. Мінімальний конфіг лише для DHCP:

interface=m9-gl
bind-interfaces
except-interface=lo
port=0
dhcp-range=192.168.1.100,192.168.1.150,255.255.255.0,12h
dhcp-option=option:router,192.168.1.1
dhcp-option=option:dns-server,192.168.1.1
dhcp-leasefile=/tmp/m9/leases
log-dhcp
no-ping

port=0 вимикає вбудований DNS: тут потрібен лише DHCP. dhcp-range задає пул і термін оренди. no-ping потрібен, бо за замовчуванням dnsmasq перед Offer пінгує адресу, щоб перевірити, чи вона вільна. У нашому спостереженні це коштувало трьох секунд між Discover і Offer.

Terminal window
sudo ip netns exec m9gw dnsmasq -C dnsmasq.conf
sudo ip netns exec m9cli dhclient -v -d -1 m9-c

Клієнт друкує чотири кроки:

DHCPDISCOVER on m9-c to 255.255.255.255 port 67 interval 3
DHCPOFFER of 192.168.1.116 from 192.168.1.1
DHCPREQUEST for 192.168.1.116 on m9-c to 255.255.255.255 port 67
DHCPACK of 192.168.1.116 from 192.168.1.1
bound to 192.168.1.116 -- renewal in 20855 seconds.

Те саме з боку проводу дає tcpdump -i m9-gl -nn -vv 'udp port 67 or udp port 68'. Ось Offer, скорочено:

192.168.1.1.67 > 192.168.1.116.68: BOOTP/DHCP, Reply, xid 0x7a4ea138
Your-IP 192.168.1.116
Server-IP 192.168.1.1
DHCP-Message (53), length 1: Offer
Server-ID (54), length 4: 192.168.1.1
Lease-Time (51), length 4: 43200
RN (58), length 4: 21600
RB (59), length 4: 37800
Subnet-Mask (1), length 4: 255.255.255.0
Domain-Name-Server (6), length 4: 192.168.1.1
Default-Gateway (3), length 4: 192.168.1.1

Тут видно: xid ідентифікує транзакцію (той самий у всіх чотирьох пакетах, так клієнт відрізняє свою відповідь від чужих), термін оренди 43200 секунд (дванадцять годин), RN і RB це T1 і T2: 21600 с (половина) і 37800 с (87,5%). Поле Your-IP це запропонована адреса. У tcpdump на veth може зустрітися повідомлення bad udp cksum: суму драйвер обчислює відкладено, бо пакет ніколи не виходить на справжній дріт, і помилкою це не є.

Сервер веде власний реєстр оренд. Формат файлу leases у dnsmasq: час спливу в секундах від епохи, MAC, адреса, ім’я хоста, ідентифікатор клієнта.

1790796347 42:21:ed:5b:03:eb 192.168.1.116 vm *

А клієнт показує, що адреса отримана динамічно:

$ ip -4 addr show dev m9-c
inet 192.168.1.116/24 brd 192.168.1.255 scope global dynamic m9-c

Слово dynamic означає, що адреса тимчасова, з орендою; у повному виводі поруч є valid_lft (скільки секунд вона ще дійсна).

Спершу переконаємося, що проблема існує. Сервер не має маршруту до приватної мережі, і без NAT відповідь не повернеться:

$ ip netns exec m9cli curl -sS -m 2 http://203.0.113.10/
curl: (28) Connection timed out after 2002 milliseconds

Додаємо правило на шлюзі:

Terminal window
sudo ip netns exec m9gw sysctl -w net.ipv4.ip_forward=1
sudo ip netns exec m9gw nft -f - <<'EOF'
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "m9-gw" masquerade
}
}
EOF

Таблиця сімейства ip (тобто IPv4) із назвою nat; у ній ланцюжок postrouting, прикріплений до хука postrouting типу nat із пріоритетом srcnat. Саме правило: для пакетів, що виходять інтерфейсом m9-gw, застосувати masquerade. Тепер:

$ ip netns exec m9cli curl -sS -m 2 http://203.0.113.10/
you are 203.0.113.1:60508

Тестовий сервер повертає адресу, з якої до нього прийшли: це публічна адреса шлюзу, а не 192.168.1.116. Порт 60508 той самий, який обрав клієнт: NAT намагається його зберегти.

Замість masquerade можна написати явний snat to 203.0.113.1 (перевірено: працює так само). Різниця, як сказано вище, у тому, що masquerade щоразу дивиться на адресу інтерфейсу.

$ ip netns exec m9gw conntrack -L
tcp 6 119 TIME_WAIT src=192.168.1.116 dst=203.0.113.10 sport=60508 dport=80 \
src=203.0.113.10 dst=203.0.113.1 sport=80 dport=60508 [ASSURED] mark=0 use=1

Запис містить два кортежі. Перший, src=192.168.1.116 ... sport=60508, це пакет у напрямку від клієнта так, як він прийшов на шлюз. Другий, src=203.0.113.10 dst=203.0.113.1 ..., це відповідь, яку очікують. Зверніть увагу: у відповіді призначення 203.0.113.1, а не 192.168.1.116. Це і є трансляція. Число 119 це секунди до видалення запису; [ASSURED] означає, що потік бачили в обох напрямках.

Відкритий потік живе інакше:

tcp 6 431999 ESTABLISHED src=192.168.1.116 dst=203.0.113.10 sport=48448 dport=80 ...
udp 17 29 src=192.168.1.116 dst=203.0.113.10 sport=36076 dport=5000 ...

Для ESTABLISHED залишилось майже п’ять діб, для UDP усього 29 секунд, і це згадані вище тайм-аути. Послідовність станів одного TCP-з’єднання видно в потоці подій:

$ ip netns exec m9gw conntrack -E -p tcp --dport 80
[NEW] tcp 6 120 SYN_SENT src=192.168.1.116 dst=203.0.113.10 sport=53538 dport=80 [UNREPLIED] ...
[UPDATE] tcp 6 60 SYN_RECV ...
[UPDATE] tcp 6 432000 ESTABLISHED ... [ASSURED]
[UPDATE] tcp 6 120 FIN_WAIT ...
[UPDATE] tcp 6 29 LAST_ACK ...
[UPDATE] tcp 6 120 TIME_WAIT ...

Корисні ще conntrack -C (скільки записів зараз), conntrack -F (скинути таблицю) і conntrack -S (лічильники помилок, серед них записи, що не вдалося вставити).

На клієнті m9cli запущено веб-сервер на порту 8000. Шлюз має переслати на нього порт 8080 публічної адреси:

Terminal window
sudo ip netns exec m9gw nft add chain ip nat prerouting \
'{ type nat hook prerouting priority dstnat; policy accept; }'
sudo ip netns exec m9gw nft add rule ip nat prerouting \
ip daddr 203.0.113.1 tcp dport 8080 dnat to 192.168.1.116:8000

Ланцюжок прикріплено до хука prerouting із пріоритетом dstnat: підміна повинна статися до маршрутизації. Перевірка із зовнішнього боку:

$ ip netns exec m9srv curl -s -o /dev/null -w '%{http_code}\n' http://203.0.113.1:8080/
200
$ ip netns exec m9gw conntrack -L -p tcp | grep 8080
tcp 6 119 TIME_WAIT src=203.0.113.10 dst=203.0.113.1 sport=54796 dport=8080 \
src=192.168.1.116 dst=203.0.113.10 sport=8000 dport=54796 ...

Кортежі тут виглядають інакше, ніж у попередньому прикладі. Перший каже, що клієнт стукав у 203.0.113.1:8080. Другий, що відповідь очікують від 192.168.1.116:8000. Змінилась адреса призначення, а адреса джерела лишилася справжньою, тож внутрішній сервер бачить справжню адресу клієнта.

Правило dnat вище не обмежене інтерфейсом, а тому спрацьовує й для клієнтів у мережі. Запит із m9cl2 (192.168.1.50) до 203.0.113.1:8080 зависає:

$ ip netns exec m9cl2 curl -sS -m 3 http://203.0.113.1:8080/
curl: (28) Connection timed out after 3002 milliseconds
192.168.1.50.54468 > 203.0.113.1.8080: Flags [S]
192.168.1.116.8000 > 192.168.1.50.54468: Flags [S.], ...
192.168.1.50.54468 > 192.168.1.116.8000: Flags [R], ...

SYN пішов на публічну адресу, SYN-ACK прийшов від приватної, і клієнт відповів RST: рівно той збій, який описано вище. Виправляємо SNAT-ом для потоків, що йдуть із локальної мережі назад у неї:

Terminal window
sudo ip netns exec m9gw nft add rule ip nat postrouting \
oifname "m9br" ip saddr 192.168.1.0/24 ip daddr 192.168.1.116 tcp dport 8000 masquerade

Після цього curl повертає 200.

NAT не файрвол: експеримент

Section titled “NAT не файрвол: експеримент”

Дамо серверу маршрут до приватної мережі через шлюз, наче ми провайдер, що маршрутизує її:

Terminal window
sudo ip netns exec m9srv ip route add 192.168.1.0/24 via 203.0.113.1
$ ip netns exec m9srv curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.116:8000/
200

Сервер дістався до приватної адреси напряму, хоч masquerade налаштований. Правила NAT діють лише на ті пакети, які вони вміють переписати. Ті, що прийшли зовні на приватну адресу, NAT не чіпає, і шлюз їх просто пересилає за таблицею маршрутів.

Закриває це фільтр:

Terminal window
sudo ip netns exec m9gw nft -f - <<'EOF'
table inet filter {
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct status dnat accept
iifname "m9br" accept
}
}
EOF

Політика drop відкидає все пересилання за замовчуванням. Пропускаються лише відповіді на потоки, що вже йдуть (established,related), потоки, які пройшли DNAT, тобто проброшені порти (ct status dnat), і все, що починається з боку локальної мережі (iifname "m9br"). Результат:

$ ip netns exec m9srv curl -sS -m 2 http://192.168.1.116:8000/
curl: (28) Connection timed out after 2002 milliseconds
$ ip netns exec m9srv curl -s -o /dev/null -w '%{http_code}\n' http://203.0.113.1:8080/
200
$ ip netns exec m9cli curl -s http://203.0.113.10/
you are 203.0.113.1:36108

Пряма спроба не проходить, проброс працює, вихідні з’єднання клієнтів теж. Захист дало правило фільтрації, яке так само спирається на conntrack, як і NAT, але є окремим кроком. Докладніше про це в модулі 16 і в лабораторній B4.

Типові помилки розуміння

Section titled “Типові помилки розуміння”

NAT захищає від атак ззовні. Вхідний пакет не доходить до приватної адреси лише тому, що до неї немає маршруту в публічній мережі. Тільки правила фільтрації з відстеженням стану відкидають те, чого внутрішні хости не запрошували. Побутовий роутер має обидва механізми, тож здається, що це один.

NAT змінює лише адресу. У PAT переписується ще й порт, і саме це дозволяє багатьом хостам ділити одну адресу. Тому NAT має зберігати стан: без таблиці відповіді нікуди повертати.

Записи в conntrack існують, доки триває з’єднання. Вони живуть за тайм-аутами: для UDP це десятки секунд, для TIME_WAIT двохвилинний період. Запис може зникнути, поки програма ще вважає, що з’єднання відкрито, і тоді перший же наступний пакет загубиться. Через це VPN і VoIP надсилають keepalive.

Проброс портів на роутері розв’язує проблему зовнішнього доступу. Лише якщо роутер має публічну адресу. За CGNAT адреса на вашому роутері приватна (часто з діапазону 100.64.0.0/10), і проброс не діє, бо назовні сидить ще один NAT.

Якщо роутер роздає адреси, він і є DHCP-сервером мережі. Сервер це лише програма, яка слухає порт 67. Вона може працювати на роутері, на окремій машині або на маршрутизаторі в іншому сегменті, з’єднаному через relay. А в мережі, де дві такі програми відповідають одночасно, клієнт візьме ту, що швидша.

DHCP і DNS це одна служба. Це два окремі протоколи: DHCP лише повідомляє клієнтові, який DNS-сервер використовувати, а більше нічого про нього не знає. Плутають їх через те, що dnsmasq уміє обидва.

Перевір себе

1. Чому перше повідомлення DHCP (Discover) широкомовне й надсилається з адреси 0.0.0.0?
2. Сервер DHCP вимкнули на годину при оренді на 12 годин. Що станеться з клієнтами, які вже мають адреси?
3. Два хости за NAT одночасно відкривають зʼєднання з одного й того самого порту 40000 на один сервер. Як шлюз їх розрізняє?
4. Ви встановили проброс порту на домашньому роутері, але з інтернету до сервера не зайти. На WAN-інтерфейсі роутера адреса 100.72.15.9. Що це означає?
5. У NAT-шлюзі працює masquerade, але політика forward дозволяє все. Сусід по сегменту провайдера дописав маршрут до вашої приватної підмережі через ваш шлюз. Що станеться?
6. Проброс порту працює з інтернету, але клієнт у тій самій локальній мережі, звертаючись до публічної адреси, отримує зависання. SYN-ACK приходить не з тієї адреси, куди слали SYN. Як це виправити?
7. У conntrack для UDP-потоку, що простоює, запис зникає за 30 секунд. Що це означає для застосунку, який хоче тримати UDP-«зʼєднання» крізь NAT відкритим?

B2. Linux як маршрутизатор. Три підмережі, статичні маршрути, NAT і DHCP через dnsmasq: усе, що в цьому модулі, ви зберете самі й перевірите чекером.

B4. Файрвол і WireGuard. Політика «за замовчуванням заборонено» в nftables, stateful-правила й проброс портів. Відмінність між NAT і фільтрацією, показана вище, там стає практичною вимогою.

  • Kurose, Ross, Computer Networking: A Top-Down Approach, розділ про мережевий рівень (DHCP і NAT)
  • Stevens, TCP/IP Illustrated, Vol. 1, розділи про DHCP і NAT
  • Peterson, Davie, Computer Networks: A Systems Approach
  • RFC 2131 і RFC 2132 (DHCP та його опції), RFC 1918 (приватні адреси), RFC 6598 (адресний простір для CGNAT)
  • Документація nftables: wiki.nftables.org, розділи про NAT і stateful-фільтрацію
  • man nft(8), man dnsmasq(8), man conntrack(8), man dhclient(8)