Прикладні мережеві протоколи DNS та SMTP
Прикладні мережеві протоколи DNS та SMTP
🎯 Навчальні цілі
- Усвідомити фундаментальну роль системи доменних імен (DNS) як критичної інфраструктури інтернету, без якої неможливе функціонування жодного мережевого сервісу.
- Зрозуміти ієрархічну структуру DNS-простору імен від кореневих серверів до локальних зон, механізми делегування відповідальності та кешування на різних рівнях.
- Опанувати формат DNS-повідомлень: заголовок, секції запитів та відповідей, формат ресурсних записів (Resource Records) і роль кодів відповідей.
- Вивчити типи DNS-записів (A, AAAA, CNAME, MX, TXT, SRV) та зрозуміти їхнє практичне застосування для маршрутизації трафіку, виявлення сервісів та верифікації доменів.
- Дослідити архітектурну проблему обмеження CNAME на вершині зони (Zone Apex) за стандартом RFC 1034 та зрозуміти, як технології CNAME Flattening / ALIAS, новий стандарт RFC 9460 (HTTPS/SVCB), заголовок Host в HTTP/1.1 та розширення SNI в TLS уможливили повноцінне функціонування сайтів без піддомену
www. - Навчитися програмно виконувати DNS-резолвінг у Go через пакет
net.Resolverіз підтримкою кастомних DNS-серверів, таймаутів та обробки помилок. - Зрозуміти архітектуру електронної пошти та роль протоколу SMTP (Simple Mail Transfer Protocol) як стандарту передачі повідомлень між поштовими серверами.
- Розібрати структуру SMTP-сеансу: команди (HELO, MAIL FROM, RCPT TO, DATA), коди відповідей та діаграму послідовності успішної доставки листа.
- Опанувати формат MIME (Multipurpose Internet Mail Extensions): багатокомпонентні повідомлення (Multipart/alternative, Multipart/mixed), кодування Base64 та вкладення файлів.
- Вивчити сучасні механізми захисту електронної пошти: шифрування каналу через STARTTLS, автентифікація джерела за допомогою SPF, цифрові підписи DKIM та політики DMARC.
- Навчитися програмно надсилати електронні листи через Go-пакет
net/smtpіз налаштуванням автентифікації, TLS та формуванням MIME-повідомлень.
🛠️ Технологічний стек
- Мова: Go 1.22+
- Пакети стандартної бібліотеки:
net,net/smtp,net/mail,mime,encoding/base64 - Інструменти діагностики:
dig/nslookup/hostдля DNS-запитів,telnetдля ручного SMTP-сеансу, Wireshark для аналізу трафіку - Тестові сервіси: MailHog / Mailpit для локального емулятора SMTP-сервера, Cloudflare DNS / Google Public DNS (8.8.8.8)
Система доменних імен (DNS): від числових адрес до зрозумілих імен
У попередніх лекціях ми працювали з мережевими адресами у форматі IP-адрес: 127.0.0.1 для локального сервера, 192.168.1.100 для пристрою в локальній мережі, публічні адреси типу 93.184.216.34 для з'єднання з віддаленими серверами в інтернеті. Але коли ви, як звичайний користувач, відкриваєте браузер і вводите в адресному рядку https://www.google.com або коли ваш мобільний застосунок на React Native підключається до API за адресою https://api.company.com/v1/users, — ви ніде не вказуєте IP-адресу. Як же комп'ютер дізнається, на яку конкретну числову адресу йому потрібно встановлювати TCP-з'єднання?
Відповідь проста і водночас надзвичайно складна: за лаштунками працює система доменних імен (Domain Name System, DNS) — глобальна розподілена база даних, яка перетворює зручні для людини символьні імена (як-от www.google.com) на числові IP-адреси (наприклад, 142.250.185.68), необхідні для маршрутизації пакетів у мережі. DNS — це один із найстаріших і найкритичніших протоколів інтернету, без якого неможливе функціонування практично жодного сучасного мережевого сервісу: вебсайтів, REST API, електронної пошти, мобільних застосунків, IoT-пристроїв, систем потокового відео — усі вони в тому чи іншому вигляді покладаються на DNS для перетворення імен на адреси.
HOSTS.TXT, який у ранні дні інтернету (тоді він називався ARPANET) вручну розподілявся між усіма вузлами мережі. Із зростанням кількості хостів до тисяч і десятків тисяч стало очевидно, що централізоване керування одним файлом більше не є масштабованим рішенням, і виникла потреба в розподіленій ієрархічній системі, здатній обробляти мільярди запитів на день.Чому DNS є критично важливим для клієнт-серверних систем
Спробуємо усвідомити масштаб впливу DNS на повсякденну роботу будь-якого інженера-програміста. Кожного разу, коли ваш Go-сервер виконує виклик http.Get("https://api.external-service.com/data"), перед встановленням TCP-з'єднання Go-рантайм звертається до системного DNS-резолвера операційної системи із запитом: «Яка IP-адреса відповідає імені api.external-service.com?». Резолвер, у свою чергу, може мати кешовану відповідь із попередніх запитів або звернутися до рекурсивного DNS-сервера вашого інтернет-провайдера, який запитає авторитетні DNS-сервери домену .com, потім авторитетні сервери зони external-service.com, отримає відповідь, поверне її вашому резолверу, закешує на певний час (визначений полем TTL — Time To Live) і лише після цього ваша програма зможе встановити з'єднання.
Якщо на будь-якому з цих етапів станеться збій — DNS-сервер недоступний, доменне ім'я не існує, відповідь сфальсифікована зловмисником (атака DNS Spoofing) — ваш застосунок отримає помилку, навіть якщо сам сервер, до якого ви намагаєтеся підключитися, працює ідеально. Саме тому розуміння архітектури DNS, форматів запитів і відповідей, типів ресурсних записів та механізмів безпеки є абсолютно критичним для будь-якого бекенд-розробника.
Ієрархічна структура DNS-простору імен: від кореня до вузла
Система доменних імен побудована як ієрархічне дерево, структурно схоже на файлову систему Unix, де кожен вузол дерева представляє зону відповідальності (zone), а кожна зона керується набором авторитетних DNS-серверів (authoritative nameservers). Розглянемо повне доменне ім'я (Fully Qualified Domain Name, FQDN) www.example.com. — зверніть увагу на крапку в кінці, яка є важливою, хоча зазвичай її опускають у повсякденному користуванні.
Розбір структури FQDN зправа наліво
Доменне ім'я читається справа наліво, від загального до конкретного:
- Крапка в кінці (
.) — це корінь дерева DNS (root zone). Він не має імені, але є початковою точкою всієї ієрархії. Кореневі DNS-сервери (їх усього 13 логічних кластерів по всьому світу, позначених літерами відAдоM:a.root-servers.net,b.root-servers.netтощо) зберігають інформацію про те, які сервери відповідають за домени верхнього рівня (TLD). com— це домен верхнього рівня (Top-Level Domain, TLD). Існують загальні TLD (gTLD) як-от.com,.org,.net,.edu, і національні TLD (ccTLD) як-от.ua,.us,.uk. За кожен TLD відповідають окремі авторитетні сервери, наприклад, зону.comкерує організація Verisign.example— це домен другого рівня (second-level domain), зареєстрований власником (фізичною особою, компанією, організацією) через акредитованого реєстратора доменів (наприклад, GoDaddy, Namecheap). Власник домену налаштовує власні авторитетні DNS-сервери (або використовує сервіси на кшталт Cloudflare DNS, AWS Route 53), які зберігають інформацію про всі піддомени третього рівня.www— це піддомен третього рівня (subdomain), який вказує на конкретний сервер або сервіс. Традиційноwwwвикористовується для вебсерверів, але можуть існувати будь-які інші імена:api.example.com,mail.example.com,cdn.example.comтощо.

Процес рекурсивного резолвінгу: як клієнт отримує відповідь
Коли ваш застосунок (наприклад, Go-клієнт) викликає net.LookupHost("www.example.com"), за лаштунками відбувається рекурсивний процес резолвінгу (recursive resolution), який можна розбити на наступні кроки:
Крок 1. Перевірка локального кешу резолвера
Операційна система має власний DNS-резолвер (у Linux це зазвичай systemd-resolved, у macOS — mDNSResponder), який зберігає кеш недавніх DNS-відповідей. Якщо запис для www.example.com уже є в кеші і його час життя (TTL) ще не вичерпано — відповідь повертається негайно, без мережевих запитів.
Крок 2. Звернення до рекурсивного DNS-сервера
Якщо кешу немає, резолвер надсилає запит до налаштованого рекурсивного DNS-сервера (зазвичай це сервер інтернет-провайдера або публічні резолвери на кшталт Google 8.8.8.8, Cloudflare 1.1.1.1). Цей сервер бере на себе відповідальність за виконання всього ланцюжка запитів замість клієнта.
Крок 3. Запит до кореневого сервера
Рекурсивний сервер звертається до одного з 13 кореневих серверів із запитом: «Хто відповідає за зону .com?». Кореневий сервер не знає кінцевої адреси www.example.com, але знає адреси авторитетних серверів для TLD .com і повертає їх у відповіді (тип запису NS — Name Server).
Крок 4. Запит до TLD-сервера
Рекурсивний сервер звертається до TLD-сервера .com із тим самим запитом. TLD-сервер також не знає кінцевої адреси, але знає, які сервери відповідають за зону example.com, і повертає їхні адреси (знову NS-записи).
Крок 5. Запит до авторитетного сервера домену
Нарешті, рекурсивний сервер звертається до авторитетного DNS-сервера зони example.com (наприклад, ns1.example.com) із запитом про www.example.com. Цей сервер має авторитетну відповідь (authoritative answer) — він точно знає, що www.example.com вказує на IP-адресу 93.184.216.34, і повертає запис типу A (для IPv4) або AAAA (для IPv6).
Крок 6. Повернення відповіді клієнту та кешування
Рекурсивний сервер отримує кінцеву відповідь, кешує її на час, визначений полем TTL (наприклад, 300 секунд), і повертає IP-адресу клієнтській програмі. Тепер Go-застосунок може встановити TCP-з'єднання з 93.184.216.34:443 і розпочати TLS-рукостискання для HTTPS-запиту.
www.google.com. Якщо кожен запит вимагатиме проходження всього ланцюжка від кореневих серверів до авторитетних — це створило б колосальне навантаження на DNS-інфраструктуру. Кешування на рівні рекурсивних резолверів (які обслуговують тисячі клієнтів) різко зменшує кількість запитів до авторитетних серверів і прискорює час відповіді для кінцевих користувачів.Формат DNS-повідомлень: структура запитів та відповідей
Протокол DNS працює переважно через UDP на порту 53 (хоча для великих відповідей розміром понад 512 байт може використовуватися TCP). Кожне DNS-повідомлення — як запит від клієнта, так і відповідь від сервера — має єдиний бінарний формат, визначений у RFC 1035. Розуміння цієї структури є критично важливим, якщо вам потрібно реалізовувати власний DNS-клієнт, проксі-сервер або аналізувати DNS-трафік за допомогою Wireshark.
Структура заголовка DNS-повідомлення
Кожне DNS-повідомлення починається з 12-байтного заголовка (header), який містить метадані про тип повідомлення, кількість записів та прапорці керування. Заголовок складається з наступних полів:
- Transaction ID (16 біт) — унікальний ідентифікатор запиту, згенерований клієнтом. Коли сервер надсилає відповідь, він копіює цей ідентифікатор у свою відповідь, щоб клієнт міг зіставити відповідь із початковим запитом.
- Flags (16 біт) — набір прапорців, які визначають характер повідомлення:
- QR (1 біт):
0= запит (query),1= відповідь (response). - Opcode (4 біти): тип операції (зазвичай
0= стандартний запит). - AA (1 біт): Authoritative Answer — чи є відповідь авторитетною (від сервера, який безпосередньо відповідає за цю зону).
- TC (1 біт): Truncated — чи було повідомлення обрізане через обмеження розміру UDP (512 байт). Якщо
TC=1, клієнт повинен повторити запит через TCP. - RD (1 біт): Recursion Desired — клієнт просить сервер виконати рекурсивний резолвінг.
- RA (1 біт): Recursion Available — сервер підтримує рекурсію.
- Z (3 біти): зарезервовані біти (повинні бути нульовими).
- RCODE (4 біти): код відповіді (успіх, помилка, домен не існує тощо).
- QR (1 біт):
- QDCOUNT (16 біт) — кількість записів у секції запитів (Questions).
- ANCOUNT (16 біт) — кількість записів у секції відповідей (Answers).
- NSCOUNT (16 біт) — кількість записів у секції авторитетних серверів (Authority).
- ARCOUNT (16 біт) — кількість записів у додатковій секції (Additional).
Після заголовка йдуть чотири секції змінної довжини:
- Question Section — містить один або більше запитів (зазвичай один): доменне ім'я, тип запиту (A, AAAA, MX тощо) та клас (зазвичай
IN— Internet). - Answer Section — містить ресурсні записи (Resource Records), які є прямою відповіддю на запит.
- Authority Section — містить записи NS, які вказують на авторитетні сервери для зони (використовується в процесі делегування).
- Additional Section — містить додаткові записи, які можуть бути корисними (наприклад, IP-адреси серверів, згаданих у секції Authority).

Коди відповідей (RCODE)
Поле RCODE у заголовку вказує на результат обробки запиту:
- 0 (NOERROR) — успішна відповідь, запис знайдено.
- 1 (FORMERR) — помилка формату запиту (сервер не зміг його розпарсити).
- 2 (SERVFAIL) — внутрішня помилка сервера під час обробки запиту.
- 3 (NXDOMAIN) — Non-Existent Domain — доменне ім'я не існує. Це найважливіший код помилки: якщо ви намагаєтеся підключитися до
nonexistent.example.com, і DNS повертає NXDOMAIN, ваш застосунок має обробити цю ситуацію як неіснуючий домен, а не тимчасовий збій. - 4 (NOTIMP) — операція не підтримується сервером.
- 5 (REFUSED) — сервер відмовився обробляти запит (можливо, через політики безпеки або обмеження доступу).
Типи DNS-записів: від базових A та AAAA до службових SRV
Ресурсні записи (Resource Records, RR) — це фундаментальні структури даних, які зберігаються в DNS і повертаються у відповідях. Кожен запис має наступну загальну структуру:
NAME TTL CLASS TYPE RDATA
- NAME — доменне ім'я, до якого відноситься запис.
- TTL (Time To Live) — час у секундах, протягом якого запис може зберігатися в кеші резолверів.
- CLASS — клас мережі (зазвичай
IN— Internet). - TYPE — тип запису (A, AAAA, CNAME тощо).
- RDATA — дані запису (формат залежить від типу).
Розглянемо найважливіші типи записів, з якими ви стикатиметеся під час розробки клієнт-серверних застосунків.
Запис A (Address) — відображення на IPv4
Тип: A (числове значення 1)
Призначення: зіставляє доменне ім'я з 32-бітною IPv4-адресою.
Приклад:
www.example.com. 300 IN A 93.184.216.34
Цей запис говорить: «Домен www.example.com вказує на IPv4-адресу 93.184.216.34, і цю інформацію можна кешувати протягом 300 секунд (5 хвилин)».
Запити типу A — це найпоширеніший тип DNS-запитів у світі, бо вони необхідні для будь-якого підключення до вебсерверів, API, баз даних тощо через IPv4.
Запис AAAA (Quad-A) — відображення на IPv6
Тип: AAAA (числове значення 28)
Призначення: зіставляє доменне ім'я з 128-бітною IPv6-адресою.
Приклад:
www.example.com. 300 IN AAAA 2606:2800:220:1:248:1893:25c8:1946
У міру вичерпання адресного простору IPv4 та масового впровадження IPv6, записи AAAA стають дедалі важливішими. Сучасні застосунки повинні підтримувати обидва типи запитів: спочатку спробувати AAAA (якщо система підтримує IPv6), а у разі невдачі — відступити до A-запису.
net.Resolver у Go) часто виконують паралельні запити A та AAAA одночасно і повертають обидві адреси. Клієнтська програма намагається встановити з'єднання спочатку через IPv6 (якщо доступний), а у разі невдачі — через IPv4. Цей підхід називається Happy Eyeballs (RFC 8305) і забезпечує найкращу продуктивність незалежно від типу мережі.Запис CNAME (Canonical Name) — аліас доменного імені
Тип: CNAME (числове значення 5)
Призначення: створює аліас (псевдонім) для іншого доменного імені. Коли клієнт запитує CNAME-запис, резолвер автоматично виконує додатковий запит для канонічного імені.
Приклад:
blog.example.com. 300 IN CNAME www.example.com.
www.example.com. 300 IN A 93.184.216.34
Цей запис говорить: «blog.example.com насправді є аліасом для www.example.com, тож іди і дізнайся IP-адресу www.example.com».
CNAME-записи зручні для керування кількома піддоменами, які вказують на один і той самий сервер: якщо IP-адреса сервера зміниться, достатньо оновити лише один A-запис для www.example.com, і всі аліаси автоматично вказуватимуть на нову адресу.
example.com), бо для нього зазвичай потрібні MX-записи для пошти та інші службові записи. Для вирішення цієї проблеми деякі DNS-провайдери (Cloudflare, AWS Route 53) пропонують власні розширення на кшталт ALIAS або ANAME, які працюють як CNAME, але дозволяють співіснування з іншими записами.Запис MX (Mail Exchanger) — маршрутизація електронної пошти
Тип: MX (числове значення 15)
Призначення: вказує, на який поштовий сервер потрібно надсилати електронні листи, адресовані до домену.
Приклад:
example.com. 300 IN MX 10 mail1.example.com.
example.com. 300 IN MX 20 mail2.example.com.
Кожен MX-запис має пріоритет (перше число): чим менше значення, тим вищий пріоритет. У цьому прикладі поштовий сервер спочатку спробує доставити лист на mail1.example.com (пріоритет 10), і лише якщо цей сервер недоступний — звернеться до резервного mail2.example.com (пріоритет 20).
user@example.com, ваш SMTP-сервер спочатку виконує DNS-запит типу MX для домену example.com, отримує список поштових серверів, потім виконує A/AAAA-запити для кожного з них, щоб дізнатися їхні IP-адреси, і тільки після цього встановлює SMTP-з'єднання для передачі листа. Без коректних MX-записів доставка пошти на домен неможлива.Запис TXT — довільні текстові дані
Тип: TXT (числове значення 16)
Призначення: зберігає довільні текстові рядки, які використовуються для верифікації володіння доменом, налаштування політик безпеки пошти (SPF, DKIM, DMARC) та інших службових цілей.
Приклад:
example.com. 300 IN TXT "v=spf1 include:_spf.google.com ~all"
example.com. 300 IN TXT "google-site-verification=rXOxyZounnZasA8Z7oaD3c14JdjS9aKSWvsR1EbUSIQ"
TXT-записи надзвичайно гнучкі і використовуються для багатьох цілей:
- SPF (Sender Policy Framework) — вказує, які IP-адреси мають право надсилати пошту від імені домену.
- DKIM (DomainKeys Identified Mail) — містить відкритий ключ для перевірки цифрових підписів листів.
- DMARC (Domain-based Message Authentication, Reporting and Conformance) — визначає політику обробки листів, які не пройшли перевірку SPF/DKIM.
- Верифікація володіння доменом для сервісів на кшталт Google Workspace, AWS, Azure.
Запис SRV (Service) — виявлення сервісів
Тип: SRV (числове значення 33)
Призначення: надає інформацію про розташування конкретних сервісів: порт, пріоритет, вагу та ім'я хоста.
Формат:
_service._proto.name TTL IN SRV priority weight port target
Приклад:
_http._tcp.example.com. 300 IN SRV 10 60 80 server1.example.com.
_http._tcp.example.com. 300 IN SRV 10 40 80 server2.example.com.
SRV-записи особливо корисні для мікросервісних архітектур і систем виявлення сервісів (service discovery): замість жорсткого прописування адрес серверів у конфігурації, клієнт може запитати DNS про доступні екземпляри сервісу, отримати список із пріоритетами та вагами (для балансування навантаження) і динамічно підключитися до найоптимальнішого вузла.
Еволюція доменної адресації: чому сучасний веб відмовився від обов'язкового піддомену www
Ще на початку 2000-х років адреса будь-якого вебсайту в інтернеті обов'язково починалася з префікса www (наприклад, http://www.google.com або http://www.yahoo.com). Спроба ввести ім'я домену без префікса нерідко призводила до помилки з'єднання або вимагала від системних адміністраторів нетривіальних налаштувань інфраструктури. Сьогодні ж користувачі звикли вводити лаконічні імена на кшталт github.com чи kostyl.dev, а більшість сучасних браузерів узагалі приховують www в адресному рядку для спрощення інтерфейсу.
Проте перехід від обов'язкового піддомену www до так званого «голого домену» — це не просто зміна дизайнерських уподобань користувачів чи маркетологів. За цим стоїть тривала інженерна еволюція мережевих протоколів, подолання фундаментального обмеження стандарту DNS сорокарічної давнини та поява нових стандартів інтернет-маршрутизації.
Історичні витоки префікса www та функціональна спеціалізація хостів
У 1990-х роках Всесвітнє павутиння (World Wide Web) було лише однією з багатьох мережевих служб інтернету. Користувачі активно застосовували протокол передачі файлів (File Transfer Protocol, FTP) для обміну документами, протокол термінального доступу (Telnet) для дистанційного керування комп'ютерами, телеконференції Usenet через протокол NNTP, а також електронну пошту (SMTP).
В умовах обмежених ресурсів тогочасної комп'ютерної техніки кожен із цих сервісів зазвичай розгортався на окремому фізичному сервері зі своєю власною апаратною мережевою картою та унікальною IP-адресою. Щоб користувачі та клієнтські програми розуміли, до якої саме машини вони звертаються, в індустрії утвердилася угода про найменування за допомогою піддоменів третього рівня, які прямо вказували на протокол або призначення вузла:
ftp.company.com— вказував на IP-адресу FTP-сховища файлів;mail.company.com(абоsmtp.company.com) — адресував поштовий сервер організації;news.company.com— спрямовував запити до сервера телеконференцій Usenet;irc.company.com— сервер чатів у реальному часі;www.company.com— окремий сервер, на якому працював HTTP-демон (HTTP daemon, наприклад NCSA HTTPd або ранній Apache), призначений для роздачі HTML-сторінок.
Домен без будь-якого піддомену — тобто домен другого рівня company.com — називають вершиною зони (zone apex), кореневим доменом (root domain) або голим доменом (naked domain, apex domain). Традиційно на вершині зони не розміщували вебсайти: вона асоціювалася з самою організацією або її центральним маршрутизатором мережі.
kostyl.dev вершиною зони є саме ім'я kostyl.dev, тоді як www.kostyl.dev є дочірнім вузлом (піддоменом).Архітектурна проблема: обмеження стандарту RFC 1034 щодо записів CNAME
Коли веб став домінуючим сервісом в інтернеті, виникло закономірне бажання зробити URL-адреси простішими та вводити в браузері просто company.com замість довгого www.company.com. Проте інженери зіткнулися з жорстким технічним бар'єром, закладеним у базових стандартах DNS.
Згідно зі специфікацією RFC 1034 (розділ 3.6.2), ухваленою ще в 1987 році:
«Якщо вузол дерева DNS містить запис типу CNAME, ніякі інші ресурсні записи будь-якого типу для цього вузла існувати не можуть».
Це правило є абсолютно логічним з точки зору резолвера: запис CNAME є канонічним аліасом (покажчиком), який наказує клієнту повністю підставити інше доменне ім'я замість поточного. Якби поруч із CNAME дозволялися інші записи, виникала б неоднозначність, які саме дані повертати — власні чи з цільового домену.
Але тут криється фундаментальний конфлікт для вершини зони:
- За стандартами DNS будь-яка валідна зона на своїй вершині (
company.com) зобов'язана містити запис SOA (Start of Authority, який описує базові параметри зони, серійний номер і таймери) та щонайменше два записи NS (Name Server, які вказують на авторитетні сервери, що обслуговують цю зону). Без них делегування домену в кореневих серверах верхнього рівня (TLD) неможливе. - Крім того, майже будь-яка компанія має корпоративну електронну пошту виду
ceo@company.com. Це вимагає наявності записів MX безпосередньо на вершині зониcompany.com, а також записів TXT для політик безпеки SPF і DKIM.
Наслідок: ви не можете створити запис CNAME для вершини зони company.com, оскільки це автоматично суперечило б наявності обов'язкових записів SOA, NS та MX на цьому ж рівні! Будь-який DNS-сервер, налаштований таким чином, порушував би специфікацію RFC 1034, а доставка електронної пошти на домен або резолвінг самої зони повністю виходили б з ладу.
Водночас для піддомену www.company.com жодного конфлікту немає: www не є вершиною зони, не має власних SOA та NS записів, і на нього не надсилають електронну пошту. Тому для www можна без будь-яких проблем створити запис CNAME:
www.company.com. 300 IN CNAME my-load-balancer.cloudprovider.com.
Хмарна революція та глухий кут зі статичними A-записами
Поки вебсервери розміщувалися на фізичних машинах із фіксованими IP-адресами, адміністратори могли обходитися простим рішенням: прописувати однакові A-записи (IPv4) як для company.com, так і для www.company.com:
company.com. 300 IN A 198.51.100.10
www.company.com. 300 IN A 198.51.100.10
Проте наприкінці 2000-х років індустрія перейшла до хмарних обчислень (Cloud Computing), платформ як послуги (PaaS) та глобальних мереж доставки контенту (Content Delivery Networks, CDN) — таких як AWS CloudFront / ELB, Cloudflare, Akamai, Fastly, Heroku, GitHub Pages, Vercel тощо.
У сучасній хмарній архітектурі виділені постійні IP-адреси відійшли в минуле:
- Балансувальники навантаження та точки присутності CDN (Points of Presence, PoP) використовують динамічні пули IP-адрес або маршрутизацію Anycast, які постійно масштабуються, змінюються при міграції трафіку або планових роботах провайдера.
- Замість статичної IP-адреси хмарний провайдер видає клієнту канонічне доменне ім'я, наприклад:
d123456abcdef.cloudfront.net,myapp.herokuapp.comабоcustom.cname.cloudflare.net. - Для піддомену
www.company.comце налаштовується миттєво через CNAME. Але як направити відвідувачів на цей самий хмарний балансувальник за адресоюcompany.com, якщо CNAME там суворо заборонений стандартом?
Утворений глухий кут змушував інженерів тримати спеціальні проміжні сервери з фіксованими IP-адресами виключно для виконання HTTP-редиректу з company.com на www.company.com. Це було надзвичайно неефективно, дорого та створювало єдину точку відмови (Single Point of Failure, SPoF).
Подолання обмежень: механізм CNAME Flattening (ALIAS / ANAME)
Перший справжній прорив у цій проблемі відбувся на рівні авторитетних DNS-провайдерів (зокрема Cloudflare, AWS Route 53, DNSimple, DNS Made Easy). Вони створили технологію, яка дістала назву сплющення CNAME (CNAME Flattening), або віртуальні записи типу ALIAS та ANAME.
Суть цієї технології полягає в перенесенні обробки псевдоніма на бік авторитетного DNS-сервера:
Крок 1. Конфігурація розширеного запису в панелі DNS
Власник домену вказує для вершини зони @ (тобто company.com) цільове хмарне ім'я, наприклад ALIAS my-app.edge.cdn.net.
Крок 2. Перехоплення запиту авторитетним DNS-сервером
Коли клієнтський або рекурсивний DNS-резолвер запитує A чи AAAA-запис для company.com, авторитетний DNS-сервер не віддає клієнту CNAME (що порушило б RFC). Натомість авторитетний сервер сам виконує внутрішній рекурсивний запит: звертається до DNS для резолвінгу імені my-app.edge.cdn.net.
Крок 3. Синтез стандартних ресурсних записів
Авторитетний сервер отримує реальні актуальні IP-адреси вузлів CDN (наприклад, 104.16.132.5 та 2606:4700::6810:8405) і динамічно синтезує стандартні A та AAAA записи для company.com на льоту.
Крок 4. Доставка валідної відповіді клієнту
Клієнт отримує звичайнісіньку відповідь із записами типу A/AAAA, наче домен мав статичні адреси. Стандарти RFC 1034 жодним чином не порушуються: у відповіді немає забороненого CNAME, а записи SOA, NS і MX на вершині зони продовжують функціонувати абсолютно автономно.
Новий офіційний стандарт IETF: DNS-записи HTTPS та SVCB (RFC 9460)
Хоча CNAME Flattening розв'язав проблему на практиці, він залишався специфічною функцією конкретних комерційних DNS-провайдерів і не був універсальним стандартом IETF. Ситуація змінилася в листопаді 2023 року, коли IETF після багаторічної розробки спільно з інженерами Apple, Google, Cloudflare та Mozilla опублікував офіційний стандарт RFC 9460, що визначає нові типи DNS-записів: SVCB (Service Binding) та його веб-спеціалізацію — запис типу HTTPS (тип 65).
Запис типу HTTPS вирішує проблему вершини зони офіційно на рівні самого протоколу DNS через два фундаментальні режими роботи:
- Режим аліасу (AliasMode, пріоритет
Priority = 0):
Дозволяє прямо на вершині зони вказати канонічне доменне ім'я іншого цільового хоста. На відміну від старого CNAME, наявність записуHTTPSзPriority = 0не конфліктує з іншими записами (SOA, NS, MX, TXT) на цьому ж вузлі:company.com. 300 IN HTTPS 0 www.company.com.
Сучасний браузер, надсилаючи паралельний запит на запис типу HTTPS (разом із запитами A/AAAA), розпізнає вказівкуAliasModeі негайно резолвить цільове ім'я без додаткових HTTP-редиректів. - Сервісний режим (ServiceMode, пріоритет
Priority > 0):
Передає клієнтові повний набір метаданих для встановлення з'єднання ще до надсилання першого TCP-пакета:company.com. 300 IN HTTPS 1 . ( alpn="h3,h2" ipv4hint=104.18.25.100,104.18.26.100 ipv6hint=2606:4700::6812:1964 ech="..." )
Цей запис повідомляє браузеру:alpn="h3,h2"— сервер підтримує сучасні протоколи HTTP/3 поверх QUIC та HTTP/2. Клієнт може одразу відправляти QUIC-пакети без попереднього «промацування» через HTTP/1.1.ipv4hint/ipv6hint— IP-адреси для підключення передаються прямо всередині HTTPS-відповіді, що усуває необхідність виконання окремих додаткових DNS-запитів типу A та AAAA.ech(Encrypted Client Hello) — публічний ключ для шифрування імені домену (SNI) на етапі TLS-рукостискання, що надійно приховує від інтернет-провайдерів та цензорів, який саме сайт відвідує користувач.
Роль протоколів HTTP та TLS: Virtual Hosting та Server Name Indication (SNI)
Здатність DNS спрямовувати запити до apex domain була лише половиною справи. Друга половина полягала в тому, щоб сам вебсервер міг зрозуміти, який саме домен запитує клієнт, і надати коректний SSL/TLS сертифікат.
У ранньому протоколі HTTP/1.0 клієнт відкривав TCP-з'єднання з IP-адресою і надсилав запит виду:
GET /index.html HTTP/1.0
У цьому запиті не було вказано, до якого саме доменного імені звертається браузер. Якщо на одній IP-адресі розташовувалися сайти company.com та other.org, сервер не міг знати, яку сторінку повертати. Тому кожен домен вимагав окремої виділеної IP-адреси.
Цю проблему розв'язали два стандарти:
- Заголовок
Hostв HTTP/1.1 (RFC 2616): заголовок став обов'язковим для всіх HTTP-запитів:Це дало початок технології віртуального хостингу на основі імен (name-based virtual hosting). Один вебсервер (Nginx, Apache, Caddy) відтепер міг обслуговувати тисячі різних сайтів на одній-єдиній IP-адресі, спрямовуючи запит до потрібного віртуального блокуGET /index.html HTTP/1.1 Host: company.comserverза значенням заголовкаHost. - Розширення SNI для TLS (RFC 6066): коли інтернет перейшов на шифрування HTTPS, виникла нова перешкода. Оскільки TLS-рукостискання відбувається до того, як клієнт передасть зашифрований HTTP-заголовок
Host, вебсервер не знав, який SSL-сертифікат пред'явити клієнту при встановленні зашифрованого каналу. Стандарт SNI (Server Name Indication) додав можливість передавати ім'я потрібного домену у відкритому полі розширенняClientHelloна першому кроці TLS-з'єднання. Завдяки SNI сервер віддає правильний сертифікат (наприклад, мультидоменний SAN або wildcard*.company.com, що охоплює якcompany.com, так іwww.company.com).
Сучасна інженерна практика: канонізація доменів та налаштування перенаправлень
Хоча сьогодні завдяки CNAME Flattening та запису HTTPS вебсайт чудово відкривається як за адресою https://company.com, так і за адресою https://www.company.com, дозволяти сайту одночасно жити за обома адресами вважається грубою архітектурною помилкою.
Існує дві критичні причини, чому трафік слід примусово зводити до одного канонічного варіанта:
- SEO та уникнення дублювання сторінок: пошукові роботи (Googlebot) сприймають
company.comтаwww.company.comяк два абсолютно різні вебсайти. Якщо обидва повертають однаковий контент із кодом200 OK, виникає штраф за дублювання вмісту (duplicate content penalty), а посилальна вага сторінок розмивається навпіл. - Безпека та ізоляція файлів Cookie (Cookie Scoping): це найважливіший аспект безпеки бекенд-розробки:
- Якщо вебзастосунок на
company.comвстановлює Cookie без суворого обмеження або з атрибутомDomain=company.com, ці Cookie автоматично надсилатимуться браузером до всіх піддоменів:api.company.com,admin.company.com,staging.company.com. Це може призвести до ненавмисного витоку сесійних токенів у разі компрометації будь-якого допоміжного піддомену, а також збільшує розмір заголовків кожного мережевого запиту (наприклад, до статичних CDN-ресурсівstatic.company.com). - Якщо ж основним доменом обрано
www.company.com, файли Cookie встановлюються виключно для вузлаwwwі за замовчуванням фізично ізольовані від інших піддоменів системи.
- Якщо вебзастосунок на
- Для невеликих проєктів, портфоліо, лендінгів, сайтів документації та SaaS-сервісів стандартом став apex domain (
kostyl.dev,github.com) через його візуальну естетичність, зручність запам'ятовування та лаконічність. - Для масштабних корпоративних порталів (як-от
www.google.com,www.facebook.com) часто зберігають www саме через тонкий контроль над ізоляцією Cookie від численних глобальних піддоменів та оптимізацію передачі статичного контенту.
Для реалізації канонізації на рівні зворотного проксі (reverse proxy) або вебсервера налаштовують постійне перенаправлення з HTTP-кодом 301 Moved Permanently або сучасним 308 Permanent Redirect (який суворо зберігає початковий HTTP-метод запиту):
# 1. Перенаправлення з www на канонічний голий домен
server {
listen 80;
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 301 перенаправлення зі збереженням повного шляху та параметрів запиту
return 301 https://example.com$request_uri;
}
# 2. Основний сервер застосунку
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# У вебсервері Caddy автоматичне отримання сертифікатів
# та канонічний редирект налаштовуються надзвичайно лаконічно:
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
reverse_proxy localhost:8080
}
package main
import (
"net/http"
"strings"
)
// CanonicalHostMiddleware гарантує, що всі запити спрямовуються на єдине канонічне ім'я хоста
func CanonicalHostMiddleware(canonicalHost string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
// Відкидаємо порт, якщо запит містить host:port (наприклад, :8080)
if colonIdx := strings.IndexByte(host, ':'); colonIdx != -1 {
host = host[:colonIdx]
}
// Якщо хост не відповідає канонічному (наприклад, прийшов www.example.com замість example.com)
if !strings.EqualFold(host, canonicalHost) {
targetURL := "https://" + canonicalHost + r.URL.RequestURI()
http.Redirect(w, r, targetURL, http.StatusMovedPermanently) // HTTP 301
return
}
next.ServeHTTP(w, r)
})
}
https://) та піддомен www з метою спрощення сприйняття та захисту користувачів від фішингу. Для більшості пересічних людей www не несе корисної інформації, а довгий рядок відволікає увагу від справжнього зареєстрованого домену другого рівня. Приховування другорядних префіксів допомагає користувачеві швидше помітити потенційну підміну домену в атаках соціальної інженерії (наприклад, різницю між paypal.com та paypal.com.fraud-site.ru). При подвійному кліку по адресному рядку браузер завжди розгортає повне канонічне ім'я з усіма префіксами.evil.com спробує встановити заголовок Set-Cookie: token=123; Domain=com, це дозволило б перехоплювати або перезаписувати cookie для всіх доменів зони .com. Браузери звіряються зі списком публічних суфіксів і забороняють встановлювати cookie на рівні TLD (наприклад, .com, .org) або складових національних доменів (.kiev.ua, .co.uk), дозволяючи це лише для зареєстрованих організаційних доменів та їхніх прямих піддоменів.Програмний DNS-резолвінг у Go через пакет net.Resolver
Стандартна бібліотека Go надає потужний та гнучкий інструментарій для виконання DNS-запитів через пакет net, зокрема структуру net.Resolver. На відміну від простих функцій на кшталт net.LookupHost, які використовують системний резолвер операційної системи, net.Resolver дозволяє налаштувати власні DNS-сервери, таймаути, контексти скасування та детально контролювати процес резолвінгу.
Базовий приклад: резолвінг A-записів
Розглянемо найпростіший випадок — отримання IPv4-адреси для доменного імені:
package main
import (
"context"
"fmt"
"log"
"net"
"time"
)
func main() {
// Створюємо резолвер за замовчуванням (використовує системні налаштування DNS)
resolver := &net.Resolver{}
// Контекст із таймаутом 5 секунд
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Виконуємо резолвінг доменного імені
hostname := "www.example.com"
addrs, err := resolver.LookupHost(ctx, hostname)
if err != nil {
log.Fatalf("Помилка DNS-резолвінгу для %s: %v", hostname, err)
}
fmt.Printf("DNS-записи для %s:\n", hostname)
for _, addr := range addrs {
fmt.Printf(" - %s\n", addr)
}
}
Вихід програми:
DNS-записи для www.example.com:
- 93.184.216.34
- 2606:2800:220:1:248:1893:25c8:1946
Зверніть увагу, що LookupHost повертає як IPv4, так і IPv6 адреси (якщо обидва типи записів існують). Це зручно для реалізації dual-stack підтримки в клієнтських застосунках.
Використання кастомного DNS-сервера
Часто у production-середовищах потрібно використовувати конкретні DNS-сервери замість системних (наприклад, внутрішній корпоративний DNS або публічні резолвери Cloudflare/Google для кращої продуктивності). net.Resolver дозволяє налаштувати це через поле Dial:
package main
import (
"context"
"fmt"
"log"
"net"
"time"
)
func main() {
// Створюємо резолвер із кастомним DNS-сервером (Google Public DNS 8.8.8.8)
resolver := &net.Resolver{
PreferGo: true, // Використовувати власну реалізацію Go замість системної
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 3 * time.Second,
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hostname := "www.google.com"
addrs, err := resolver.LookupHost(ctx, hostname)
if err != nil {
log.Fatalf("Помилка резолвінгу: %v", err)
}
fmt.Printf("Адреси для %s (через DNS 8.8.8.8):\n", hostname)
for _, addr := range addrs {
fmt.Printf(" - %s\n", addr)
}
}
internal-api.company.local), недоступні через публічні резолвери. У таких випадках ваш Go-застосунок повинен бути налаштований на використання корпоративного DNS. Крім того, публічні резолвери на кшталт Cloudflare (1.1.1.1) або Google (8.8.8.8) часто працюють швидше за DNS інтернет-провайдерів та підтримують сучасні стандарти безпеки (DNS-over-HTTPS).Запит конкретних типів DNS-записів
Іноді потрібно отримати не просто IP-адреси, а конкретні типи записів: MX для налаштування поштового клієнта, TXT для перевірки SPF-політик, SRV для виявлення мікросервісів. net надає спеціалізовані методи для кожного типу:
package main
import (
"context"
"fmt"
"log"
"net"
)
func main() {
resolver := &net.Resolver{}
ctx := context.Background()
domain := "example.com"
// Запит MX-записів (поштові сервери)
mxRecords, err := resolver.LookupMX(ctx, domain)
if err != nil {
log.Fatalf("Помилка запиту MX: %v", err)
}
fmt.Printf("MX-записи для %s:\n", domain)
for _, mx := range mxRecords {
fmt.Printf(" Пріоритет %d: %s\n", mx.Pref, mx.Host)
}
// Запит TXT-записів (SPF, DKIM, верифікація)
txtRecords, err := resolver.LookupTXT(ctx, domain)
if err != nil {
log.Fatalf("Помилка запиту TXT: %v", err)
}
fmt.Printf("\nTXT-записи для %s:\n", domain)
for _, txt := range txtRecords {
fmt.Printf(" - %s\n", txt)
}
}
Приклад виходу:
MX-записи для example.com:
Пріоритет 10: mail1.example.com.
Пріоритет 20: mail2.example.com.
TXT-записи для example.com:
- v=spf1 include:_spf.google.com ~all
- google-site-verification=rXOxyZounnZasA8Z7oaD3c14JdjS9aKSWvsR1EbUSIQ
Обробка помилок DNS та діагностика проблем
Одна з найважливіших навичок при роботі з DNS — це вміння коректно інтерпретувати помилки та відрізняти тимчасові збої від постійних. Пакет net у Go повертає типізовані помилки, які можна перевірити через механізм errors.As:
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"time"
)
func main() {
resolver := &net.Resolver{}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
hostname := "nonexistent-domain-12345.com"
addrs, err := resolver.LookupHost(ctx, hostname)
if err != nil {
// Перевіряємо конкретний тип помилки
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.IsNotFound {
// Домен не існує (NXDOMAIN) — постійна помилка
log.Printf("❌ Домен %s не існує в DNS. Перевірте правильність імені.", hostname)
} else if dnsErr.IsTimeout {
// Таймаут DNS-запиту — тимчасова помилка, можна повторити
log.Printf("⏱️ Таймаут DNS-запиту для %s. Спробуйте пізніше.", hostname)
} else if dnsErr.IsTemporary {
// Інша тимчасова помилка (наприклад, SERVFAIL)
log.Printf("⚠️ Тимчасова помилка DNS для %s: %v", hostname, dnsErr)
} else {
// Постійна помилка іншого характеру
log.Printf("🚫 Постійна помилка DNS для %s: %v", hostname, dnsErr)
}
} else if errors.Is(err, context.DeadlineExceeded) {
log.Printf("⏱️ Контекст вичерпав таймаут під час DNS-резолвінгу %s", hostname)
} else {
log.Printf("Невідома помилка: %v", err)
}
return
}
fmt.Printf("Успішний резолвінг: %v\n", addrs)
}
Кешування DNS-відповідей на рівні застосунку
Хоча операційна система та рекурсивні резолвери кешують DNS-відповіді, іноді корисно реалізувати власний кеш на рівні застосунку, особливо якщо ваш сервіс виконує тисячі DNS-запитів на секунду до одних і тих самих доменів. Це зменшує латентність та навантаження на мережу:
package main
import (
"context"
"fmt"
"net"
"sync"
"time"
)
// Проста структура для кешування DNS-адрес
type DNSCache struct {
mu sync.RWMutex
entries map[string]cacheEntry
}
type cacheEntry struct {
addrs []string
expiresAt time.Time
}
func NewDNSCache() *DNSCache {
return &DNSCache{
entries: make(map[string]cacheEntry),
}
}
func (c *DNSCache) Lookup(ctx context.Context, hostname string, ttl time.Duration) ([]string, error) {
// Спочатку перевіряємо кеш
c.mu.RLock()
entry, exists := c.entries[hostname]
c.mu.RUnlock()
if exists && time.Now().Before(entry.expiresAt) {
fmt.Printf("✅ Кеш: повертаємо адреси для %s з кешу\n", hostname)
return entry.addrs, nil
}
// Якщо в кеші немає або TTL вичерпано — виконуємо реальний DNS-запит
fmt.Printf("🔍 DNS: виконуємо резолвінг для %s\n", hostname)
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, hostname)
if err != nil {
return nil, err
}
// Зберігаємо в кеш
c.mu.Lock()
c.entries[hostname] = cacheEntry{
addrs: addrs,
expiresAt: time.Now().Add(ttl),
}
c.mu.Unlock()
return addrs, nil
}
func main() {
cache := NewDNSCache()
ctx := context.Background()
// Перший запит — виконується реальний DNS-резолвінг
addrs1, _ := cache.Lookup(ctx, "www.example.com", 10*time.Second)
fmt.Printf("Адреси: %v\n\n", addrs1)
// Другий запит через 2 секунди — береться з кешу
time.Sleep(2 * time.Second)
addrs2, _ := cache.Lookup(ctx, "www.example.com", 10*time.Second)
fmt.Printf("Адреси: %v\n\n", addrs2)
// Третій запит через 12 секунд — TTL вичерпано, виконується новий резолвінг
time.Sleep(12 * time.Second)
addrs3, _ := cache.Lookup(ctx, "www.example.com", 10*time.Second)
fmt.Printf("Адреси: %v\n", addrs3)
}
Протокол SMTP: основа архітектури електронної пошти
Тепер, коли ми глибоко розібралися з системою доменних імен, перейдемо до другого фундаментального протоколу прикладного рівня — SMTP (Simple Mail Transfer Protocol). Цей протокол, стандартизований у 1982 році в RFC 821 (пізніше оновлений у RFC 5321), є основою архітектури електронної пошти в інтернеті. Саме завдяки SMTP мільярди листів щодня доставляються між поштовими серверами по всьому світу.
На відміну від DNS, який працює переважно через UDP і оптимізований для швидких запитів-відповідей малого розміру, SMTP є текстовим протоколом, що працює поверх надійного TCP-з'єднання на порту 25 (для передачі між серверами) або портах 587 (для відправки від клієнта до сервера з автентифікацією) і 465 (застарілий порт для SMTP-over-TLS).
Архітектура поштової системи: від відправника до отримувача
Перш ніж заглиблюватися в технічні деталі протоколу, важливо зрозуміти загальну архітектуру електронної пошти, яка складається з кількох окремих компонентів:
- MUA (Mail User Agent) — поштовий клієнт користувача (Outlook, Thunderbird, Gmail у браузері, мобільний застосунок на React Native). Це те, через що ви пишете та читаєте листи.
- MSA (Mail Submission Agent) — сервер, який приймає листи від MUA для відправки. Зазвичай це той самий поштовий сервер, але який працює на порту 587 і вимагає автентифікації (логін/пароль).
- MTA (Mail Transfer Agent) — поштовий сервер, який передає листи між серверами через порт 25. Саме MTA виконують DNS-запити типу MX, щоб дізнатися, на який сервер доставляти лист для конкретного домену.
- MDA (Mail Delivery Agent) — компонент, який доставляє лист у поштову скриньку отримувача на сервері. Користувач потім завантажує листи через протоколи IMAP (порт 143/993) або POP3 (порт 110/995).
Чому SMTP розділений на окремі етапи
Розділення на MSA (submission) та MTA (relay) може здатися надмірним ускладненням, але воно вирішує важливі проблеми безпеки:
- MSA (порт 587) вимагає автентифікації перед відправкою листа. Це запобігає використанню поштового сервера для масового розсилання спаму з випадкових IP-адрес.
- MTA (порт 25) працює без автентифікації (бо поштові сервери довіряють один одному в межах глобальної інфраструктури інтернету), але застосовує інші механізми захисту: перевірку SPF-записів, валідацію DKIM-підписів, аналіз репутації відправника.
Структура SMTP-сеансу: команди та коди відповідей
Протокол SMTP побудований як діалог між клієнтом (відправником) і сервером (отримувачем), де кожна сторона по черзі надсилає текстові команди та відповіді. Усі команди та відповіді є читабельними ASCII-рядками, завершеними символами \r\n (carriage return + line feed), що робить протокол надзвичайно зручним для налагодження вручну через telnet.
Основні команди SMTP
- HELO / EHLO (Extended Hello) — перше повідомлення клієнта після встановлення TCP-з'єднання. Клієнт представляється, вказуючи своє доменне ім'я або IP-адресу.
EHLO mail.example.com - MAIL FROM — вказує адресу відправника (так званий «конверт» листа, який може відрізнятися від поля
From:у заголовках листа).MAIL FROM:<alice@example.com> - RCPT TO — вказує адресу отримувача. Можна надіслати кілька команд RCPT TO, щоб доставити один лист кільком отримувачам.
RCPT TO:<bob@recipient.com> - DATA — сигналізує початок передачі тіла листа (заголовки + текст повідомлення). Після цієї команди клієнт надсилає багаторядковий текст, завершуючи його рядком із однією крапкою
.на окремому рядку.DATA Subject: Тестовий лист From: alice@example.com To: bob@recipient.com Це тіло листа. . - QUIT — завершує SMTP-сеанс і закриває TCP-з'єднання.
QUIT - STARTTLS — оновлює з'єднання з відкритого текстового на шифроване TLS. Після успішного TLS-рукостискання клієнт повинен знову надіслати EHLO.
STARTTLS
Коди відповідей SMTP
Кожна команда клієнта отримує у відповідь тризначний числовий код із текстовим описом. Перша цифра коду вказує на категорію відповіді:
- 2xx — успіх (команда прийнята і виконана).
- 220 — сервер готовий (привітання при з'єднанні).
- 250 — команда успішно виконана.
- 221 — сервер закриває з'єднання (відповідь на QUIT).
- 3xx — проміжний успіх (потрібна додаткова інформація).
- 354 — почніть передачу тіла листа (відповідь на DATA).
- 4xx — тимчасова помилка (спробуйте пізніше).
- 421 — сервіс недоступний, з'єднання буде закрито.
- 450 — поштова скринька тимчасово недоступна.
- 5xx — постійна помилка (операція неможлива, повтор безглуздий).
- 550 — поштова скринька не існує або доступ заборонено.
- 552 — перевищено квоту зберігання.
- 554 — транзакція відхилена (часто через антиспам-фільтри).
Повна діаграма типового SMTP-сеансу
Формат MIME: багатокомпонентні листи та вкладення файлів
Оригінальний стандарт SMTP, визначений у RFC 821, передбачав передачу лише простого ASCII-тексту без можливості включення зображень, документів, HTML-розмітки чи символів національних алфавітів (кирилиця, ієрогліфи тощо). Але вже на початку 1990-х років стало очевидно, що електронна пошта має підтримувати багатий контент. Саме тому був розроблений стандарт MIME (Multipurpose Internet Mail Extensions, RFC 2045–2049), який розширив можливості формату електронних листів, не змінюючи сам протокол SMTP.
Основні концепції MIME
MIME вводить поняття заголовків типу контенту (Content-Type) та кодування (Content-Transfer-Encoding), які дозволяють передавати через текстовий протокол бінарні дані (зображення, PDF-файли, аудіо) та текст у будь-яких кодуваннях (UTF-8 для кирилиці, Shift-JIS для японської мови тощо).
Ключові MIME-заголовки:
- Content-Type — вказує тип вмісту частини листа:
text/plain; charset=utf-8— простий текст у кодуванні UTF-8.text/html; charset=utf-8— HTML-розмітка (для красивих листів із форматуванням).image/jpeg— зображення JPEG.application/pdf— PDF-документ.multipart/mixed— лист складається з кількох частин (наприклад, текст + вкладення).multipart/alternative— кілька альтернативних версій того самого контенту (наприклад, plain text і HTML).
- Content-Transfer-Encoding — спосіб кодування бінарних даних у текстову форму:
7bit— чистий ASCII (типово для простого тексту).quoted-printable— кодування для тексту з небагатьма не-ASCII символами (економить місце порівняно з Base64).base64— бінарне кодування, де кожні 3 байти вихідних даних перетворюються на 4 символи ASCII (збільшення розміру на ~33%, але універсальне для будь-яких бінарних файлів).
- Content-Disposition — вказує, як обробляти частину:
inline— відобразити вміст безпосередньо в листі (для вбудованих зображень).attachment; filename="document.pdf"— вкладений файл, який потрібно завантажити окремо.
Структура багатокомпонентного листа (Multipart)
Коли лист містить кілька частин (наприклад, текстова версія + HTML-версія + вкладений файл), він оформляється як multipart/mixed або multipart/alternative. Кожна частина відокремлюється унікальним boundary (розділювачем) — довільним рядком, який не зустрічається в жодній із частин.
Приклад структури MIME-листа з текстом і PDF-вкладенням:
From: alice@example.com
To: bob@recipient.com
Subject: Звіт за квартал
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Part_12345"
------=_Part_12345
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Доброго дня, Боб!
Надсилаю звіт за третій квартал у вкладенні.
З повагою,
Аліса
------=_Part_12345
Content-Type: application/pdf; name="report_Q3.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report_Q3.pdf"
JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAw
... (багато рядків Base64-коду) ...
RU9GCg==
------=_Part_12345--
Зверніть увагу:
- Заголовок
Content-Type: multipart/mixed; boundary="----=_Part_12345"оголошує, що лист складається з кількох частин, розділених рядком------=_Part_12345. - Кожна частина має власні заголовки
Content-Type,Content-Transfer-EncodingтаContent-Disposition. - Останній розділювач завершується двома дефісами
--, що сигналізує кінець багатокомпонентної структури.
multipart/alternative — клієнт вибирає кращу для себе версію. Якщо ж лист містить різний контент (текст листа + вкладений файл), використовується multipart/mixed — клієнт відображає всі частини.Кодування Base64 для вкладень
Бінарні файли (зображення, документи, архіви) не можна передати через текстовий SMTP-протокол напряму, тому вони кодуються в Base64 — алгоритм, який перетворює довільні байти в набір символів A-Z, a-z, 0-9, +, /. У Go кодування Base64 є частиною стандартної бібліотеки:
package main
import (
"encoding/base64"
"fmt"
"os"
)
func main() {
// Читаємо бінарний файл
fileData, err := os.ReadFile("example.pdf")
if err != nil {
panic(err)
}
// Кодуємо в Base64
encoded := base64.StdEncoding.EncodeToString(fileData)
fmt.Printf("Розмір оригіналу: %d байтів\n", len(fileData))
fmt.Printf("Розмір після Base64: %d байтів\n", len(encoded))
fmt.Printf("Збільшення: %.1f%%\n", (float64(len(encoded))/float64(len(fileData))-1)*100)
// Виводимо перші 100 символів закодованого вмісту
fmt.Printf("Початок закодованих даних:\n%s...\n", encoded[:100])
}
Типовий вихід:
Розмір оригіналу: 245760 байтів
Розмір після Base64: 327680 байтів
Збільшення: 33.3%
Початок закодованих даних:
JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAwIFIKL01lZGlhQm94IFswIDAgNjEy...
Механізми захисту електронної пошти: SPF, DKIM, DMARC та STARTTLS
Протокол SMTP у своїй оригінальній формі не передбачав жодних механізмів автентифікації відправника: будь-який сервер міг видати себе за будь-який домен, що призвело до епідемії спаму та фішингу. Протягом останніх двох десятиліть було розроблено кілька стандартів, які закривають ці прогалини безпеки.

SPF (Sender Policy Framework)
SPF — це TXT-запис у DNS, який визначає, які IP-адреси мають право надсилати пошту від імені домену. Коли поштовий сервер отримує лист, він виконує DNS-запит типу TXT для домену відправника та перевіряє, чи IP-адреса відправника входить до дозволеного списку.
Приклад SPF-запису:
example.com. IN TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.google.com ~all"
Розшифровка:
v=spf1— версія SPF.ip4:192.0.2.0/24— дозволена підмережа IPv4.include:_spf.google.com— дозволити IP-адреси, вказані в SPF-записі Google (якщо компанія використовує Gmail для корпоративної пошти).~all— soft fail для всіх інших адрес (позначити як підозріле, але не відхиляти одразу).
DKIM (DomainKeys Identified Mail)
DKIM додає до кожного відправленого листа цифровий підпис, створений закритим ключем домену. Підпис обчислюється для заголовків та тіла листа і додається як заголовок DKIM-Signature. Отримуючий сервер завантажує відкритий ключ домену з DNS (TXT-запис) і перевіряє підпис.
Приклад DKIM-підпису в листі:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=example.com; s=default;
h=from:to:subject:date:message-id;
bh=2jmj7l5rSw0yVb/vlWAYkK/YBwk=;
b=dzdVyOf8SdaTK4N...
TXT-запис з відкритим ключем DKIM:
default._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3..."
DKIM гарантує, що лист дійсно надісланий із серверів домену example.com і не був модифікований у дорозі.
DMARC (Domain-based Message Authentication, Reporting and Conformance)
DMARC об'єднує SPF та DKIM у єдину політику та визначає, що робити з листами, які не пройшли перевірку. Це теж TXT-запис у DNS.
Приклад DMARC-запису:
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com"
Розшифровка:
v=DMARC1— версія протоколу.p=quarantine— політика: помістити листи, які не пройшли перевірку, у папку «Спам».p=reject— більш жорстка політика: відхилити такі листи повністю.rua=mailto:...— адреса для отримання агрегованих звітів про перевірки.
STARTTLS — шифрування SMTP-каналу
За замовчуванням SMTP передає дані (включно з паролями автентифікації та вмістом листів) у відкритому вигляді. Команда STARTTLS дозволяє оновити з'єднання до шифрованого TLS після початкового рукостискання. Це обов'язкова практика для портів 587 (submission) і бажана для порту 25 (relay).
Програмне надсилання листів через Go: пакет net/smtp
Стандартна бібліотека Go надає пакет net/smtp, який реалізує клієнтську частину протоколу SMTP і дозволяє надсилати електронні листи з Go-застосунків без зовнішніх залежностей. Розглянемо практичні приклади від найпростішого до повноцінного з MIME-вкладеннями.
Базовий приклад: відправка простого текстового листа
package main
import (
"fmt"
"log"
"net/smtp"
)
func main() {
// Налаштування SMTP-сервера (приклад з Gmail)
smtpHost := "smtp.gmail.com"
smtpPort := "587"
senderEmail := "your-email@gmail.com"
senderPassword := "your-app-password" // Використовуйте App Password, не основний пароль!
// Дані листа
from := senderEmail
to := []string{"recipient@example.com"}
subject := "Тестовий лист з Go"
body := "Це простий текстовий лист, надісланий через пакет net/smtp."
// Формуємо повідомлення (заголовки + тіло)
message := []byte(
"From: " + from + "\r\n" +
"To: " + to[0] + "\r\n" +
"Subject: " + subject + "\r\n" +
"\r\n" +
body + "\r\n",
)
// Автентифікація PLAIN
auth := smtp.PlainAuth("", senderEmail, senderPassword, smtpHost)
// Надсилаємо лист
err := smtp.SendMail(
smtpHost+":"+smtpPort,
auth,
from,
to,
message,
)
if err != nil {
log.Fatalf("Помилка надсилання листа: %v", err)
}
fmt.Println("✅ Лист успішно надіслано!")
}
os.Getenv("SMTP_PASSWORD")) або секрети з систем керування конфігурацією (AWS Secrets Manager, HashiCorp Vault). Для Gmail обов'язково створіть App Password у налаштуваннях облікового запису, а не використовуйте основний пароль.Розширений приклад: HTML-лист із вкладенням
Для формування складних MIME-листів зручно використовувати допоміжні функції. Ось повноцінний приклад із HTML-версією та PDF-вкладенням:
package main
import (
"encoding/base64"
"fmt"
"log"
"net/smtp"
"os"
"strings"
)
func main() {
smtpHost := "smtp.gmail.com"
smtpPort := "587"
senderEmail := "your-email@gmail.com"
senderPassword := "your-app-password"
from := senderEmail
to := []string{"recipient@example.com"}
subject := "Звіт за квартал"
// Читаємо вкладений файл
attachmentPath := "report.pdf"
attachmentData, err := os.ReadFile(attachmentPath)
if err != nil {
log.Fatalf("Не вдалося прочитати вкладення: %v", err)
}
// Формуємо MIME-лист
boundary := "----=_Part_Boundary_12345"
var messageBuilder strings.Builder
messageBuilder.WriteString(fmt.Sprintf("From: %s\r\n", from))
messageBuilder.WriteString(fmt.Sprintf("To: %s\r\n", to[0]))
messageBuilder.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
messageBuilder.WriteString("MIME-Version: 1.0\r\n")
messageBuilder.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", boundary))
messageBuilder.WriteString("\r\n")
// Частина 1: HTML-текст
messageBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
messageBuilder.WriteString("Content-Type: text/html; charset=utf-8\r\n")
messageBuilder.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
messageBuilder.WriteString("\r\n")
messageBuilder.WriteString("<html><body>\r\n")
messageBuilder.WriteString("<h1>Доброго дня!</h1>\r\n")
messageBuilder.WriteString("<p>Надсилаю звіт за третій квартал у вкладенні.</p>\r\n")
messageBuilder.WriteString("<p>З повагою,<br><strong>Аліса</strong></p>\r\n")
messageBuilder.WriteString("</body></html>\r\n")
messageBuilder.WriteString("\r\n")
// Частина 2: PDF-вкладення
messageBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
messageBuilder.WriteString("Content-Type: application/pdf; name=\"report.pdf\"\r\n")
messageBuilder.WriteString("Content-Transfer-Encoding: base64\r\n")
messageBuilder.WriteString("Content-Disposition: attachment; filename=\"report.pdf\"\r\n")
messageBuilder.WriteString("\r\n")
// Кодуємо вкладення в Base64 і розбиваємо на рядки по 76 символів
encoded := base64.StdEncoding.EncodeToString(attachmentData)
for i := 0; i < len(encoded); i += 76 {
end := i + 76
if end > len(encoded) {
end = len(encoded)
}
messageBuilder.WriteString(encoded[i:end] + "\r\n")
}
// Завершальний boundary
messageBuilder.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
message := []byte(messageBuilder.String())
// Надсилаємо
auth := smtp.PlainAuth("", senderEmail, senderPassword, smtpHost)
err = smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, message)
if err != nil {
log.Fatalf("Помилка надсилання: %v", err)
}
fmt.Println("✅ HTML-лист із вкладенням успішно надіслано!")
}
Використання сторонніх бібліотек для роботи з MIME: пакет jordan-wright/email
Стандартний пакет net/smtp у Go є транспортним клієнтом низького рівня: він відповідає виключно за встановлення TCP-з'єднання, проходження TLS-рукостискання та відправку сирих рядків протоколу SMTP. Сама стандартна бібліотека свідомо не містить високорівневого конструктора MIME-повідомлень. У попередньому прикладі ми власноруч генерували унікальні boundary, вручну прописували службові заголовки Content-Type, обчислювали Base64 і розбивали його на рядки фіксованої довжини по 76 символів згідно з RFC 2045.
У промисловій розробці писати такий низькорівневий код вручну недоцільно та ризиковано: найменша помилка в розділювачах \r\n, пропущений дефіс чи відсутність кодування кириличних символів у темі листа за стандартом RFC 2047 призведуть до того, що поштовий клієнт (Gmail або Outlook) відобразить лист як пошкоджений або надішле вкладення у вигляді нечитабельного тексту.
В екосистемі Go історично склалося кілька рішень:
gopkg.in/gomail.v2— тривалий час був найвідомішим інструментом, проте проєкт є непідтримуваним (deprecated / unmaintained) ще з 2016 року, не розвивається під сучасні версії Go та не враховує нові стандарти.github.com/jordan-wright/email— найпопулярніша, перевірена часом та елегантна бібліотека в сучасному Go. Вона розроблена як легковагова обгортка над стандартними пакетамиnet/smtpтаnet/mail, яка бере на себе всю складність формування валідних багатокомпонентних MIME-повідомлень, зберігаючи при цьому повну сумісність зі стандартним інтерфейсомsmtp.Auth.
go get -u github.com/jordan-wright/email
Розглянемо, як той самий сценарій надсилання HTML-листа з вкладеним PDF-файлом реалізується за допомогою бібліотеки jordan-wright/email:
package main
import (
"fmt"
"log"
"net/smtp"
"github.com/jordan-wright/email"
)
func main() {
// Параметри підключення до SMTP-сервера
smtpHost := "smtp.gmail.com"
smtpPort := "587"
senderEmail := "your-email@gmail.com"
senderPassword := "your-app-password" // Пароль застосунку (App Password)
// Створюємо новий об'єкт повідомлення
e := email.NewEmail()
// Налаштовуємо адресні поля та тему
e.From = fmt.Sprintf("Аліса <%s>", senderEmail)
e.To = []string{"recipient@example.com"}
e.Subject = "Звіт за квартал"
// Текстова альтернатива (для поштових клієнтів без підтримки HTML)
e.Text = []byte("Доброго дня!\n\nНадсилаю звіт за третій квартал у вкладенні.\n\nЗ повагою,\nАліса")
// Основне тіло листа у форматі HTML
e.HTML = []byte(`
<html>
<body>
<h1>Доброго дня!</h1>
<p>Надсилаю звіт за третій квартал у вкладенні.</p>
<p>З повагою,<br><strong>Аліса</strong></p>
</body>
</html>
`)
// Прикріплюємо файл звіту
// Бібліотека автоматично визначає MIME-тип, кодує дані в Base64
// та генерує коректні заголовки Content-Disposition і Content-Transfer-Encoding
_, err := e.AttachFile("report.pdf")
if err != nil {
log.Fatalf("Не вдалося прикріпити файл report.pdf: %v", err)
}
// Автентифікація через стандартний пакет Go net/smtp
auth := smtp.PlainAuth("", senderEmail, senderPassword, smtpHost)
// Відправка повідомлення через SMTP із автоматичним STARTTLS
err = e.Send(smtpHost+":"+smtpPort, auth)
if err != nil {
log.Fatalf("Помилка надсилання листа: %v", err)
}
fmt.Println("✅ HTML-лист із вкладенням успішно надіслано через jordan-wright/email!")
}
jordan-wright/email:- Автоматична гібридна структура
multipart/alternative+multipart/mixed: якщо вказати одночасноe.Text,e.HTMLта вкладення черезe.AttachFile(), бібліотека створить правильне вкладене дерево MIME-розділювачів, де клієнт зможе вибрати між HTML і Text, а вкладення відобразяться окремими файлами. - Безпечне кодування UTF-8 заголовків: якщо поле
Subjectмістить кирилицю чи емодзі, бібліотека автоматично закодує його у відповідність до стандарту RFC 2047 (=?utf-8?b?...?=), що запобігає появі «знаків запитання» замість букв в Outlook або Apple Mail. - Пряма сумісність: метод
e.Send()приймає стандартну адресу хоста та стандартнийsmtp.Auth, не вимагаючи сторонніх конфігурацій з'єднання.
Локальне тестування SMTP: емулятори MailHog та Mailpit
Під час розробки та налагодження функціональності надсилання листів критично важливо мати можливість перевіряти роботу без фактичної відправки листів у реальні поштові скриньки (що може призвести до спаму, випадкового розголошення тестових даних або витрати квот на сервісах розсилки). Для цього існують локальні SMTP-емулятори, які приймають листи, зберігають їх у пам'яті та надають вебінтерфейс для перегляду.
Налаштування MailHog через Docker
MailHog — це легковаговий SMTP-сервер на Go, який ідеально підходить для розробки. Він слухає на порту 1025 для SMTP-з'єднань та надає вебінтерфейс на порту 8025 для перегляду отриманих листів.
Запуск через Docker:
docker run -d \
--name mailhog \
-p 1025:1025 \
-p 8025:8025 \
mailhog/mailhog
Налаштування Go-клієнта для MailHog:
package main
import (
"fmt"
"log"
"net/smtp"
)
func main() {
// MailHog слухає на localhost:1025 і НЕ вимагає автентифікації
smtpHost := "localhost"
smtpPort := "1025"
from := "test@example.com"
to := []string{"recipient@example.com"}
subject := "Тестовий лист через MailHog"
body := "Цей лист ніколи не буде надісланий у реальну мережу, але його можна переглянути в вебінтерфейсі."
message := []byte(
"From: " + from + "\r\n" +
"To: " + to[0] + "\r\n" +
"Subject: " + subject + "\r\n" +
"\r\n" +
body + "\r\n",
)
// Без автентифікації (auth = nil)
err := smtp.SendMail(smtpHost+":"+smtpPort, nil, from, to, message)
if err != nil {
log.Fatalf("Помилка: %v", err)
}
fmt.Println("✅ Лист надіслано в MailHog!")
fmt.Println("🌐 Переглянути: http://localhost:8025")
}
Після запуску програми відкрийте браузер на адресі http://localhost:8025 — ви побачите отриманий лист у красивому інтерфейсі з можливістю перегляду вихідного коду (включно з усіма MIME-заголовками), що надзвичайно зручно для налагодження.

Mailpit як сучасна заміна MailHog
Для більш вимогливих проєктів, інтеграційного тестування та щоденної розробки індустріальним стандартом став Mailpit — сучасний, швидкий та легковаговий емулятор електронної пошти на Go, створений як пряма заміна застарілому MailHog. Mailpit активно розвивається, зберігає історію листів у вбудованій базі даних SQLite, підтримує перевірку спам-рейтингу (SpamAssassin), має темну тему, повноцінне REST API та потужний веб-інтерфейс із вкладками перегляду HTML-розмітки, текстової версії, аналізу сирого MIME та попереднього перегляду вкладень (зображення, PDF тощо).
Запуск Mailpit через Docker:
docker run -d \
--name mailpit \
-p 1025:1025 \
-p 8025:8025 \
axllent/mailpit

Інтеграція DNS та SMTP у клієнт-серверній архітектурі
Тепер, коли ми детально розібрали обидва протоколи окремо, важливо усвідомити, як вони взаємодіють у реальних клієнт-серверних системах. Розглянемо типовий сценарій: мобільний застосунок на React Native дозволяє користувачеві скинути пароль через електронну пошту.
Архітектура системи скидання пароля
Ключові моменти інтеграції
- DNS є обов'язковим кроком перед SMTP: Go-бекенд не може просто «надіслати лист на gmail.com» — спочатку він виконує MX-запит, щоб дізнатися, який поштовий сервер відповідає за цей домен, потім виконує A/AAAA-запит для отримання IP-адреси цього сервера.
- Кешування DNS-відповідей: якщо ваш сервіс надсилає тисячі листів на день, критично важливо кешувати DNS-відповіді (як ми обговорювали раніше), щоб не виконувати окремий MX-запит для кожного листа.
- Обробка помилок на кожному рівні: DNS може повернути NXDOMAIN (домен не існує), SMTP-сервер може повернути 550 (поштова скринька не існує) або 421 (тимчасово недоступний). Ваш Go-бекенд повинен коректно інтерпретувати ці помилки та відповідно інформувати клієнтський застосунок.
- Асинхронна обробка: надсилання електронної пошти — це відносно повільна операція (встановлення TCP-з'єднання, TLS-рукостискання, передача даних), яка може зайняти кілька секунд. У production-системах листи зазвичай ставляться в чергу фонових завдань (наприклад, Redis Queue або RabbitMQ), а фоновий воркер обробляє їх асинхронно, не блокуючи HTTP-запит від клієнта.
Існує два підходи:
- Оптимістична верифікація: відправити лист із посиланням для підтвердження та дозволити користувачу користуватися сервісом обмежено до підтвердження. Якщо SMTP-сервер поверне 550 (поштова скринька не існує), записати це у лог і позначити облік ування як непідтверджений.
- Попередня перевірка через SMTP VRFY: деякі SMTP-сервери підтримують команду
VRFY, яка дозволяє перевірити існування поштової скриньки без фактичного надсилання листа. Проте багато сучасних серверів (включно з Gmail) відключили цю команду через ризики збору адрес спамерами, тому цей підхід не завжди працює.
Найкраща практика — комбінувати оптимістичну відправку з моніторингом bounce-повідомлень (листів про невдалу доставку), які надсилають SMTP-сервери у разі помилок.
Підсумки: ключові висновки та практичні рекомендації
У цій лекції ми детально розібрали два фундаментальні протоколи прикладного рівня — DNS та SMTP, які є невід'ємною частиною будь-якої сучасної клієнт-серверної архітектури. Підсумуємо найважливіші концепції та практичні поради для розробників.
DNS — критична інфраструктура, про яку легко забути
- DNS є набагато більшим, ніж просто «телефонна книга інтернету»: це розподілена база даних, яка зберігає метаінформацію про мережеві сервіси (MX-записи для пошти, SRV-записи для виявлення сервісів, TXT-записи для верифікації та політик безпеки).
- Завжди обробляйте DNS-помилки коректно: розрізняйте постійні помилки (NXDOMAIN — домен не існує) і тимчасові (SERVFAIL, таймаут). Використовуйте експоненційну затримку для повторних спроб у разі тимчасових збоїв.
- Кешуйте DNS-відповіді відповідально: поважайте TTL, встановлені адміністраторами доменів, але реалізуйте власний кеш на рівні застосунку для високонавантажених сценаріїв.
- Підтримуйте dual-stack (IPv4 + IPv6): сучасні застосунки повинні виконувати паралельні запити A та AAAA та намагатися підключитися через обидва протоколи (Happy Eyeballs).
SMTP — більше, ніж просто надсилання текстових повідомлень
- Розуміння архітектури пошти критично важливе: знання різниці між MSA (submission), MTA (relay) та MDA (delivery) допомагає діагностувати проблеми доставки та правильно налаштовувати інфраструктуру.
- Завжди використовуйте STARTTLS для захисту даних: передача паролів автентифікації та вмісту листів через незашифроване з'єднання є неприпустимою практикою в сучасних системах.
- Впроваджуйте SPF, DKIM та DMARC: без цих механізмів захисту ваші листи з високою ймовірністю потраплятимуть у спам або відхилятимуться поштовими серверами отримувачів.
- Формуйте MIME-повідомлення коректно: неправильна структура багатокомпонентних листів (відсутність boundary, некоректне кодування Base64, відсутність заголовків Content-Type) призводить до того, що листи відображаються як нечитабельний текст або вкладення не відкриваються.
- Тестуйте локально через MailHog/Mailpit: ніколи не налагоджуйте функціональність пошти безпосередньо на production-серверах або через реальні поштові скриньки.
Інтеграція в Go-застосунках
Стандартна бібліотека Go надає потужні та мінімалістичні інструменти для роботи з обома протоколами:
net.Resolverдля DNS-резолвінгу з підтримкою кастомних серверів, таймаутів та контекстів скасування.net/smtpдля надсилання листів із підтримкою автентифікації та TLS.
Для production-систем зазвичай використовуються асинхронні черги завдань (Redis Queue, RabbitMQ) для обробки надсилання листів у фоновому режимі, щоб не блокувати HTTP-запити від клієнтів. Розгорнутий приклад такої архітектури ми розглянемо в наступних лекціях, присвячених асинхронному обміну повідомленнями.
Контрольні запитання для самоперевірки
Рекурсивний резолвінг: клієнт надсилає один запит до рекурсивного DNS-сервера (зазвичай сервер інтернет-провайдера або публічний резолвер на кшталт 8.8.8.8), який бере на себе відповідальність за виконання всього ланцюжка запитів (до кореневих серверів, TLD-серверів, авторитетних серверів) та повертає клієнту готову відповідь — IP-адресу або помилку.
Ітеративний резолвінг: клієнт сам виконує кожен крок: спочатку запитує кореневий сервер, який повертає адреси TLD-серверів; потім запитує TLD-сервер, який повертає адреси авторитетних серверів зони; нарешті запитує авторитетний сервер і отримує кінцеву відповідь. Цей підхід рідко використовується звичайними клієнтами через складність, але саме так працюють рекурсивні резолвери «під капотом».
Base64 перетворює кожні 3 байти бінарних даних у 4 символи ASCII (оскільки 2^6 = 64 можливих символів, а кожен символ Base64 кодує 6 біт інформації). Отже, 3 байти = 24 біти перетворюються на 4 × 6 = 24 біти у вигляді 4 символів ASCII. Але кожен ASCII-символ займає 1 байт (8 біт) при передачі, тому 3 байти вихідних даних перетворюються на 4 байти закодованих даних: (4 / 3 - 1) × 100% ≈ 33% збільшення.
Чи можна оптимізувати? Ні, якщо ви обмежені текстовим протоколом SMTP. Але існують альтернативи: наприклад, замість вкладення великих файлів у лист можна надіслати посилання для завантаження (file upload link) на хмарне сховище (AWS S3, Google Cloud Storage), де файл зберігається в оригінальному бінарному форматі без зайвих накладних витрат.
Основні механізми захисту:
- Автентифікація на порту 587 (MSA): вимагати логін/пароль перед прийняттям листів від клієнтів.
- Rate Limiting: обмежувати кількість листів, які один користувач може надіслати за одиницю часу (наприклад, не більше 100 листів на годину).
- SPF, DKIM, DMARC: перевіряти вхідні листи на відповідність цим стандартам і відхиляти підозрілі.
- IP-репутація та чорні списки (DNSBL): автоматично перевіряти IP-адреси відправників за базами відомих джерел спаму (Spamhaus, Barracuda).
- Greylisting: тимчасово відхиляти листи від невідомих відправників (код 4xx) і приймати їх лише при повторній спробі через кілька хвилин (легітимні сервери повторюють спробу, спамери зазвичай ні).
- Контент-фільтри: аналізувати текст та заголовки листів за допомогою систем на кшталт SpamAssassin або навчених ML-моделей.
Рекомендована література та джерела для поглибленого вивчення
Специфікації RFC (офіційні стандарти IETF)
- RFC 1034, RFC 1035: Domain Names — Concepts and Facilities; Implementation and Specification (оригінальні специфікації DNS, 1987).
- RFC 8499: DNS Terminology (сучасний глосарій термінів DNS, 2019).
- RFC 5321: Simple Mail Transfer Protocol (оновлена специфікація SMTP, 2008).
- RFC 5322: Internet Message Format (формат заголовків електронної пошти).
- RFC 2045–2049: Multipurpose Internet Mail Extensions (MIME) (п'ять документів, що визначають структуру MIME-повідомлень).
- RFC 7208: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email.
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures.
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC).
- RFC 3207: SMTP Service Extension for Secure SMTP over TLS (STARTTLS).
Книги та навчальні ресурси
- Kurose J. F., Ross K. W. Computer Networking: A Top-Down Approach. 8th Edition. Pearson, 2021. (Розділи 2.4–2.5: DNS та електронна пошта).
- Liu C., Albitz P. DNS and BIND. 5th Edition. O'Reilly Media, 2006. (Поглиблене вивчення адміністрування DNS-серверів).
- Офіційна документація Go: https://pkg.go.dev/net (пакети
net,net/smtp,net/mail,mime).
Інструменти діагностики та тестування
- dig / nslookup / host — консольні утиліти для виконання DNS-запитів вручну.
- Wireshark: аналіз DNS та SMTP-трафіку на рівні пакетів.
- MailHog: https://github.com/mailhog/MailHog
- Mailpit: https://github.com/axllent/mailpit
- MXToolbox: https://mxtoolbox.com — онлайн-інструменти для перевірки MX-записів, SPF, DKIM, DMARC та репутації поштових серверів.
✅ Результати навчання
Після опрацювання цієї лекції ви здатні:
- Пояснити ієрархічну структуру DNS та процес рекурсивного резолвінгу від кореневих серверів до авторитетних.
- Програмно виконувати DNS-запити різних типів (A, AAAA, MX, TXT, SRV) через Go-пакет
net.Resolver. - Розбирати формат DNS-повідомлень та інтерпретувати коди відповідей.
- Розуміти архітектуру електронної пошти та роль компонентів MSA, MTA, MDA.
- Створювати SMTP-з'єднання, виконувати рукостискання та коректно обробляти коди відповідей сервера.
- Формувати багатокомпонентні MIME-повідомлення з HTML-вмістом і Base64-вкладеннями.
- Налаштовувати механізми захисту пошти (SPF, DKIM, DMARC, STARTTLS).
- Надсилати електронні листи через Go-пакет
net/smtpз автентифікацією та шифруванням.
🔗 Зв'язок із наступними темами
- Лекція 8: Протокол HTTP та стандартний вебсервер Go — розглянемо, як HTTP-клієнти спираються на DNS для резолвінгу доменних імен API-серверів.
- Лекція 10: Шарувата архітектура та конвеєри обробки запитів — інтеграція функціональності надсилання листів у Clean Architecture.
- Лекція 12: Валідація даних та документування у Gin — реалізація ендпоінтів реєстрації та скидання пароля з відправкою верифікаційних листів.
- Лекція 16: Асинхронний обмін повідомленнями та черги — організація фонової обробки надсилання листів через RabbitMQ для масштабованості.
Криптографічний захист каналів зв
Повний розгляд криптографічних основ захисту мережевих комунікацій мовою Go: доступне пояснення симетричного та асиметричного шифрування на прикладі листування, хеш-функції та цифрові підписи, протокол TLS 1.3 із детальним розбором фаз рукостискання та захисту від атак Man-in-the-Middle, інфраструктура відкритих ключів (PKI) та сертифікати X.509, центри сертифікації (CA), пакет crypto/tls для створення захищених серверів і клієнтів, взаємна автентифікація (mTLS) та модель Zero Trust на прикладі корпоративного мікросервісу.
Протокол HTTP та стандартний вебсервер Go
Еволюція протоколу HTTP від версії 1.0 до HTTP/3 з транспортом QUIC, детальний розбір анатомії HTTP-запитів та відповідей (методи, статус-коди, заголовки, тіло), принципи ідемпотентності та безпеки методів, стандартний пакет net/http у Go (інтерфейс http.Handler, роутер http.ServeMux, кастомний http.Server), налаштування таймаутів, TLS та обробка потокових даних.