Initial commit
This commit is contained in:
@@ -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
|
||||||
Generated
+8
@@ -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
|
||||||
Generated
+9
@@ -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>
|
||||||
Generated
+14
@@ -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>
|
||||||
Generated
+8
@@ -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>
|
||||||
Generated
+30
@@ -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
@@ -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>
|
||||||
Generated
+14
@@ -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
@@ -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
|
||||||
|
```
|
||||||
@@ -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),
|
||||||
|
что доказывает идеальную параллельную работу пула воркеров на всех доступных
|
||||||
|
ядрах процессора хоста.
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# sudo: Запускает команду от имени суперпользователя (root) для доступа к файлам логов Docker.
|
||||||
|
#
|
||||||
|
# docker logs: Команда Docker для чтения стандартного вывода (stdout и stderr) из контейнера.
|
||||||
|
#
|
||||||
|
# stun_server: Имя конкретного контейнера (заданное в container_name), логи которого нужно прочитать.
|
||||||
|
|
||||||
|
sudo docker logs stun_server
|
||||||
@@ -1 +1,8 @@
|
|||||||
|
// Имя модуля (Namespace) вашего проекта.
|
||||||
|
// Все внутренние импорты в коде будут начинаться с этого префикса (например, "STUN/internal/config").
|
||||||
module STUN
|
module STUN
|
||||||
|
|
||||||
|
// Минимально необходимая версия компилятора Go для сборки данного проекта.
|
||||||
|
// Начиная с Go 1.21, эта строка также управляет поведением GOTOOLCHAIN,
|
||||||
|
// заставляя систему автоматически скачивать именно версию 1.27.1, если локальная версия старее.
|
||||||
|
go 1.27.1
|
||||||
|
|||||||
@@ -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 // Пакет успешно обработан без единой аллокации памяти в куче
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# sudo: Запускает команду с правами суперадминистратора (root),
|
||||||
|
# так как управление Docker-демоном требует повышенных привилегий.
|
||||||
|
#
|
||||||
|
# docker compose: Утилита Docker для управления многоконтейнерными приложениями,
|
||||||
|
# описанными в файле docker-compose.yml.
|
||||||
|
#
|
||||||
|
# up: Основная команда, которая создает, собирает (если нужно)
|
||||||
|
# и запускает контейнеры, описанные в конфигурации.
|
||||||
|
#
|
||||||
|
# -d: Флаг "detached mode" (фоновый режим). Запускает контейнер
|
||||||
|
# в бэкграунде и сразу освобождает консоль для ввода других команд.
|
||||||
|
# Если запустить без него, логи сервера займут весь терминал.
|
||||||
|
|
||||||
|
sudo docker compose up -d
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user