Initial commit

This commit is contained in:
step305
2026-09-29 20:20:26 +03:00
parent 61d0ec3bcd
commit 17b5d19f69
17 changed files with 808 additions and 14 deletions
+14
View File
@@ -0,0 +1,14 @@
# ==============================================================================
# НАСТРОЙКИ ОКРУЖЕНИЯ (ENVIRONMENT VARIABLES)
# ==============================================================================
# Публичный IPv4-адрес вашего VPS-сервера.
#
# КРИТИЧЕСКИ ВАЖНО:
# Значение '127.0.0.1' (localhost) подходит ТОЛЬКО для локального тестирования на своем ПК.
# Чтобы STUN-сервер корректно работал в интернете и обслуживал реальных WebRTC-клиентов,
# этот адрес ОБЯЗАТЕЛЬНО должен быть заменен на внешний статический IP вашего сервера.
#
# На этот IP-адрес Go-код сервера будет мапить входящие запросы при генерации
# STUN-атрибута RESPONSE-ORIGIN.
PUBLIC_IP=127.0.0.1
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" serverName="call.memsik.pro" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="call.memsik.pro">
<serverdata>
<mappings>
<mapping deploy="/project/stun" local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/STUN.iml" filepath="$PROJECT_DIR$/.idea/STUN.iml" />
</modules>
</component>
</project>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<option name="projectDefaultTargetUuid" value="e91f31a9-e537-4008-92cc-a99093076c30" />
<targets>
<target name="step305@call.memsik.pro:23459" type="ssh/sftp" uuid="e91f31a9-e537-4008-92cc-a99093076c30">
<config>
<option name="projectRootOnTarget" value="/home/step305/project/stun" />
<option name="serverName" value="step305@call.memsik.pro:23459 password" />
</config>
<ContributedStateBase type="GoLanguageRuntime">
<config>
<option name="compiledExecutablesVolume">
<VolumeState>
<option name="remotePath" value="/home/step305/project/stun/bin" />
</VolumeState>
</option>
<option name="goPath" value="/home/step305/go" />
<option name="goRoot" value="/usr/local/go/bin/go" />
<option name="projectSourcesVolume">
<VolumeState>
<option name="remotePath" value="/home/step305/project/stun" />
</VolumeState>
</option>
</config>
</ContributedStateBase>
</target>
</targets>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebServers">
<option name="servers">
<webServer id="8bed373a-25b8-43ed-8828-5263b43c4494" name="call.memsik.pro">
<fileTransfer rootFolder="/home/step305" accessType="SFTP" host="call.memsik.pro" port="23459" sshConfigId="5fbaf4f9-d62c-40bd-8a45-19e904b83b6c" sshConfig="step305@call.memsik.pro:23459 password">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" keepAliveTimeout="180000" passiveMode="true" shareSSLContext="true" />
</advancedOptions>
</fileTransfer>
</webServer>
</option>
</component>
</project>
+172
View File
@@ -0,0 +1,172 @@
## ==============================================================================
## ПОДГОТОВКА И НАСТРОЙКА VPS, УСТАНОВКА GO, DOCKER
## ==============================================================================
## Обновить списки пакетов репозиториев, чтобы получить информацию о свежих версиях
```bash
sudo apt update
```
## Обновить все установленные в системе пакеты до актуальных версий (-y автоматически соглашается)
```bash
sudo apt upgrade -y
```
## Скачиваем официальный архив с бинарными файлами Go 1.27.1 для Linux x64
```bash
wget https://go.dev/dl/go1.27.1.linux-amd64.tar.gz
```
## Удалить старую версию Go (если она была), чтобы избежать конфликта файлов
```bash
sudo rm -rf /usr/local/go
```
## Распаковать скачанный архив Go в системную директорию /usr/local
```bash
sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz
```
## Добавить путь к бинарникам Go в переменную PATH текущего пользователя для автозапуска
```bash
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
```
## Перезапустить профиль, чтобы изменения переменной PATH вступили в силу без перезагрузки сервера
```bash
source ~/.profile
```
## Удалить скачанный архив Go, так как он больше не нужен и занимает место
```bash
rm go1.27.1.linux-amd64.tar.gz
```
## Проверить успешность установки Go, выводя текущую версию в консоль
```bash
go version
```
## ------------------------------------------------------------------------------
## СЕТЕВОЙ ТЮНИНГ ЯДРА LINUX ДЛЯ ВЫСОКИХ НАГРУЗОК (HIGHLOAD UDP)
## ------------------------------------------------------------------------------
## По умолчанию буферы ОС слишком малы для STUN-сервера. Если их не увеличить,
## при шторме UDP-пакетов ядро Linux начнет их просто дропать (терять).
## Увеличить максимальный размер буфера приема UDP-пакетов операционной системы до ~25 Мб
```bash
sudo sysctl -w net.core.rmem_max=26214400
```
## Увеличить максимальный размер буфера отправки UDP-пакетов до ~25 Мб
```bash
sudo sysctl -w net.core.wmem_max=26214400
```
## Устанавить дефолтный (стартовый) размер буфера приема для новых сокетов на ~25 Мб
```bash
sudo sysctl -w net.core.rmem_default=26214400
```
## Устанавить дефолтный (стартовый) размер буфера отправки для новых сокетов на ~25 Мб
```bash
sudo sysctl -w net.core.wmem_default=26214400
```
## Сохранить настройки
```bash
sudo tee -a /etc/sysctl.conf <<EOF
```
## Настроить буферов сокетов для Highload UDP STUN-сервера
```bash
net.core.rmem_max=26214400
net.core.wmem_max=26214400
net.core.rmem_default=26214400
net.core.wmem_default=26214400
EOF
```
## Применить настройки
```bash
sudo sysctl -p
```
## ------------------------------------------------------------------------------
## УСТАНОВКА DOCKER И DOCKER COMPOSE
## ------------------------------------------------------------------------------
## Устанавить утилиты для работы с SSL-сертификатами и скачивания файлов через терминал
```bash
sudo apt-get install ca-certificates curl
```
## Создать защищенную директорию для хранения GPG-ключей сторонних репозиториев
```bash
sudo install -m 0755 -d /etc/apt/keyrings
```
## Скачать официальный публичный GPG-ключ шифрования Docker для проверки подлинности пакетов
```bash
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
```
## Выдать права на чтение этого ключа всем пользователям системы
```bash
sudo chmod a+r /etc/apt/keyrings/docker.asc
```
## Добавить официальный стабильный репозиторий Docker в список источников Ubuntu (apt)
```bash
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```
## Обновить кэш пакетов apt, чтобы система «увидела» добавленный репозиторий Docker
```bash
sudo apt-get update
```
## Устанавивить движок Docker, консольный клиент, контейнеризатор и плагин Docker Compose V2
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
```
## ==============================================================================
## НАСТРОЙКА ФАЙЕРВОЛЛА (БЕЗОПАСНОСТЬ)
## ==============================================================================
## Разрешить входящий трафик на порт 3478 по протоколу UDP (основной порт STUN-сервера)
```bash
sudo ufw allow 3478/udp
```
## ==============================================================================
## НАСТРОЙКА И УПРАВЛЕНИЕ СЕРВЕРОМ
## ==============================================================================
## Автоматически заменить значение PUBLIC_IP в файле .env на реальный IP-адрес вашего VPS
```bash
sed -i 's/^PUBLIC_IP=.*/PUBLIC_IP=ВНЕШНИЙ_IP/' .env
```
## Сделать все bash-скрипты автоматизации в текущей папке исполняемыми (разрешаем их запуск)
```bash
chmod +x *.sh
```
## Запустить STUN-сервер (внутри скрипта обычно вызывается docker compose up -d)
```bash
./start.sh
```
## Открыть поток логов контейнера в реальном времени для мониторинга работы сервера
```bash
./get_logs.sh
```
## Остановить STUN-сервер (внутри обычно вызывается docker compose down)
```bash
./stop.sh
```
+110 -12
View File
@@ -1,12 +1,110 @@
# Установка Go ## Итоговое техническое описание проекта: Высоконагруженный STUN-сервер на Go
```bash Данный проект представляет собой сверхбыстрый, отказоустойчивый и оптимизированный
sudo apt update под экстремальные нагрузки STUN-сервер (Session Traversal Utilities for NAT),
sudo apt upgrade -y написанный на языке Go (Golang). Сервер разработан в соответствии со стандартами
wget https://go.dev/dl/go1.27.1.linux-amd64.tar.gz RFC 5389 и RFC 8489 и предназначен для использования в инфраструктурах реального
sudo rm -rf /usr/local/go времени (WebRTC, VoIP, SIP, онлайн-игры) с целью определения внешних IP-адресов
sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz и портов клиентов, находящихся за NAT.
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile Проект полностью контейнеризирован с помощью Docker и Docker Compose, а также
source ~/.profile защищен на уровне ядра ОС Linux, что позволяет ему обрабатывать тысячи запросов
rm go1.27.1.linux-amd64.tar.gz в секунду (RPS) на минимальных серверных мощностях.
go version ------------------------------
``` ## 🚀 Архитектурные особенности и ключевые фишки
Архитектура проекта сфокусирована на достижении максимальной пропускной способности
и минимального времени отклика (в тестах задержка составляет всего 20–30 мс):
1. Концепция True Zero-Alloc (Нулевое выделение памяти в куче):
* Вместо аллокации нового буфера под каждый входящий UDP-пакет, каждый независимый
* воркер создает фиксированный срез байт (buf := make([]byte, 1500)) единожды
* перед запуском бесконечного цикла.
* Ответ клиенту формируется в том же самом массиве данных поверх исходного
запроса (buf = buf[:cap(buf)]).
* Сборщик мусора Go (Garbage Collector) вообще не нагружается во время
обработки сетевого флуда. Это исключает микропаузы (STW) сервера под нагрузкой.
2. Использование современного пакета net/netip:
* Взамен старого типа net.IP (который является срезом и всегда аллоцируется в куче),
в коде применен пакет net/netip.
* Объект netip.AddrPort хранит данные в виде легковесной структуры фиксированного
размера, которая обрабатывается исключительно на уровне быстрого стека процессора.
3. Многопоточный пул воркеров (Worker Pool):
* Сервер автоматически определяет количество доступных логических ядер процессора
(runtime.NumCPU()) и запускает строго эквивалентное количество горутин-воркеров.
* Ядро Linux равномерно распределяет сетевые прерывания от UDP-сокета,
утилизируя процессор на все 100%.
4. Мягкое завершение работы (Graceful Shutdown):
* Сервер перехватывает системные сигналы остановки от операционной системы
или Docker-демона (SIGINT, SIGTERM).
* При получении команды на выключение сокет закрывается, воркеры корректно
дорабатывают текущую итерацию, а sync.WaitGroup гарантирует завершение процессов
без потери обрабатываемых пакетов.
------------------------------
## 🐳 Контейнеризация и Безопасность в Docker
Проект поставляется со специализированной конфигурацией развертывания,
обеспечивающей максимальную производительность и изоляцию:
* Многоэтапная сборка (Multi-stage Build): Образ собирается в официальном
контейнере golang:alpine, где происходит компиляция бинарника со флагами
-ldflags="-s -w" (вырезаются отладочные символы, что уменьшает размер файла
в 2 раза).
* Супер-легковесный и безопасный финальный слой (scratch): Финальный образ
собирается на базе абсолютно пустого слоя FROM scratch. Внутри контейнера
нет Linux-дистрибутива, нет пакетных менеджеров, утилит (sh, bash, ls)
и зависимостей. Вес готового Docker-образа составляет всего ~15-20 МБ.
* Принцип Non-Root: Внутри контейнера процесс STUN-сервера запускается
от имени непривилегированного системного пользователя stunuser.
В случае гипотетической уязвимости в коде, злоумышленник не сможет выйти на
хост-систему или получить права суперпользователя.
* Сетевой режим network_mode: "host": Контейнер подключен к сетевой карте
VPS напрямую. Это исключает прослойку docker-proxy и встроенную маршрутизацию
Docker, которая является главным «узким горлышком» для UDP-трафика и приводит
к потере пакетов под нагрузкой.
------------------------------
## 🛠️ Системный тюнинг ОС Linux под Highload UDP
По умолчанию буферы сетевых сокетов в Linux настроены на низкую нагрузку.
При шторме UDP-запросов буфер быстро переполняется, и ядро ОС начинает молча
дропать (терять) пакеты еще до того, как Go-приложение успеет их прочитать.
Для решения этой проблемы в проекте применяется сетевой тюнинг ядра через
конфигурацию sysctl. Максимальные и дефолтные буферы приема (rmem) и отправки
(wmem) сетевых сокетов расширяются до 25 Мегабайт (26214400 байт):
* net.core.rmem_max=26214400
* net.core.wmem_max=26214400
Эти настройки жестко фиксируются в системном файле /etc/sysctl.conf хост-машины,
благодаря чему сервер гарантированно выдерживает кратковременные всплески трафика
(микро-штормы), удерживая пакеты в памяти ОС до момента их вычитки воркерами.
------------------------------
## 📋 Структура конфигурационных файлов проекта
Проект имеет модульную и простую структуру, состоящую из следующих компонентов:
1. main.go — Весь исходный код сервера. Содержит логику инициализации,
пул воркеров, парсинг сырых байт STUN-заголовка, генерацию XOR-маппинга для
обхода NAT и отправку ответов.
2. go.mod — Файл описания Go-модуля (проект STUN на базе Go 1.27.1) с
автоматической фиксацией низкоуровневых системных зависимостей (golang.org/x/sys).
3. Dockerfile — Инструкция сборки безопасного микро-контейнера на базе
scratch от лица non-root пользователя.
4. docker-compose.yml — Манифест для оркестрации: настраивает запуск в
режиме host, прокидывает переменные окружения, ограничивает ресурсы
(Limits: 2 CPU, 128MB RAM) и защищает диск от переполнения логами
(ротация логов ограничена 3 файлами по 10МБ).
5. .env — Файл конфигурации переменных среды, где хранится внешний публичный
IPv4-адрес сервера (PUBLIC_IP), необходимый для корректного заполнения
STUN-аттрибутов ответа.
------------------------------
## 📈 Результаты тестирования пропускной способности
В ходе стресс-тестирования методом асинхронного UDP-флуда
(параллельный запуск тысячи клиентов turnutils_stunclient), сервер показал
следующие результаты:
* Линейная масштабируемость: Обработка пакета из 2000 одновременных
запросов выполняется за ~1.19 сек, демонстрируя чистую скорость ~1550-1700 RPS
(запросов в секунду) на один клиентский поток тестирования.
* Параллельная утилизация ядер: Время работы в пользовательском пространстве
(user) и пространстве ядра (sys) превышает реальное время теста (real),
что доказывает идеальную параллельную работу пула воркеров на всех доступных
ядрах процессора хоста.
+48
View File
@@ -0,0 +1,48 @@
# Описываем список запускаемых сервисов (контейнеров)
services:
# Название нашего сервиса внутри экосистемы Docker Compose
stun:
# Путь к папке, где находится Dockerfile для сборки образа.
# Точка означает текущую директорию.
build: .
# Имя и тег, которые будут присвоены собранному Docker-образу
image: go-stun-server:latest
# Фиксированное имя контейнера в операционной системе (для удобства просмотра через `docker ps`)
container_name: stun_server
# Сетевой режим: "host". Контейнер использует сетевой стек хоста напрямую.
# Включает максимальную производительность UDP, убирает задержки и накладные расходы
# стандартного механизма маршутизации Docker (docker-proxy). Порты пробрасывать не нужно.
network_mode: "host"
# Политика перезапуска: контейнер будет автоматически перезапускаться при падении приложения,
# сбое Docker-демона или полной перезагрузке самого VPS (если только вы не остановили его вручную).
restart: unless-stopped
# Переменные окружения, которые прокидываются внутрь приложения
environment:
# Задаем переменную PUBLIC_IP. Конструкция \${VARIABLE:-default} означает:
# взять значение PUBLIC_IP из файла .env системы, а если его там нет, использовать дефолтный 127.0.0.1
- PUBLIC_IP=${PUBLIC_IP:-127.0.0.1}
# Секция управления ресурсами при развертывании (работает как в Swarm, так и в стандартном Compose)
deploy:
resources:
# Устанавливаем жесткие лимиты, которые контейнер не сможет превысить
limits:
# Выделяем серверу максимум 2 процессорных ядра (ядра CPU)
cpus: '2'
# Ограничиваем оперативную память до 128 Мб (для STUN этого более чем достаточно)
memory: 128M
# Настройка ротации и сбора логов контейнера (важно для защиты диска от переполнения)
logging:
# Используем стандартный драйвер логирования в JSON-файлы
driver: "json-file"
options:
# Максимальный размер одного файла лога — 10 Мегабайт
max-size: "10m"
# Хранить не более 3 архивных файлов логов. Старые логи будут автоматически затираться.
max-file: "3"
+70
View File
@@ -0,0 +1,70 @@
# ==============================================================================
# ЭТАП СБОРКИ (BUILDER)
# ==============================================================================
# Используем официальный образ Go на базе легковесного дистрибутива Alpine Linux.
# Именуем этот этап как "builder", чтобы позже забрать из него скомпилированный файл.
FROM golang:1.27-alpine AS builder
# Устанавливаем Git без сохранения кэша пакетов (уменьшает размер слоя).
# Он необходим Go для скачивания внешних зависимостей (например, golang.org/x/sys).
RUN apk add --no-cache git
# Объявляем переменные окружения для создания изолированного системного пользователя.
ENV USER=stunuser
ENV UID=10001
# Создаем безопасного пользователя внутри сборочного контейнера:
# --disabled-password: без пароля (вход по паролю заблокирован)
# --gecos "": отключает заполнение анкетных данных (имя, телефон и т.д.)
# --home "/nonexistent": без домашней папки
# --shell "/sbin/nologin": запрещает пользователю выполнять bash/sh команды в системе
# --no-create-home: принудительно запрещает создавать домашний каталог
RUN adduser --disabled-password --gecos "" --home "/nonexistent" --shell "/sbin/nologin" --no-create-home --uid "${UID}" "${USER}"
# Устанавливаем рабочую директорию, где будет происходить сборка кода
WORKDIR /app
# Шаг оптимизации кэша Docker: сначала копируем только файлы зависимостей go.mod и go.sum.
# Звездочка * защищает от ошибки сборки, если файла go.sum еще физически не существует.
COPY go.mod* go.sum* ./
# Если файл go.mod существует, скачиваем все внешние библиотеки и проверяем их хэш-суммы.
# Благодаря этому шагу зависимости будут кэшироваться и не будут скачиваться заново при изменении кода.
RUN if [ -f go.mod ]; then go mod download && go mod verify; fi
# Копируем весь оставшийся исходный код проекта (main.go и другие файлы) в рабочую директорию
COPY . .
# Компилируем высокопроизводительный бинарник:
# CGO_ENABLED=0: отключаем зависимости от C-библиотек (делает бинарник полностью автономным)
# GOOS=linux: жестко указываем целевую операционную систему Linux
# -ldflags="-s -w": отключает генерацию отладочной информации (символов) и таблиц DWARF.
# Это уменьшает размер итогового файла почти в 2 раза без потери скорости.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/stun_server .
# ==============================================================================
# ФИНАЛЬНЫЙ ЭТАП (PRODUCTION)
# ==============================================================================
# FROM scratch — это пустой слой. В нем нет Linux, нет bash, нет пакетных менеджеров.
# Это гарантирует минимальный размер контейнера и максимальную безопасность (его невозможно взломать).
FROM scratch
# Переносим файлы пользователей и групп, созданные на этапе сборки.
# Без этого контейнер scratch не узнает, кто такой "stunuser".
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Копируем единственный скомпилированный исполняемый файл STUN-сервера из этапа builder
COPY --from=builder /app/stun_server /stun_server
# Переключаем контекст выполнения. Сервер будет работать от имени stunuser, а не root.
# Если в коде сервера найдут уязвимость, злоумышленник все равно не получит права root на хосте.
USER stunuser:stunuser
# Документируем, что контейнер слушает порт 3478 по протоколу UDP
EXPOSE 3478/udp
# Точка входа: при старте контейнера автоматически запускается наш STUN-сервер.
# Использование квадратных скобок [строгий JSON] запускает процесс напрямую (PID 1) без лишней оболочки sh.
ENTRYPOINT ["/stun_server"]
+7
View File
@@ -0,0 +1,7 @@
# sudo: Запускает команду от имени суперпользователя (root) для доступа к файлам логов Docker.
#
# docker logs: Команда Docker для чтения стандартного вывода (stdout и stderr) из контейнера.
#
# stun_server: Имя конкретного контейнера (заданное в container_name), логи которого нужно прочитать.
sudo docker logs stun_server
+7
View File
@@ -1 +1,8 @@
// Имя модуля (Namespace) вашего проекта.
// Все внутренние импорты в коде будут начинаться с этого префикса (например, "STUN/internal/config").
module STUN module STUN
// Минимально необходимая версия компилятора Go для сборки данного проекта.
// Начиная с Go 1.21, эта строка также управляет поведением GOTOOLCHAIN,
// заставляя систему автоматически скачивать именно версию 1.27.1, если локальная версия старее.
go 1.27.1
+264 -2
View File
@@ -1,7 +1,269 @@
package main package main
import "fmt" import (
// context: Используется для управления жизненным циклом приложения.
// Позволяет мягко (Graceful Shutdown) передать сигнал отмены всем воркерам.
"context"
// encoding/binary: Позволяет читать и писать числа в сетевом байтовом порядке
// (BigEndian / Network Byte Order). Незаменим для парсинга и сборки STUN-заголовков.
"encoding/binary"
// errors: Предоставляет базовые инструменты для создания и сравнения ошибок.
// Используется для создания pre-allocated (заранее созданных) констант ошибок.
"errors"
// flag: Реализует парсинг флагов командной строки (аргументов запуска).
// Нужен для ручной передачи внешнего IP через параметр, например, `--public-ip=91.210.106.64`.
"flag"
// log/slog: Современный пакет структурированного логирования.
// Позволяет выводить системные сообщения в формате "ключ=значение" с высокой скоростью.
"log/slog"
// net: Базовый сетевой пакет Go. Используется для открытия сетевого сокета
// через net.ListenUDP и работы с традиционными UDP-соединениями.
"net"
// net/netip: Сверхбыстрый и современный пакет для работы с IP-адресами (с Go 1.18).
// В отличие от старого net.IP, структуры netip.Addr распределяются в стеке (Zero-Alloc).
"net/netip"
// os: Позволяет взаимодействовать с операционной системой хоста.
// Используется для чтения переменных окружения (os.Getenv) и выходов в случае критических сбоев.
"os"
// os/signal: Перехватывает системные сигналы от ОС (например, нажатие Ctrl+C).
// Нужен для того, чтобы сервер не умирал мгновенно, а корректно закрывал сокеты при остановке.
"os/signal"
// runtime: Позволяет управлять планировщиком Go и исследовать среду выполнения.
// Вызов runtime.NumCPU() определяет количество доступных ядер для пула воркеров.
"runtime"
// sync: Предоставляет примитивы синхронизации горутин.
// Структура sync.WaitGroup гарантирует, что сервер не закроется, пока все воркеры не завершат работу.
"sync"
// syscall: Низкоуровневый интерфейс к системным вызовам операционной системы.
// Нужен для точного перехвата сигнала завершения процесса (syscall.SIGTERM) от Docker.
"syscall"
)
// Константы протокола STUN (RFC 5389 / RFC 8489)
const (
Port = 3478 // Стандартный UDP-порт для STUN-сервисов
BufferSize = 1500 // Размер буфера под MTU (максимальный пакет без фрагментации IP)
STUNHeaderSize = 20 // Фиксированный размер заголовка STUN (20 байт)
MagicCookie = 0x2112A442 // Магическое число STUN для идентификации протокола
AttrResponseOrigin = 0x802B // Код атрибута, указывающий исходный IP/порт сервера
)
// Преаллоцированные ошибки для защиты от лишних выделений памяти в рантайме
var (
errPacketTooShort = errors.New("packet too short")
errInvalidMethod = errors.New("invalid message type or method")
errInvalidCookie = errors.New("invalid magic cookie")
)
func main() { func main() {
fmt.Println("Hello, World!") // Инициализируем быстрый структурированный логгер (slog) в текстовом формате
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
// Регистрируем флаг командной строки для ручного ввода публичного IP
flagStr := flag.String("public-ip", "", "Внешний публичный IPv4-адрес сервера (для логов)")
flag.Parse()
// Логика приоритета: если флаг пустой, ищем значение в переменных окружения (Docker/.env)
publicIPStr := *flagStr
if publicIPStr == "" {
publicIPStr = os.Getenv("PUBLIC_IP")
}
// Парсим и валидируем IP-адрес
var publicIP netip.Addr
if publicIPStr != "" {
var err error
publicIP, err = netip.ParseAddr(publicIPStr)
// Сервер строго IPv4, поэтому проверяем Is4()
if err != nil || !publicIP.Is4() {
slog.Error("Указан некорректный IPv4 public-ip", "val", publicIPStr)
os.Exit(1)
}
} else {
// Дефолтный фолбек на localhost, если адрес вообще нигде не указан
publicIP = netip.AddrFrom4([4]byte{127, 0, 0, 1})
}
// Определяем количество логических ядер процессора для запуска оптимального пула воркеров
numWorkers := runtime.NumCPU()
// Настраиваем контекст, который слушает сигналы завершения работы ОС (Ctrl+C / SIGTERM от Docker)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
// Открываем единый UDP-сокет на всех интерфейсах (0.0.0.0) и порту 3478
addr := &net.UDPAddr{IP: net.IPv4zero, Port: Port}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
slog.Error("Не удалось запустить UDP сервер", "error", err)
os.Exit(1)
}
defer conn.Close()
// Настройка буферов сокета на уровне ядра ОС (4 Мегабайта).
// Защищает от потери пакетов, пока Go-горутины переключают контекст.
_ = conn.SetReadBuffer(4194304)
_ = conn.SetWriteBuffer(4194304)
// Запускаем пул параллельных воркеров по числу ядер CPU
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
startWorker(ctx, conn, publicIP)
}()
}
slog.Info("STUN-сервер запущен (True Zero-Alloc, IPv4)", "port", Port, "workers", numWorkers)
// Блокируем главный поток, ожидая системного сигнала на остановку
<-ctx.Done()
slog.Info("Завершение работы сервера...")
// Принудительно закрываем сокет. Это мгновенно разблокирует методы Read во всех воркерах.
conn.Close()
wg.Wait() // Ждем, пока все воркеры корректно завершат текущую итерацию цикла
slog.Info("Сервер успешно остановлен")
}
func startWorker(ctx context.Context, conn *net.UDPConn, publicIP netip.Addr) {
// КЛЮЧЕВАЯ ОПТИМИЗАЦИЯ: Буфер выделяется один раз ПЕРЕД циклом для каждого воркера.
// Переменная не "убегает" в кучу (heap alloc = 0) и переиспользуется для каждого пакета.
buf := make([]byte, BufferSize)
for {
// Неблокирующая быстрая проверка — не приказал ли контекст завершить работу
select {
case <-ctx.Done():
return
default:
}
// Читаем пакет из сетевого сокета прямо в преаллоцированный буфер.
// addrPort возвращает netip.AddrPort (аллокация структуры в стеке, zero-alloc).
n, addrPort, err := conn.ReadFromUDPAddrPort(buf)
if err != nil {
if errors.Is(err, net.ErrClosed) {
return // Нормальный выход, если сокет был закрыт главным потоком
}
slog.Debug("Ошибка чтения из UDP сокета", "error", err)
continue
}
// Быстрая фильтрация невалидных пакетов или IPv6-запросов (так как сервер под IPv4)
if !addrPort.IsValid() || !addrPort.Addr().Is4() {
continue
}
// Передаем пакет на разбор и формирование ответа
_ = handleSTUN(conn, addrPort, buf, n, publicIP)
}
}
func handleSTUN(conn *net.UDPConn, remoteAddr netip.AddrPort, buf []byte, packetLen int, publicIP netip.Addr) error {
// Базовая проверка: STUN-пакет не может быть меньше своего заголовка
if packetLen < STUNHeaderSize {
return errPacketTooShort
}
// Читаем первые 2 байта (Message Type). Сервер обрабатывает только Binding Request (0x0001)
messageType := binary.BigEndian.Uint16(buf[0:2])
if messageType != 0x0001 {
return errInvalidMethod
}
// Проверяем корректность Magic Cookie (байты 4-7)
magicCookie := binary.BigEndian.Uint32(buf[4:8])
if magicCookie != MagicCookie {
return errInvalidCookie
}
// Извлекаем уникальный Transaction ID (12 байт), чтобы клиент понял, на какой запрос пришел ответ
var transactionID [12]byte
copy(transactionID[:], buf[8:20])
// Получаем IP-адрес клиента в виде массива из 4 байт (стековая структура)
clientIP := remoteAddr.Addr().As4()
// Фокус оптимизации: Расширяем видимость среза до полной емкости буфера (1500 байт),
// чтобы писать ответ прямо поверх данных запроса без аллокации новой памяти.
buf = buf[:cap(buf)]
// Смещение (указатель записи). Заголовок пока пропускаем, начнем писать атрибуты с 20-го байта.
pos := STUNHeaderSize
// --------------------------------------------------------------------------
// АТРИБУТ 1: XOR-MAPPED-ADDRESS (0x0020) — возвращает клиенту его внешний IP/Порт с XOR-шифрованием
// --------------------------------------------------------------------------
binary.BigEndian.PutUint16(buf[pos:pos+2], 0x0020) // Type
binary.BigEndian.PutUint16(buf[pos+2:pos+4], 8) // Length (8 байт на данные)
buf[pos+4] = 0 // Зарезервировано
buf[pos+5] = 0x01 // Protocol Family: IPv4
// Шифруем порт: делаем XOR порта клиента с первыми 16 битами Magic Cookie
xorPort := remoteAddr.Port() ^ uint16(MagicCookie>>16)
binary.BigEndian.PutUint16(buf[pos+6:pos+8], xorPort)
// Шифруем IP: делаем XOR IP-адреса клиента с полным 32-битным Magic Cookie
xorIP := binary.BigEndian.Uint32(clientIP[:]) ^ MagicCookie
binary.BigEndian.PutUint32(buf[pos+8:pos+12], xorIP)
pos += 12 // Сдвигаем указатель на 12 байт вперед
// --------------------------------------------------------------------------
// АТРИБУТ 2: MAPPED-ADDRESS (0x0001) — старый формат без XOR (для совместимости со старым софтом)
// --------------------------------------------------------------------------
binary.BigEndian.PutUint16(buf[pos:pos+2], 0x0001) // Type
binary.BigEndian.PutUint16(buf[pos+2:pos+4], 8) // Length
buf[pos+4] = 0 // Зарезервировано
buf[pos+5] = 0x01 // Protocol Family: IPv4
binary.BigEndian.PutUint16(buf[pos+6:pos+8], remoteAddr.Port()) // Порт в чистом виде
copy(buf[pos+8:pos+12], clientIP[:]) // IP в чистом виде
pos += 12
// --------------------------------------------------------------------------
// АТРИБУТ 3: RESPONSE-ORIGIN (0x802B) — сообщает клиенту, с какого IP сервера отправлен ответ
// --------------------------------------------------------------------------
binary.BigEndian.PutUint16(buf[pos:pos+2], AttrResponseOrigin) // Type
binary.BigEndian.PutUint16(buf[pos+2:pos+4], 8) // Length
buf[pos+4] = 0 // Зарезервировано
buf[pos+5] = 0x01 // Protocol Family: IPv4
binary.BigEndian.PutUint16(buf[pos+6:pos+8], uint16(Port)) // Порт нашего STUN сервера
serverIPBytes := publicIP.As4()
copy(buf[pos+8:pos+12], serverIPBytes[:]) // Внешний IP нашего сервера из переменной окружения
pos += 12
// 计算 Длина ответа
totalLen := pos // Полный размер пакета (20 байт заголовок + 36 байт атрибуты = 56 байт)
attrLen := totalLen - STUNHeaderSize // Чистый размер только блока атрибутов (36 байт)
// --------------------------------------------------------------------------
// ЗАПОЛНЕНИЕ СТАРТОВОГО ЗАГОЛОВКА STUN (Первые 20 байт пакета)
// --------------------------------------------------------------------------
binary.BigEndian.PutUint16(buf[0:2], 0x0101) // Message Type: Binding Success Response
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen)) // Записываем длину атрибутов в заголовок
binary.BigEndian.PutUint32(buf[4:8], MagicCookie) // Возвращаем Magic Cookie
copy(buf[8:20], transactionID[:]) // Возвращаем оригинальный Transaction ID клиента
// Отправляем сформированный срез байт обратно клиенту по UDP
if _, err := conn.WriteToUDPAddrPort(buf[:totalLen], remoteAddr); err != nil {
if !errors.Is(err, net.ErrClosed) {
slog.Debug("Ошибка отправки ответа", "remote", remoteAddr.String(), "error", err.Error())
}
return err
}
return nil // Пакет успешно обработан без единой аллокации памяти в куче
} }
+14
View File
@@ -0,0 +1,14 @@
# sudo: Запускает команду с правами суперадминистратора (root),
# так как управление Docker-демоном требует повышенных привилегий.
#
# docker compose: Утилита Docker для управления многоконтейнерными приложениями,
# описанными в файле docker-compose.yml.
#
# up: Основная команда, которая создает, собирает (если нужно)
# и запускает контейнеры, описанные в конфигурации.
#
# -d: Флаг "detached mode" (фоновый режим). Запускает контейнер
# в бэкграунде и сразу освобождает консоль для ввода других команд.
# Если запустить без него, логи сервера займут весь терминал.
sudo docker compose up -d
+13
View File
@@ -0,0 +1,13 @@
# sudo: Запускает команду с правами суперадминистратора (root),
# так как управление инфраструктурой Docker требует повышенных прав.
#
# docker compose: Утилита Docker для управления архитектурой проекта,
# описанной в файле docker-compose.yml.
#
# down: Полная остановка и очистка окружения.
# В отличие от команды `stop`, она делает три вещи:
# 1. Безопасно останавливает процесс (сигналом SIGTERM).
# 2. Полностью удаляет (уничтожает) контейнер stun_server.
# 3. Удаляет внутренние виртуальные сети, созданные для этого сервиса.
sudo docker compose down