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
+264 -2
View File
@@ -1,7 +1,269 @@
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() {
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 // Пакет успешно обработан без единой аллокации памяти в куче
}