Привет, Хабр! Я full-cycle разработчик, работаю с Rust/Zig/Go и увлекаюсь APL как особым способом мышления о коде. На самом деле я пишу на большом количестве языков, но сегодня не об этом. Хочу рассказать об опыте миграции критической бизнес-логики с PHP на Zig и интеграции её в бинарник.

Контекст проекта

Мой установщик дистрибутива, изначально установка была архаичной: распакуй, запусти скрипт и т.д. Здесь всё автоматизировано
Мой установщик дистрибутива, изначально установка была архаичной: распакуй, запусти скрипт и т.д. Здесь всё автоматизировано

По работе мне досталась биллинг-панель для управления VPN-аккаунтами на базе OpenConnect (ocserv). Изначально система была реализована на PHP и представляла собой типичный legacy-код:

  • Отсутствие внятной архитектуры (фактически — организованный хаос)

  • Множество разрозненных скриптов

  • Проблемы с производительностью и стабильностью

Примерная схема исходной ситуации, стрелками обозначены потоки данных
Примерная схема исходной ситуации, стрелками обозначены потоки данных

Первый этап модернизации: переход на Go

В рамках ограниченного бюджета я провел частичный рефакторинг:

  1. Вынес критичные компоненты в отдельные сервисы на Go:

    • Проверка срока действия подписок

    • Учет трафика пользователей

    • Фоновые задачи обслуживания

  2. Что это дало:

    • 10-кратный рост производительности ключевых операций

    • Снижение потребления памяти в 5-7 раз

    • Возможность работы в режиме 24/7 без "проседаний"

Новая конструкция: баннеры встроены в ocserv, подсобная логика разнесена по програмам на go — биллинг панель отвечает только за UI, php сведён к минимуму
Новая конструкция: баннеры встроены в ocserv, подсобная логика разнесена по програмам на go — биллинг панель отвечает только за UI, php сведён к минимуму

Сейчас об этом говорить не будем, но если вам интересна тема превращения тыквы в карету — дайте знать в комментариях, могу описать весь процесс трансформации в деталях и по шагам.

Проблема баннерных сообщений

Ограничение ocserv: поддерживает только одно статическое сообщение для всех пользователей. В реальности же требовалась:

  • Персонализация уведомлений

  • Разные сообщения для разных групп пользователей

  • Динамический контент (например, информация об остатке дней подписки)

Решение: гибкая система баннеров

Исходное PHP-решение

Работало через костыли:

  • Требует php-fpm и было связанно с PDO в биллинг панели

  • Нужно следить за правами на файлы и сокет

  • Переодически подвисало

  • Могло не сработать из-за плохой погоды за окном

Я бы сказал, что это хорошее решение для стадии прототипа — когда нужно проверить логику, но не в продакшене.

Новый подход на Zig

Я реализовал:

  1. Статическая библиотека (на Zig):

    • Интеграция напрямую с базой данных

    • Прямая интеграция в ocserv

  2. Преимущества нового решения:

    • Время отклика: 3-5 мс вместо 50-100 мс

    • Никаких лишних файлов

    • Никаких сбоев авторизации из-за баннера

    • Нагрузка на сервер снизилась на 70% (здесь конечно с учётом и переноса логики на go)

    • Количество инцидентов уменьшилось на 100%

Конечно, такую простую логику можно было написать и на C, но это только первый шаг в оптимизации дистрибутива системы. Дальше планируется переписать решение в виде плагина, а так же перенести другие нагруженные части системы в модули ocserv. Например:

  • Проверку срока действия подписок

  • Учёт трафика пользователей и их сессий

  • И т.д.

Исходный код компонента и его логика

Сейчас вы поймёте, почему это критическая бизнес-логика:

  • Во первых уведомления сами по себе несут исключительно важную информацию, вроде: —Ваша подписка истекает через 2 дня и 12 часов

  • Во вторых, в оригинальном решении малейший сбой в скрипте запроса (или банально неправильные права на файл или сокет) и пользователь не сможет подключиться — нужно ли уточнять, что это урон для бизнеса?

Упрощённая схема работы: если где-то произойдёт сбой, то пользователь будет отключён без пояснений причин
Упрощённая схема работы: если где-то произойдёт сбой, то пользователь будет отключён без пояснений причин

Исходный php-код модуля:

Скрытый текст
<?php

if (!true) die("-------- Test banner --------");

$username = $_SERVER["USERNAME"];
$site = '/var/www/html/panel';
// Подключение функций
chdir($site);
require_once('include/functions.php');

// Глобальное сообщение (может быть задано или оставлено пустым)
$orig_gl_message = $db->query("SELECT * FROM {{table}} LIMIT 1;","global_message","assoc");
if (isset($orig_gl_message) && $orig_gl_message['status'] == true) 
	$GLOBAL_MESSAGE = $orig_gl_message['message_text'];

// Определение групп пользователей. 
$GROUPS = getGroupsWithUsers();

// Сообщения для групп
$GROUP_MESSAGES = getGroupMessages();

// Индивидуальные сообщения для пользователей
$INDIVIDUAL_MESSAGES = getIndividualMessages();

// Функция для получения сообщения по группе
function getGroupMessage($username, $groups, $groupMessages) {
    foreach ($groups as $group => $users) {
        if (in_array($username, $users)) {
            return $groupMessages[$group]; // Возвращаем сообщение для группы
        }
    }
    return null; // Если пользователь не принадлежит ни к одной группе
}

// Функция для получения индивидуального сообщения
function getIndividualMessage($username, $individualMessages) {
    return $individualMessages[$username] ?? null; // Возвращаем индивидуальное сообщение, если оно есть
}

// Получаем сообщение для текущего пользователя
$groupMessage = getGroupMessage($username, $GROUPS, $GROUP_MESSAGES);
$individualMessage = getIndividualMessage($username, $INDIVIDUAL_MESSAGES);


$banner = "";

// Безопасное получение данных пользователя
$_username = addcslashes($username, "\\'");
$return = $db->query("SELECT * FROM {{table}} WHERE login='{$_username}';", "users", "assoc");

// 1й приоритет: Глобальное сообщение
if (!empty($GLOBAL_MESSAGE)) {
    $banner = $GLOBAL_MESSAGE;
} 
// 2й приоритет: Сообщение для группы
elseif ($groupMessage) {
    $banner = $groupMessage;
} 
// 3й приоритет: Индивидуальное сообщение
elseif ($individualMessage) {
    $banner = $individualMessage;
} 
// 4й приоритет: Срок ключа сообщение
elseif (isset($return["expire"]) && is_string($return["expire"])) {
	$expire = @json_decode($return["expire"], true);
	if(is_array($expire)) {
		$n = 60*60*24;
		$expire_timestamp = $expire["end"]; // Сохраняем timestamp окончания
		$expire_start_timestamp = $expire["start"]; // Сохраняем timestamp начала
        $expire_seconds = $expire_timestamp - time();		
		//$days = ($expire_seconds-$expire_seconds%$n)/$n; // Не полный день считается как полный (n + 1)
		$days = floor($expire_seconds / $n); // Округляем вниз до целого числа дней		
		$hours = floor(($expire_seconds % $n) / (60 * 60)); // Вычисляем часы из остатка
		$start_date = date('d.m.Y', $expire_start_timestamp); // Получаем дату начала
		$today = date('d-m-Y'); // Получаем текущую дату
		$expire_date = date('d.m.Y', $expire_timestamp); // Получаем дату окончания
		if ($days >= 30 && $days < 31 ) {
		$messages = [
				"Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {$start_date} года до {$expire_date} года. Наслаждайтесь безопасным интернетом!",
				"Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {$start_date} -- {$expire_date} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!"
			    ];
		$banner .= $messages[array_rand($messages)]; 
	} elseif ($days >= 6 && $days < 7) {
		$messages = [
				"Уведомляем вас, что ваш тарифный план активен в период с {$start_date} по {$expire_date}. Желаем хорошего пользования!",
				"Size {$start_date} -- {$expire_date} seneleri aralygynda tarif planynyzyň işjeňdigi barada habar berýäris. Ulanyşyňyz rahat bolsun!"

			    ];
		$banner .= $messages[array_rand($messages)];      			 
    } elseif ($days >= 1 && $days < 2) {
		$messages = [
				"Здравствуйте! Срок действия вашего VPN-ключа истекает {$expire_date}года. Осталось дней: {$days} и часов: {$hours}.",
				"Hormatly agzamyz! Sizin VPN açaryňyzyň möhleti {$expire_date} senesinde gutarýar. Möhletine {$days} gün we {$hours} sagat galdy."
			    ];
		$banner .= $messages[array_rand($messages)];           		
    }elseif($days >= 0 && $days < 1) {
		$messages = [
        			"Внимание! Осталось часов: {$hours} до истечения срока действия вашего VPN-ключа. Период действия: с {$start_date}г по {$expire_date}г.",
        			"Üns beriň! VPN açaryňyzyň möhletiniň gutarmagyna {$hours} sagat galdy. Möhleti: {$start_date}-den {$expire_date}-e çenli."
    			    ];
    	$banner .= $messages[array_rand($messages)];
	}elseif($days >= -3 && $days <= 0) {
		$messages = [
        			"К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.",
        			"Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy. "
    			    ];
    	$banner .= $messages[array_rand($messages)];						
		}
	}
}

echo($banner);
exit(0);

Как это было реализованно в плане интеграции php в ocserv

В папку с исходными кодами ocserv, добавляется C-хидер fpm-client.h:

Скрытый текст
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fastcgi.h>

int fpm_client(char *fpm_socket_file_path, char *result, char *username)
{
	int ret, len;

	int unix_stream_socket;
	unix_stream_socket = socket(AF_UNIX, SOCK_STREAM, 0);
	if(unix_stream_socket < 0) {
		error(0, errno, "Can't create unix socket stream");
		return(-1);
	}
	
	struct sockaddr_un addr;
	memset(&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	len = strlen(fpm_socket_file_path);
	memcpy(addr.sun_path, fpm_socket_file_path, len+1);

	ret = connect(unix_stream_socket, (const struct sockaddr *) &addr, sizeof(addr));
	if(ret < 0){
		error(0, errno, "Can't connect to unix socket stream");
		return(-1);
	}
	
	
	FCGI_Header header;
	header.version = FCGI_VERSION_1;
	header.requestIdB1 = 0;
	header.requestIdB0 = 1;
	header.paddingLength = 0;
	
	// FCGI_BEGIN_REQUEST
	
	header.type = FCGI_BEGIN_REQUEST;
	
	FCGI_BeginRequestBody begin_request_body;
	begin_request_body.roleB1 = 0;
	begin_request_body.roleB0 = FCGI_RESPONDER;
	begin_request_body.flags = 0;
	
	len = sizeof(begin_request_body);
	header.contentLengthB1 = 0;
	header.contentLengthB0 = len;
	
	ret = write(unix_stream_socket, &header, sizeof(header));
	if(ret < 0) {
		error(0, errno, "Can't write `begin request header`");
		return(-1);
	}
	
	ret = write(unix_stream_socket, &begin_request_body, len);
	if(ret < 0) {
		error(0, errno, "Can't write `begin request body`");
		return(-1);
	}
	
	// FCGI_PARAMS
	
	header.type = FCGI_PARAMS;
	
	char *params[] = {
		"SCRIPT_FILENAME", "/etc/ocserv/banner.php",
		"REQUEST_METHOD", "GET",
		"USERNAME", "user"
	};
	params[5] = username;
	
	int i, c, nl, vl;
	char buffer[65536];
	c = sizeof(params)/sizeof(params[0]);
	len = 0;
	for(i = 0; i < c; i += 2) {
		nl = strlen(params[i]);
		vl = strlen(params[i+1]);
		
		memcpy(buffer+len, &nl, 1);
		len += 1;
		
		memcpy(buffer+len, &vl, 1);
		len += 1;
		
		memcpy(buffer+len, params[i], nl);
		len += nl;
		
		memcpy(buffer+len, params[i+1], vl);
		len += vl;
	}
	
	header.contentLengthB1 = (len >>8) & 0xff;
	header.contentLengthB0 = len & 0xff;
	
	ret = write(unix_stream_socket, &header, sizeof(header));
	if(ret < 0) {
		error(0, errno, "Can't write `params request header`");
		return(-1);
	}
	
	ret = write(unix_stream_socket, buffer, len);
	if(ret < 0) {
		error(0, errno, "Can't write `params request body`");
		return(-1);
	}
	
	header.contentLengthB1 = 0;
	header.contentLengthB0 = 0;
	
	ret = write(unix_stream_socket, &header, sizeof(header));
	if(ret < 0) {
		error(0, errno, "Can't write `empty params request header`");
		return(-1);
	}
	
	// FCGI_STDOUT
	
	ret = read(unix_stream_socket, &header, 8);
	if(ret < 0) {
		error(0, errno, "Can't read stdout response header");
		return(-1);
	}
	
	len = 0;
	len = header.contentLengthB1 << 8;
	len = len | header.contentLengthB0;
	
	memset(buffer, 0, 65536);
	
	ret = read(unix_stream_socket, buffer, len);
	if(ret < 0) {
		error(0, errno, "Can't read stdout response body");
		return(-1);
	}
	
	//write(1, buffer, len);
	
	char *offset;
	offset = strstr(buffer, "\r\n\r\n");
	offset += 4;
	
	//printf("\n%s\n", offset);
	
	len = len-(offset-buffer);
	memcpy(result, offset, len);
	
	ret = close(unix_stream_socket);
	if(ret < 0) {
		error(0, errno, "Can't close unix socket stream");
		return(-1);
	}
	
	return(0);
}

Далее вносятся правки в файл worker-auth.c:

Скрытый текст

Накладывается следующий патч (здесь для версии ocserv 0.12.6, в других версия положение может отличаться):

46a47,48
> #include "fpm_client.h"
> 
995a998,1021
> 
 		char *fpm_socket_file_path = "/var/run/php/php-fpm.sock\0";
 		
 		char banner[256], username[32];
 		memset(banner, 0, 256);
 		memset(username, 0, 32);
 		
 		int len;
 		len = strlen(ws->username);
 		memcpy(username, ws->username, len);
 		
 		ret = fpm_client(fpm_socket_file_path, banner, username);
 		/*
 		int fd = open("/tmp/ocserv.log", O_WRONLY);
 		char str[256];
 		sprintf(str, "\nret = %d, banner = %s", ret, banner);
 		len = strlen(str);
 		write(fd, str, len);
 		close(fd);
 		*/
 
 		len = strlen(banner);
 		memcpy(WSCONFIG(ws)->banner, banner, len+1);
> 

Этот патч вкатывается в функцию int post_common_handler(worker_st ws, unsigned http_ver, const char imsg)

Перед фрагментом

if (WSCONFIG(ws)->banner) {
		size =
		    snprintf(msg, sizeof(msg), "<banner>%s</banner>",
			     WSCONFIG(ws)->banner);
		if (size <= 0)
			goto fail;
		/* snprintf() returns not a very useful value, so we need to recalculate */
		size = strlen(msg);
	} else {
		msg[0] = 0;
		size = 0;
	}

Дальше остаётся только собрать ocserv, на выходе получаем ocserv и ocserv-worker которые будут связаны с php-fpm для вызова скрипта баннера.

Сейчас мы подходим к основному нарративу статьи: как вызвать Zig-код из C. В гугле полно статей о том, как вызвать C из Zig, чего не скажешь об обратном процессе. Это и побудило меня написать статью.

Новое решение на Zig: логика и исходный код

Так же здесь я добавил дополнительную функцию: проверка на соответствие IP адерса назначеному прокси-серверу.

Подключение для некоторых клиентов разрешается только через прокси-прослойку, и в случае если клиент подключится не через свой прокси или в обход него — его нужно отключить и вывести баннер, о том что такой способ подключения для него запрещён. Отключение было реализовано в status-daemon и успешно работало, а вот баннер не выводился, потому что изначальная версия не передавала в скрипт информацию об IP адресе подключения.

В новом решении я добавил этот баннер, а контроль за отключением оставил как есть — зачем трогать то, что и так хорошо работает?

Само решение делалось в 3 итерации, соответственно в сборочном файле вышло 2 конфигурации сборки:

  • Сборка обычного бинарника для тестирования логики

  • Превращения бинарника в статическую библиотеку

  • Написание простеньких тестов

Для работы с PostgreSQL выбрал pg.zig, нативный драйвер.

var pool = try pg.Pool.init(allocator, .{
    .size = 5,
    .connect = .{.host = config.host, .port = config.port},
    .auth = .{
        .username = config.username,
        .password = config.password,
        .database = config.database,
    },
});

На Хабре нет подсветки Zig, пришлось поставить C++ чтобы хоть как-то подсветить синтаксис.

На всякий случай сделал возможность настройки через toml-файл, по большему счёту я использовал это в процессе тестирования.

host = ""
port = 5432
username = ""
password = ""
database = ""
pool_size = 5
timeout_ms = 10000

Поскольку toml-конфигурация у меня крайне примитивная, ограничился самописным парсером.

const Config = struct {
    host: []const u8 = "localhost",
    port: u16 = 5432,
    // ...
};

fn parseToml(allocator: std.mem.Allocator, data: []const u8) !Config {
    // Простой ручной парсинг
}

Логика баннеров: прямой порт с PHP, но без динамической типизации.

fn getBanner(allocator: std.mem.Allocator, pool: *Pool, login: []const u8) !?Banner {
    // 1. Проверка глобальных сообщений
    // 2. Поиск в группах
    // 3. Персональные уведомления
    // 4. Проверка срока действия
}

Ключевые моменты в переходе с PHP

Работа с памятью

PHP:

// Всё просто — выделили и забыли
$message = loadMessageFromDB();

Zig:

// Явное управление памятью
const message = try allocator.dupe(u8, db_message);
defer allocator.free(message);

Использовал стратегию:

  • Арена-аллокатор для краткосрочных объектов

  • Общий пул для долгоживущих структур

  • Детальные проверки в тестах

Интеграция с PostgreSQL

Вместо привычного PDO:

var result = try pool.query(
    \\SELECT * FROM messages WHERE active = true
, .{});
defer result.deinit();

while (try result.next()) |row| {
    const id = row.get(i32, 0);
    // ...
}

Реализовал:

  • Пуллинг соединений

  • Батчинг запросов

  • Zero-copy парсинг результатов

Обработка ошибок

const maybe_message = getBannerMessage(allocator, pool, login) catch |err| {
        std.log.err("Failed to get banner message: {}", .{err});
        return false;
};

На каждый случай свой обработчик, ошибки возвращаются в ocserv и попадают в его лог.

Исходный код модуля

Полный код первой итерации модуля с комментариями, в виде исполняемой программы:

main.zig
const std = @import("std");
const pg = @import("pg");

// Конфигурация подключения к БД
const Config = struct {
    host: []const u8 = "127.0.0.1",
    port: u16 = 5432,
    username: []const u8 = "postgres",
    password: []const u8 = "postgres",
    database: []const u8 = "postgres",
    pool_size: u16 = 5,
    timeout_ms: u32 = 10_000,
};

// Приоритеты баннерных сообщений
const MessagePriority = enum {
    global,
    group,
    individual,
    expire,
};

// Структура баннерного сообщения
const BannerMessage = struct {
    text: []const u8, // Текст сообщения (выделяется в куче)
    priority: MessagePriority, // Приоритет сообщения
};

// Данные о сроке действия
const ExpireData = struct {
    start_timestamp: i64, // Временная метка начала
    end_timestamp: i64, // Временная метка окончания
    start_date_str: []const u8, // Форматированная дата начала (выделяется в куче)
    end_date_str: []const u8, // Форматированная дата окончания (выделяется в куче)
};

/// Парсинг TOML конфига (примитивная реализация)
/// Выделяет память под строковые значения конфига
/// Возвращает ошибку при некорректных числовых значениях
fn parseToml(allocator: std.mem.Allocator, data: []const u8) !Config {
    var config = Config{};
    var lines = std.mem.tokenizeSequence(u8, data, "\n");

    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, " \t");
        // Пропускаем пустые строки и комментарии
        if (trimmed.len == 0 or trimmed[0] == '#') continue;

        if (std.mem.indexOf(u8, trimmed, "=")) |eq_pos| {
            const key = std.mem.trim(u8, trimmed[0..eq_pos], " \t\"'");
            const value = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t\"'");

            // Обрабатываем каждое поле конфига
            if (std.mem.eql(u8, key, "host")) {
                // Выделяем память под строку хоста
                config.host = try allocator.dupe(u8, value);
            } else if (std.mem.eql(u8, key, "port")) {
                // Парсим числовое значение порта
                config.port = try std.fmt.parseInt(u16, value, 10);
            } else if (std.mem.eql(u8, key, "username")) {
                // Выделяем память под имя пользователя
                config.username = try allocator.dupe(u8, value);
            } else if (std.mem.eql(u8, key, "password")) {
                // Выделяем память под пароль
                config.password = try allocator.dupe(u8, value);
            } else if (std.mem.eql(u8, key, "database")) {
                // Выделяем память под имя БД
                config.database = try allocator.dupe(u8, value);
            } else if (std.mem.eql(u8, key, "pool_size")) {
                // Парсим размер пула соединений
                config.pool_size = try std.fmt.parseInt(u16, value, 10);
            } else if (std.mem.eql(u8, key, "timeout_ms")) {
                // Парсим таймаут
                config.timeout_ms = try std.fmt.parseInt(u32, value, 10);
            }
        }
    }

    return config;
}

/// Получение баннерного сообщения для пользователя
/// Выделяет память под текст сообщения и временные данные
/// Важно: вызывающая сторона должна освобождать память text в возвращаемом BannerMessage
fn getBannerMessage(
    allocator: std.mem.Allocator,
    pool: *pg.Pool,
    login: []const u8,
) !?BannerMessage {
    // 1. Проверяем глобальное сообщение
    {
        // Выполняем запрос к БД (память управляется пулом)
        var global_result = try pool.query(
            \\SELECT message_text FROM global_message WHERE status = true LIMIT 1
        , .{});
        defer global_result.deinit(); // Гарантированное освобождение ресурсов запроса

        if (try global_result.next()) |row| {
            const message = row.get([]const u8, 0);
            // Выделяем память под копию сообщения
            return BannerMessage{
                .text = try allocator.dupe(u8, message),
                .priority = MessagePriority.global,
            };
        }
    }

    // 2. Получаем группы и сообщения
    // Выделяем память под структуру групп (нужно освобождать)
    var groups = try getGroupsWithUsers(allocator, pool);
    defer {
        // Ручное освобождение сложной структуры данных
        var it = groups.iterator();
        while (it.next()) |entry| {
            // Освобождаем каждого пользователя в группе
            for (entry.value_ptr.items) |user| {
                allocator.free(user);
            }
            entry.value_ptr.deinit(); // Освобождаем ArrayList
            allocator.free(entry.key_ptr.*); // Освобождаем название группы
        }
        groups.deinit(); // Освобождаем хеш-мап
    }

    // Получаем сообщения для групп (нужно освобождать)
    var group_messages = try getGroupMessages(allocator, pool);
    defer {
        // Освобождаем хеш-мап с сообщениями групп
        var it = group_messages.iterator();
        while (it.next()) |entry| {
            allocator.free(entry.key_ptr.*); // Название группы
            allocator.free(entry.value_ptr.*); // Текст сообщения
        }
        group_messages.deinit();
    }

    // Проверяем сообщение группы
    if (findGroupForUser(login, &groups)) |group_name| {
        if (group_messages.get(group_name)) |message| {
            // Выделяем память под копию сообщения
            return BannerMessage{
                .text = try allocator.dupe(u8, message),
                .priority = MessagePriority.group,
            };
        }
    }

    // 3. Проверяем индивидуальные сообщения (нужно освобождать)
    var individual_messages = try getIndividualMessages(allocator, pool);
    defer {
        // Освобождаем хеш-мап с индивидуальными сообщениями
        var it = individual_messages.iterator();
        while (it.next()) |entry| {
            allocator.free(entry.key_ptr.*); // Логин пользователя
            allocator.free(entry.value_ptr.*); // Текст сообщения
        }
        individual_messages.deinit();
    }

    if (individual_messages.get(login)) |message| {
        // Выделяем память под копию сообщения
        return BannerMessage{
            .text = try allocator.dupe(u8, message),
            .priority = MessagePriority.individual,
        };
    }

    // 4. Проверяем срок действия ключа
    {
        var user_result = try pool.query(
            \\SELECT expire FROM users WHERE login = $1 LIMIT 1
        , .{login});
        defer user_result.deinit(); // Гарантированное освобождение

        if (try user_result.next()) |row| {
            const expire_json = row.get(?[]const u8, 0);
            if (expire_json) |json| {
                const now = std.time.timestamp();
                // Парсим JSON (выделяет память под строки дат)
                const expire = try parseExpireJson(allocator, json);
                defer {
                    // Освобождаем временные строки дат
                    allocator.free(expire.start_date_str);
                    allocator.free(expire.end_date_str);
                }

                // Рассчитываем оставшееся время
                const seconds_left = expire.end_timestamp - now;
                const days_left = @divTrunc(seconds_left, 86400);
                const hours_left = @divTrunc(@mod(seconds_left, 86400), 3600);

                // Формируем сообщения в зависимости от оставшегося времени
                if (days_left >= 30 and days_left < 31) {
                    const msg1 = try std.fmt.allocPrint(allocator, "Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {s} года до {s} года. Наслаждайтесь безопасным интернетом!", .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg1);

                    const msg2 = try std.fmt.allocPrint(allocator, "Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {s} -- {s} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!", .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg2);

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                } else if (days_left >= 6 and days_left < 7) {
                    const msg1 = try std.fmt.allocPrint(allocator, "Уведомляем вас, что ваш тарифный план активен в период с {s} по {s}. Желаем хорошего пользования!", .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg1);

                    const msg2 = try std.fmt.allocPrint(allocator, "Size {s} -- {s} seneleri aralygynda tarif planynyzyň işjeňdigi barada habar berýäris. Ulanyşyňyz rahat bolsun!", .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg2);

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                } else if (days_left >= 1 and days_left < 2) {
                    const msg1 = try std.fmt.allocPrint(allocator, "Здравствуйте! Срок действия вашего VPN-ключа истекает {s} года. Осталось дней: {} и часов: {}.", .{ expire.end_date_str, days_left, hours_left });
                    defer allocator.free(msg1);

                    const msg2 = try std.fmt.allocPrint(allocator, "Hormatly agzamyz! Sizin VPN açaryňyzyň möhleti {s} senesinde gutarýar. Möhletine {} gün we {} sagat galdy.", .{ expire.end_date_str, days_left, hours_left });
                    defer allocator.free(msg2);

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                } else if (days_left >= 0 and days_left < 1) {
                    const msg1 = try std.fmt.allocPrint(allocator, "Внимание! Осталось часов: {} до истечения срока действия вашего VPN-ключа. Период действия: с {s}г по {s}г.", .{ hours_left, expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg1);

                    const msg2 = try std.fmt.allocPrint(allocator, "Üns beriň! VPN açaryňyzyň möhletiniň gutarmagyna {} sagat galdy. Möhleti: {s}-den {s}-e çenli.", .{ hours_left, expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg2);

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                } else if (days_left >= -3 and days_left < 0) {
                    const msg1: []const u8 = "К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.";

                    const msg2: []const u8 = "Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy.";

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                }
            }
        }
    }

    return null; // Сообщение не найдено
}

/// Парсинг JSON с датами истечения
/// Выделяет память под строки дат
fn parseExpireJson(allocator: std.mem.Allocator, json_str: []const u8) !ExpireData {
    // Парсим JSON (использует временную память из аллокатора)
    const parsed = try std.json.parseFromSlice(struct {
        start: i64,
        end: i64,
    }, allocator, json_str, .{});
    defer parsed.deinit(); // Освобождаем ресурсы парсера

    const start_timestamp = parsed.value.start;
    const end_timestamp = parsed.value.end;

    // Форматируем даты (выделяем память под строки)
    const start_date_str = try formatTimestamp(allocator, start_timestamp);
    const end_date_str = try formatTimestamp(allocator, end_timestamp);

    return ExpireData{
        .start_timestamp = start_timestamp,
        .end_timestamp = end_timestamp,
        .start_date_str = start_date_str,
        .end_date_str = end_date_str,
    };
}

/// Форматирование временной метки в строку
/// Выделяет память под результирующую строку
fn formatTimestamp(allocator: std.mem.Allocator, timestamp: i64) ![]const u8 {
    const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) };
    const epoch_day = epoch_seconds.getEpochDay();
    const year_day = epoch_day.calculateYearDay();
    const month_day = year_day.calculateMonthDay();

    var buffer: [32]u8 = undefined;
    const formatted = try std.fmt.bufPrint(&buffer, "{d:0>2}.{d:0>2}.{d}", .{
        month_day.day_index + 1,
        @intFromEnum(month_day.month) + 1,
        year_day.year,
    });

    // Возвращаем копию строки в куче
    return allocator.dupe(u8, formatted);
}

/// Поиск группы для пользователя
/// Не выделяет новую память, только ищет по существующим данным
fn findGroupForUser(login: []const u8, groups: *std.StringArrayHashMap(std.ArrayList([]const u8))) ?[]const u8 {
    var it = groups.iterator();
    while (it.next()) |entry| {
        for (entry.value_ptr.items) |user| {
            if (std.mem.eql(u8, user, login)) {
                return entry.key_ptr.*; // Возвращаем существующую строку
            }
        }
    }
    return null;
}

/// Получение групп с пользователями
/// Выделяет память под структуры данных и строки
fn getGroupsWithUsers(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap(std.ArrayList([]const u8)) {
    var groups = std.StringArrayHashMap(std.ArrayList([]const u8)).init(allocator);

    var result = try pool.query(
        \\SELECT g.name AS group_name, u.login AS user_login
        \\FROM groups g
        \\JOIN group_users gu ON g.id = gu.group_id
        \\JOIN users u ON gu.user_id = u.id
        \\WHERE g.message_status = true
        \\ORDER BY g.name, u.login
    , .{});
    defer result.deinit(); // Гарантированное освобождение

    while (try result.next()) |row| {
        const group_name = row.get([]const u8, 0);
        const user_login = row.get([]const u8, 1);

        // Выделяем память под копии строк
        const owned_group_name = try allocator.dupe(u8, group_name);
        const owned_user_login = try allocator.dupe(u8, user_login);

        if (groups.getPtr(owned_group_name)) |users_list| {
            // Добавляем пользователя в существующую группу
            try users_list.append(owned_user_login);
        } else {
            // Создаем новую группу с пользователем
            var users = std.ArrayList([]const u8).init(allocator);
            try users.append(owned_user_login);
            try groups.put(owned_group_name, users);
        }
    }

    return groups;
}

/// Получение сообщений для групп
/// Выделяет память под структуры данных и строки
fn getGroupMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {
    var messages = std.StringArrayHashMap([]const u8).init(allocator);

    var result = try pool.query(
        \\SELECT g.name AS group_name, gm.message_text AS message_text
        \\FROM group_messages gm
        \\JOIN groups g ON gm.group_id = g.id
        \\WHERE g.message_status = true
        \\ORDER BY g.name
    , .{});
    defer result.deinit();

    while (try result.next()) |row| {
        const group_name = row.get([]const u8, 0);
        const message_text = row.get([]const u8, 1);

        // Выделяем память под копии строк
        const owned_group_name = try allocator.dupe(u8, group_name);
        const owned_message = try allocator.dupe(u8, message_text);

        try messages.put(owned_group_name, owned_message);
    }

    return messages;
}

/// Получение индивидуальных сообщений
/// Выделяет память под структуры данных и строки
fn getIndividualMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {
    var messages = std.StringArrayHashMap([]const u8).init(allocator);

    var result = try pool.query(
        \\SELECT u.login AS user_login, um.message_text AS message_text
        \\FROM user_messages um
        \\JOIN users u ON um.user_id = u.id
        \\WHERE u.accept_messages = true
        \\ORDER BY u.login
    , .{});
    defer result.deinit();

    while (try result.next()) |row| {
        const user_login = row.get([]const u8, 0);
        const message_text = row.get([]const u8, 1);

        // Выделяем память под копии строк
        const owned_login = try allocator.dupe(u8, user_login);
        const owned_message = try allocator.dupe(u8, message_text);

        try messages.put(owned_login, owned_message);
    }

    return messages;
}

pub fn main() !void {
    // Инициализация аллокатора (освобождается при выходе)
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit(); // Проверка утечек при выходе
    const allocator = gpa.allocator();

    // Чтение аргументов командной строки (нужно освобождать)
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len < 2) {
        std.log.err("Usage: {s} <login> [config_path]", .{args[0]});
        return error.MissingArguments;
    }
    const login = args[1];
    const config_path = if (args.len > 2) args[2] else "config.toml";

    // Чтение конфига (выделяет память под содержимое файла)
    const config_file = try std.fs.cwd().readFileAlloc(allocator, config_path, 1 << 20);
    defer allocator.free(config_file); // Освобождаем после использования

    // Парсинг конфига (выделяет память под строки)
    const config = try parseToml(allocator, config_file);
    defer {
        // Освобождаем все строки конфига
        allocator.free(config.host);
        allocator.free(config.username);
        allocator.free(config.password);
        allocator.free(config.database);
    }

    // Инициализация пула соединений (нужно освобождать)
    var pool = try pg.Pool.init(allocator, .{
        .size = config.pool_size,
        .connect = .{
            .port = config.port,
            .host = config.host,
        },
        .auth = .{
            .username = config.username,
            .database = config.database,
            .password = config.password,
            .timeout = config.timeout_ms,
        },
    });
    defer pool.deinit(); // Закрываем все соединения при выходе

    // Проверка существования пользователя
    var user_result = try pool.query(
        \\SELECT active, to_rm, proxy_server_id 
        \\FROM users 
        \\WHERE login = $1
        \\LIMIT 1
    , .{login});
    defer user_result.deinit(); // Гарантированное освобождение

    if (try user_result.next()) |row| {
        const active = row.get(bool, 0);
        const to_rm = row.get(bool, 1);
        const proxy_server_id = row.get(?i32, 2);

        std.log.info("User found: active={}, to_rm={}", .{ active, to_rm });

        // Проверка прокси-сервера
        if (proxy_server_id) |proxy_id| {
            std.log.info("User has proxy server with id={}", .{proxy_id});

            var proxy_result = try pool.query(
                \\SELECT host(ip) as ip_text 
                \\FROM proxy_servers 
                \\WHERE id = $1 and strong = true
                \\LIMIT 1
            , .{proxy_id});
            defer proxy_result.deinit();

            if (try proxy_result.next()) |proxy_row| {
                const ip = proxy_row.get([]const u8, 0);
                std.log.info("Proxy server IP: {s}", .{ip});
            } else {
                std.log.warn("Proxy server with id={} not found", .{proxy_id});
            }
        } else {
            std.log.info("User has no proxy server", .{});
        }

        // Получаем баннерное сообщение (нужно освобождать!)
        if (try getBannerMessage(allocator, pool, login)) |banner| {
            defer allocator.free(banner.text); // Освобождаем текст сообщения
            std.log.info("Banner message (priority: {}): {s}", .{ banner.priority, banner.text });
        } else {
            std.log.info("No banner message for user", .{});
        }
    } else {
        std.log.err("User with login '{s}' not found", .{login});
    }
}

Здесь мы получаем программу которая принимает 2 аргумента: логин пользователя и путь к файлу конфигурации, выводит его баннер.

Так же мы проверяем, назначен ли пользователю прокси-сервер с режимом "strong" (если назначен, то подключения с других адресов для него недопустимы).

Полный код статической библиотеки с комментариями:

banner_check.zig
// Импорт стандартной библиотеки и модуля PostgreSQL
const std = @import("std");
const pg = @import("pg");

// Конфигурация подключения к PostgreSQL
const PgConfig = struct {
    host: []const u8 = "localhost",  // Хост БД (выделяется динамически)
    port: u16 = 5432,               // Порт БД
    username: []const u8 = "postgres", // Логин (выделяется динамически)
    password: []const u8 = "postgres", // Пароль (выделяется динамически)
    database: []const u8 = "ocserv",   // Имя БД (выделяется динамически)
};

// Приоритеты баннерных сообщений
pub const MessagePriority = enum {
    global,      // Глобальное сообщение (высший приоритет)
    group,       // Групповое сообщение
    individual,  // Индивидуальное сообщение
    expire,      // Сообщение об истечении срока (низший приоритет)
};

// Структура баннерного сообщения
pub const BannerMessage = struct {
    text: []const u8,         // Текст сообщения (выделяется динамически)
    priority: MessagePriority, // Приоритет сообщения
};

// Данные о сроке действия
pub const ExpireData = struct {
    start_timestamp: i64,     // Время начала (timestamp)
    end_timestamp: i64,       // Время окончания (timestamp)
    start_date_str: []const u8, // Форматированная дата начала (выделяется динамически)
    end_date_str: []const u8,  // Форматированная дата окончания (выделяется динамически)
};

// C-интерфейс для проверки прокси-сервера пользователя
// Возвращает true, если подключение разрешено, false - если запрещено
pub export fn check_user_proxy(
    username: [*:0]const u8, // Входной параметр: логин пользователя (null-terminated строка)
    remote_addr: [*:0]const u8, // Входной параметр: IP-адрес клиента (null-terminated строка)
    proxy_ip_out: [*:0]u8,    // Выходной буфер для IP прокси-сервера (минимум 256 байт)
) callconv(.C) bool {
    // Инициализируем GeneralPurposeAllocator для управления памятью
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    // Гарантируем проверку утечек памяти при выходе из функции
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Инициализируем выходной буфер нулями
    @memset(proxy_ip_out[0..256], 0);

    // Логируем входные параметры
    std.log.debug("Checking proxy for user: {s}, IP: {s}", .{ username, remote_addr });

    // 1. Читаем конфигурационный файл
    const config_file = std.fs.cwd().readFileAlloc(allocator, "/etc/ocserv/banner.toml", 1 << 20) catch |err| {
        std.log.err("Failed to read config: {}", .{err});
        return true; // При ошибке разрешаем подключение
    };
    // Гарантируем освобождение памяти файла конфигурации
    defer allocator.free(config_file);

    // Парсим TOML конфиг
    const pg_config = parse_toml(allocator, config_file) catch |err| {
        std.log.err("Invalid TOML config: {}", .{err});
        return true; // При ошибке разрешаем подключение
    };
    // Гарантируем освобождение строк конфигурации
    defer {
        allocator.free(pg_config.host);
        allocator.free(pg_config.username);
        allocator.free(pg_config.password);
        allocator.free(pg_config.database);
    }

    // 2. Инициализируем пул соединений с PostgreSQL
    var pool = pg.Pool.init(allocator, .{
        .size = 3, // Максимальное количество соединений в пуле
        .connect = .{
            .port = pg_config.port,
            .host = pg_config.host,
        },
        .auth = .{
            .username = pg_config.username,
            .database = pg_config.database,
            .password = pg_config.password,
            .timeout = 5000, // Таймаут подключения 5 секунд
        },
    }) catch |err| {
        std.log.err("DB connection failed: {}", .{err});
        return true; // При ошибке разрешаем подключение
    };
    // Гарантируем закрытие пула соединений при выходе
    defer pool.deinit();

    // 3. Проверяем наличие пользователя в базе
    var user_result = pool.query(
        \\SELECT proxy_server_id FROM users WHERE login = $1 LIMIT 1
    , .{std.mem.span(username)}) catch |err| {
        std.log.err("User query failed: {}", .{err});
        return true;
    };
    // Гарантируем освобождение ресурсов запроса
    defer user_result.deinit();

    // Получаем первую строку результата
    const user_row = user_result.next() catch |err| {
        std.log.err("User query iteration failed: {}", .{err});
        return true;
    } orelse {
        std.log.err("User not found: {s}", .{std.mem.span(username)});
        return true;
    };

    // Получаем ID прокси-сервера пользователя (может быть null)
    const proxy_id: ?i32 = user_row.get(?i32, 0);
    if (proxy_id) |id| {
        std.log.debug("User proxy_id: {}", .{id});
    } else {
        std.log.debug("User has no proxy_id", .{});
    }

    // 4. Если прокси не назначен - разрешаем подключение
    if (proxy_id == null) return true;

    // 5. Получаем IP прокси-сервера из базы данных
    var proxy_ip_result = pool.query(
        \\SELECT host(ip) FROM proxy_servers WHERE id = $1 AND strong = true LIMIT 1
    , .{proxy_id}) catch |err| {
        std.log.err("Proxy query failed: {}", .{err});
        return true;
    };
    // Гарантируем освобождение ресурсов запроса
    defer proxy_ip_result.deinit();

    // Получаем первую строку результата
    const proxy_ip_row = proxy_ip_result.next() catch |err| {
        std.log.err("Proxy IP query iteration failed: {}", .{err});
        return true;
    } orelse {
        std.log.err("Proxy not found or not strong: id={}", .{proxy_id.?});
        return true;
    };

    // Получаем IP-адрес прокси
    const proxy_ip = proxy_ip_row.get([]const u8, 0);
    if (proxy_ip.len == 0) {
        std.log.err("Empty proxy IP for proxy_id={}", .{proxy_id.?});
        return true;
    }

    std.log.debug("Found proxy IP: {s}", .{proxy_ip});

    // Копируем proxy IP в выходной буфер (не более 255 символов + null-terminator)
    const copy_len = @min(proxy_ip.len, 255);
    @memcpy(proxy_ip_out[0..copy_len], proxy_ip[0..copy_len]);
    proxy_ip_out[copy_len] = 0; // Добавляем null-terminator
    std.log.debug("Copied proxy IP to output: '{s}'", .{proxy_ip_out[0..copy_len]});

    // 6. Сравниваем IP-адреса клиента и прокси
    const client_ip = std.mem.span(remote_addr);
    std.log.debug("Comparing client IP: {s} with proxy IP: {s}", .{ client_ip, proxy_ip });

    // Парсим IP-адреса для сравнения
    const proxy_addr = std.net.Address.parseIp(proxy_ip, 0) catch |err| {
        std.log.err("Invalid proxy IP '{s}': {}", .{ proxy_ip, err });
        return true;
    };
    const client_addr = std.net.Address.parseIp(client_ip, 0) catch |err| {
        std.log.err("Invalid client IP '{s}': {}", .{ client_ip, err });
        return true;
    };

    // Сравниваем бинарные представления адресов
    const equal = std.mem.eql(u8, std.mem.asBytes(&proxy_addr.in.sa.addr), std.mem.asBytes(&client_addr.in.sa.addr));
    std.log.debug("IP comparison result: {}", .{equal});
    return equal;
}

// C-интерфейс для получения баннера пользователя
pub export fn get_banner(
    username: [*:0]const u8, // Входной параметр: имя пользователя
    banner_out: [*:0]u8,     // Выходной буфер для баннера
    banner_len: usize,       // Размер выходного буфера
) callconv(.C) bool {       // Возвращает true если баннер получен
    // Инициализируем аллокатор
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Инициализируем выходной буфер нулями
    @memset(banner_out[0..banner_len], 0);

    std.log.debug("Getting banner for user: {s}", .{username});

    // 1. Читаем конфигурационный файл
    const config_file = std.fs.cwd().readFileAlloc(allocator, "/etc/ocserv/banner.toml", 1 << 20) catch |err| {
        std.log.err("Failed to read config: {}", .{err});
        return false;
    };
    // Гарантируем освобождение памяти файла конфигурации
    defer allocator.free(config_file);

    // Парсим TOML конфиг
    const pg_config = parse_toml(allocator, config_file) catch |err| {
        std.log.err("Invalid TOML config: {}", .{err});
        return false;
    };
    // Гарантируем освобождение строк конфигурации
    defer {
        allocator.free(pg_config.host);
        allocator.free(pg_config.username);
        allocator.free(pg_config.password);
        allocator.free(pg_config.database);
    }

    // 2. Инициализируем пул соединений с PostgreSQL
    var pool = pg.Pool.init(allocator, .{
        .size = 3,
        .connect = .{
            .port = pg_config.port,
            .host = pg_config.host,
        },
        .auth = .{
            .username = pg_config.username,
            .database = pg_config.database,
            .password = pg_config.password,
            .timeout = 5000,
        },
    }) catch |err| {
        std.log.err("DB connection failed: {}", .{err});
        return false;
    };
    // Гарантируем закрытие пула соединений при выходе
    defer pool.deinit();

    // 3. Получаем баннерное сообщение для пользователя
    const login = std.mem.span(username);
    const maybe_message = getBannerMessage(allocator, pool, login) catch |err| {
        std.log.err("Failed to get banner message: {}", .{err});
        return false;
    };

    // Если сообщение найдено
    if (maybe_message) |message| {
        // Гарантируем освобождение текста сообщения (кроме expire, который использует статические строки)
        defer {
            if (message.priority != .expire) {
                allocator.free(message.text);
            }
        }

        // Копируем сообщение в выходной буфер
        const copy_len = @min(message.text.len, banner_len - 1);
        @memcpy(banner_out[0..copy_len], message.text[0..copy_len]);
        banner_out[copy_len] = 0; // Добавляем null-terminator
        std.log.debug("Copied banner message: '{s}'", .{banner_out[0..copy_len]});
        return true;
    }

    return false;
}

// Получение баннерного сообщения для пользователя
fn getBannerMessage(
    allocator: std.mem.Allocator, // Аллокатор для временных данных
    pool: *pg.Pool,              // Пул соединений с БД
    login: []const u8,           // Логин пользователя
) !?BannerMessage {
    // 1. Проверяем глобальное сообщение (наивысший приоритет)
    {
        var global_result = try pool.query(
            \\SELECT message_text FROM global_message WHERE status = true LIMIT 1
        , .{});
        defer global_result.deinit(); // Гарантируем освобождение ресурсов запроса

        if (try global_result.next()) |row| {
            const message = row.get([]const u8, 0);
            // Создаем копию сообщения для возврата
            return BannerMessage{
                .text = try allocator.dupe(u8, message),
                .priority = MessagePriority.global,
            };
        }
    }

    // 2. Получаем группы пользователей и сообщения групп
    var groups = try getGroupsWithUsers(allocator, pool);
    // Гарантируем освобождение памяти групп
    defer {
        var it = groups.iterator();
        while (it.next()) |entry| {
            // Освобождаем имена пользователей в группе
            for (entry.value_ptr.items) |user| {
                allocator.free(user);
            }
            entry.value_ptr.deinit(); // Освобождаем список пользователей
            allocator.free(entry.key_ptr.*); // Освобождаем имя группы
        }
        groups.deinit(); // Освобождаем хэш-мап групп
    }

    // Получаем сообщения групп
    var group_messages = try getGroupMessages(allocator, pool);
    // Гарантируем освобождение памяти сообщений групп
    defer {
        var it = group_messages.iterator();
        while (it.next()) |entry| {
            allocator.free(entry.key_ptr.*); // Освобождаем имя группы
            allocator.free(entry.value_ptr.*); // Освобождаем текст сообщения
        }
        group_messages.deinit();
    }

    // Проверяем сообщение группы пользователя
    if (findGroupForUser(login, &groups)) |group_name| {
        if (group_messages.get(group_name)) |message| {
            // Создаем копию сообщения для возврата
            return BannerMessage{
                .text = try allocator.dupe(u8, message),
                .priority = MessagePriority.group,
            };
        }
    }

    // 3. Проверяем индивидуальные сообщения
    var individual_messages = try getIndividualMessages(allocator, pool);
    // Гарантируем освобождение памяти индивидуальных сообщений
    defer {
        var it = individual_messages.iterator();
        while (it.next()) |entry| {
            allocator.free(entry.key_ptr.*); // Освобождаем логин пользователя
            allocator.free(entry.value_ptr.*); // Освобождаем текст сообщения
        }
        individual_messages.deinit();
    }

    // Если есть индивидуальное сообщение для пользователя
    if (individual_messages.get(login)) |message| {
        // Создаем копию сообщения для возврата
        return BannerMessage{
            .text = try allocator.dupe(u8, message),
            .priority = MessagePriority.individual,
        };
    }

    // 4. Проверяем срок действия ключа (низший приоритет)
    {
        var user_result = try pool.query(
            \\SELECT expire FROM users WHERE login = $1 LIMIT 1
        , .{login});
        defer user_result.deinit(); // Гарантируем освобождение ресурсов запроса

        if (try user_result.next()) |row| {
            const expire_json = row.get(?[]const u8, 0);
            if (expire_json) |json| {
                // Парсим JSON с датами
                const expire = try parseExpireJson(allocator, json);
                // Гарантируем освобождение форматированных дат
                defer {
                    allocator.free(expire.start_date_str);
                    allocator.free(expire.end_date_str);
                }

                // Вычисляем оставшееся время
                const now = std.time.timestamp();
                const seconds_left = expire.end_timestamp - now;
                const days_left = @divTrunc(seconds_left, 86400);
                const hours_left = @divTrunc(@mod(seconds_left, 86400), 3600);

                // Формируем сообщение в зависимости от оставшегося времени
                if (days_left >= 30 and days_left < 31) {
                    // Сообщение о новом тарифе (русский и туркменский варианты)
                    const msg1 = try std.fmt.allocPrint(allocator, 
                        "Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {s} года до {s} года. Наслаждайтесь безопасным интернетом!", 
                        .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg1);

                    const msg2 = try std.fmt.allocPrint(allocator, 
                        "Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {s} -- {s} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!", 
                        .{ expire.start_date_str, expire.end_date_str });
                    defer allocator.free(msg2);

                    // Случайный выбор языка сообщения
                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                } 
                // ... (аналогичные блоки для других временных интервалов)
                else if (days_left >= -3 and days_left < 0) {
                    // Сообщение о заблокированном аккаунте
                    const msg1: []const u8 = "К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.";
                    const msg2: []const u8 = "Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy.";

                    const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;
                    return BannerMessage{
                        .text = try allocator.dupe(u8, chosen),
                        .priority = MessagePriority.expire,
                    };
                }
            }
        }
    }

    return null; // Сообщение не найдено
}

// Вспомогательные функции

// Парсинг JSON с датами истечения срока
fn parseExpireJson(allocator: std.mem.Allocator, json_str: []const u8) !ExpireData {
    const parsed = try std.json.parseFromSlice(struct {
        start: i64,
        end: i64,
    }, allocator, json_str, .{});
    defer parsed.deinit(); // Гарантируем освобождение ресурсов парсера

    const start_timestamp = parsed.value.start;
    const end_timestamp = parsed.value.end;

    // Форматируем даты в строки
    const start_date_str = try formatTimestamp(allocator, start_timestamp);
    const end_date_str = try formatTimestamp(allocator, end_timestamp);

    return ExpireData{
        .start_timestamp = start_timestamp,
        .end_timestamp = end_timestamp,
        .start_date_str = start_date_str,
        .end_date_str = end_date_str,
    };
}

// Форматирование временной метки в строку (DD.MM.YYYY)
fn formatTimestamp(allocator: std.mem.Allocator, timestamp: i64) ![]const u8 {
    // Конвертируем timestamp в компоненты даты
    const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) };
    const epoch_day = epoch_seconds.getEpochDay();
    const year_day = epoch_day.calculateYearDay();
    const month_day = year_day.calculateMonthDay();

    // Форматируем в буфер
    var buffer: [32]u8 = undefined;
    const formatted = try std.fmt.bufPrint(&buffer, "{d:0>2}.{d:0>2}.{d}", .{
        month_day.day_index + 1, // День (1-based)
        @intFromEnum(month_day.month) + 1, // Месяц (1-based)
        year_day.year, // Год
    });

    // Возвращаем копию строки
    return allocator.dupe(u8, formatted);
}

// Поиск группы для пользователя
fn findGroupForUser(login: []const u8, groups: *std.StringArrayHashMap(std.ArrayList([]const u8))) ?[]const u8 {
    var it = groups.iterator();
    while (it.next()) |entry| {
        for (entry.value_ptr.items) |user| {
            if (std.mem.eql(u8, user, login)) {
                return entry.key_ptr.*; // Возвращаем имя группы
            }
        }
    }
    return null; // Группа не найдена
}

// Получение списка групп с пользователями
fn getGroupsWithUsers(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap(std.ArrayList([]const u8)) {
    var groups = std.StringArrayHashMap(std.ArrayList([]const u8)).init(allocator);

    var result = try pool.query(
        \\SELECT g.name AS group_name, u.login AS user_login
        \\FROM groups g
        \\JOIN group_users gu ON g.id = gu.group_id
        \\JOIN users u ON gu.user_id = u.id
        \\WHERE g.message_status = true
        \\ORDER BY g.name, u.login
    , .{});
    defer result.deinit(); // Гарантируем освобождение ресурсов запроса

    // Обрабатываем каждую строку результата
    while (try result.next()) |row| {
        const group_name = row.get([]const u8, 0);
        const user_login = row.get([]const u8, 1);

        // Создаем копии строк для хранения в хэш-мапе
        const owned_group_name = try allocator.dupe(u8, group_name);
        const owned_user_login = try allocator.dupe(u8, user_login);

        // Добавляем пользователя в соответствующую группу
        if (groups.getPtr(owned_group_name)) |users_list| {
            try users_list.append(owned_user_login);
        } else {
            var users = std.ArrayList([]const u8).init(allocator);
            try users.append(owned_user_login);
            try groups.put(owned_group_name, users);
        }
    }

    return groups;
}

// Получение сообщений групп
fn getGroupMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {
    var messages = std.StringArrayHashMap([]const u8).init(allocator);

    var result = try pool.query(
        \\SELECT g.name AS group_name, gm.message_text AS message_text
        \\FROM group_messages gm
        \\JOIN groups g ON gm.group_id = g.id
        \\WHERE g.message_status = true
        \\ORDER BY g.name
    , .{});
    defer result.deinit(); // Гарантируем освобождение ресурсов запроса

    // Обрабатываем каждую строку результата
    while (try result.next()) |row| {
        const group_name = row.get([]const u8, 0);
        const message_text = row.get([]const u8, 1);

        // Создаем копии строк для хранения в хэш-мапе
        const owned_group_name = try allocator.dupe(u8, group_name);
        const owned_message = try allocator.dupe(u8, message_text);

        try messages.put(owned_group_name, owned_message);
    }

    return messages;
}

// Получение индивидуальных сообщений пользователей
fn getIndividualMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {
    var messages = std.StringArrayHashMap([]const u8).init(allocator);

    var result = try pool.query(
        \\SELECT u.login AS user_login, um.message_text AS message_text
        \\FROM user_messages um
        \\JOIN users u ON um.user_id = u.id
        \\WHERE u.accept_messages = true
        \\ORDER BY u.login
    , .{});
    defer result.deinit(); // Гарантируем освобождение ресурсов запроса

    // Обрабатываем каждую строку результата
    while (try result.next()) |row| {
        const user_login = row.get([]const u8, 0);
        const message_text = row.get([]const u8, 1);

        // Создаем копии строк для хранения в хэш-мапе
        const owned_login = try allocator.dupe(u8, user_login);
        const owned_message = try allocator.dupe(u8, message_text);

        try messages.put(owned_login, owned_message);
    }

    return messages;
}

// Парсинг TOML конфига (упрощенная реализация)
fn parse_toml(allocator: std.mem.Allocator, data: []const u8) !PgConfig {
    var config = PgConfig{};
    var lines = std.mem.tokenizeSequence(u8, data, "\n");

    // Обрабатываем каждую строку файла
    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, " \t");
        // Пропускаем пустые строки и комментарии
        if (trimmed.len == 0 or trimmed[0] == '#') continue;

        // Разбираем строки вида "ключ = значение"
        if (std.mem.indexOf(u8, trimmed, "=")) |eq_pos| {
            const key = std.mem.trim(u8, trimmed[0..eq_pos], " \t\"'");
            const value = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t\"'");

            // Обрабатываем каждый ключ
            if (std.mem.eql(u8, key, "host")) {
                config.host = try allocator.dupe(u8, value); // Выделяем память для хоста
            } else if (std.mem.eql(u8, key, "port")) {
                config.port = try std.fmt.parseInt(u16, value, 10); // Парсим порт
            } else if (std.mem.eql(u8, key, "username") or std.mem.eql(u8, key, "user")) {
                config.username = try allocator.dupe(u8, value); // Выделяем память для имени пользователя
            } else if (std.mem.eql(u8, key, "password")) {
                config.password = try allocator.dupe(u8, value); // Выделяем память для пароля
            } else if (std.mem.eql(u8, key, "database")) {
                config.database = try allocator.dupe(u8, value); // Выделяем память для имени БД
            }
        }
    }

    return config;
}

Использую pub export fn get_banner( вместо просто export, чтобы можно было вызвать из программы для тестирования (следующий спойлер).

На момент написания статьи я начал работать над более оптимизированной версией баннера, с лучшей архитектурой и меньшим количеством запросов к бд, с уменьшением фрагментации памяти.

Как я проверял работу статической библиотеки:

Скрытый текст

Пока функционал чуть более чем скромный, я поленился писать полноценные unit-тесты, моккировать бд и т.д. Но добавил этот пункт в TODO лист проекта.

Кстати, если вы обратили внимание — модуль возвращает true, в некоторых случаях если сталкивается с ошибкой, это сделано намеренно с целью улучшить UX пользователя.

Для проверки что всё работает как задумано, была написана такая программа, test/test_banner.zig:

const std = @import("std");

// Live тестирование на реальной базе данных
pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Тестовые данные для проверки прокси
    const ProxyTestCase = struct {
        username: []const u8,
        remote_ip: []const u8,
        expected: bool,
        description: []const u8,
    };

    const proxy_test_cases = [_]ProxyTestCase{
        .{
            .username = "test_user",
            .remote_ip = "192.168.1.1",
            .expected = false,
            .description = "Пользователь не существует",
        },
        .{
            .username = "axtest",
            .remote_ip = "1.2.3.4", // Неправильный IP
            .expected = false,
            .description = "Строгий прокси, IP не совпадает",
        },
        .{
            .username = "axtest",
            .remote_ip = "", // Правильный IP
            .expected = true,
            .description = "Строгий прокси, IP совпадает",
        },
        .{
            .username = "axtest2",
            .remote_ip = "5.6.7.8", // Любой IP будет правильным
            .expected = true,
            .description = "Нестрогий прокси",
        },
        .{
            .username = "axtest3",
            .remote_ip = "9.10.11.12", // Любой IP будет правильным
            .expected = true,
            .description = "Прокси не задан",
        },
    };

    // Тестовые данные для проверки баннеров
    const BannerTestCase = struct {
        username: []const u8,
        description: []const u8,
    };

    const banner_test_cases = [_]BannerTestCase{
        .{
            .username = "axtest",
            .description = "Пользователь с глобальным сообщением",
        },
        .{
            .username = "axtest2",
            .description = "Пользователь с групповым сообщением",
        },
        .{
            .username = "axtest3",
            .description = "Пользователь с индивидуальным сообщением",
        },
        .{
            .username = "axtest4",
            .description = "Пользователь с истекающим сроком",
        },
        .{
            .username = "axtest5",
            .description = "Пользователь без сообщений",
        },
    };

    std.debug.print("\n=== Тестирование проверки прокси ===\n", .{});
    for (proxy_test_cases) |case| {
        // Буфер для IP прокси
        var proxy_ip: [256]u8 = undefined;
        @memset(&proxy_ip, 0);
        proxy_ip[proxy_ip.len - 1] = 0; // Гарантируем null-terminator

        // Создаем нуль-терминированные строки
        const username_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.username});
        defer allocator.free(username_nt);
        const remote_ip_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.remote_ip});
        defer allocator.free(remote_ip_nt);

        // Вызываем функцию проверки прокси
        const res = @import("banner_check").check_user_proxy(
            username_nt.ptr,
            remote_ip_nt.ptr,
            @ptrCast(&proxy_ip),
        );

        // Получаем строку IP прокси
        const proxy_ip_str = if (proxy_ip[0] != 0)
            std.mem.sliceTo(&proxy_ip, 0)
        else
            "none";

        // Проверяем результат
        const status = if (res == case.expected) "PASS" else "FAIL";

        std.debug.print("[{s}] {s}: {s}@{s} => {} (proxy: {s}, expected {})\n", .{
            status,
            case.description,
            case.username,
            case.remote_ip,
            res,
            proxy_ip_str,
            case.expected,
        });
    }

    std.debug.print("\n=== Тестирование баннерных сообщений ===\n", .{});
    for (banner_test_cases) |case| {
        // Буфер для баннера (4KB должно хватить для любого сообщения)
        var banner_buffer: [4096]u8 = undefined;
        @memset(&banner_buffer, 0);
        banner_buffer[banner_buffer.len - 1] = 0; // Гарантируем null-terminator

        // Создаем нуль-терминированную строку имени пользователя
        const username_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.username});
        defer allocator.free(username_nt);

        // Вызываем функцию получения баннера
        const has_banner = @import("banner_check").get_banner(
            username_nt.ptr,
            @ptrCast(&banner_buffer),
            banner_buffer.len,
        );

        // Получаем строку баннера
        const banner_str = if (has_banner)
            std.mem.sliceTo(&banner_buffer, 0)
        else
            "no banner";

        std.debug.print("{s}: {s} => {s}\n", .{
            case.description,
            case.username,
            banner_str,
        });
    }
}

Build.zig:

Скрытый текст
const std = @import("std");

pub fn build(b: *std.Build) void {
    // Стандартные опции
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Получаем модуль pg
    const pg_module = b.dependency("pg", .{
        .target = target,
        .optimize = optimize,
    }).module("pg");

    // 1. Настраиваем исполняемый файл
    const exe = b.addExecutable(.{
        .name = "login-test",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.root_module.addImport("pg", pg_module);
    b.installArtifact(exe);

    // 2. Настраиваем статическую библиотеку
    const lib = b.addStaticLibrary(.{
        .name = "banner_check",
        .root_source_file = b.path("src/banner_check.zig"),
        .target = target,
        .optimize = .ReleaseSafe,
    });

    lib.root_module.addImport("pg", pg_module);
    lib.linkLibC();
    lib.linkSystemLibrary("pq");
    lib.bundle_compiler_rt = true;
    b.installArtifact(lib);

    // 3. Добавляем тестовую программу
    const test_exe = b.addExecutable(.{
        .name = "test_banner",
        .root_source_file = b.path("test/test_banner.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Подключаем зависимости
    test_exe.root_module.addImport("banner_check", lib.root_module);

    test_exe.linkLibrary(lib); // Линкуем нашу библиотеку
    test_exe.linkLibC();

    b.installArtifact(test_exe);

    // 4. Добавляем шаг для запуска тестов
    const run_test = b.addRunArtifact(test_exe);
    const test_step = b.step("test", "Run banner check tests");
    test_step.dependOn(&run_test.step);

    // 5. Простая генерация заголовочного файла
    const gen_header = b.addWriteFiles();
    _ = gen_header.add("include/banner_check.h",
        \\#pragma once
        \\#include <stdbool.h>
        \\bool check_user_proxy(const char* username, const char* remote_addr, char* proxy_ip_out);
    );
    b.getInstallStep().dependOn(&gen_header.step);

    // 6. Группируем шаги сборки
    const build_exe = b.step("exe", "Build only the executable");
    build_exe.dependOn(&exe.step);

    const build_lib = b.step("lib", "Build only the library");
    build_lib.dependOn(&lib.step);

    const build_test = b.step("test-build", "Build the test executable");
    build_test.dependOn(&test_exe.step);

    const build_all = b.step("all", "Build everything");
    build_all.dependOn(build_exe);
    build_all.dependOn(build_lib);
    build_all.dependOn(build_test);
    build_all.dependOn(&gen_header.step);
}

Вот как теперь это вызывается из C-кода:

Скрытый текст

banner_check.h:

#pragma once
#include <stdbool.h>
#include <stddef.h>  // for size_t

#ifdef __cplusplus
extern "C" {
#endif

external bool check_user_proxy(const char* username, const char* remote_addr, char* proxy_ip);
external bool get_banner(const char* username, char* banner_out, size_t banner_len);

#ifdef __cplusplus
}
#endif

По сути мы просто обьявляем функции, которые будем вызывать из статической библиотеки.

worker-auth.c:

В начале файла, после основных хидеров

#include "banner_check.h"

Далее в функции int post_common_handler(worker_st ws, unsigned http_ver, const char imsg) вносятся изменения:

В блоке основных определений переменных

char banner[4096], username[32], remote_addr[256], our_addr[256];
char proxy_ip[256] = {0};
	
memset(banner, 0, 256);
memset(username, 0, 32);
memset(remote_addr, 0, 256);
memset(our_addr, 0, 256);

Перед блоком с формированием баннера (оригинальный фрагмент кода):

if (WSCONFIG(ws)->banner) {
...

Добавляем код:

int len;
len = strlen(ws->username);
memcpy(username, ws->username, len);

getsockname(ws->conn_fd, (struct sockaddr*)&ws->our_addr, &ws->our_addr_len);

ret = getnameinfo((void*)&ws->remote_addr, ws->remote_addr_len, remote_addr, sizeof(remote_addr), NULL, 0, NI_NUMERICHOST);
if (ret < 0)
	goto fail;
ret = getnameinfo((void*)&ws->our_addr, ws->our_addr_len, our_addr, sizeof(our_addr), NULL, 0, NI_NUMERICHOST);
if (ret < 0)
	goto fail;
	
/* Check if proxy is allowed */
if (!is_proxy_allowed(username, remote_addr, proxy_ip)) {
	const char *proxy_msg = "Вы должны подключится используя proxy ip: ";
	size_t msg_len = strlen(proxy_msg);
	size_t ip_len = strlen(proxy_ip);
        
	// Ensure we don't overflow the banner buffer
	if (msg_len + ip_len < MAX_BANNER_SIZE + 32) {
          memcpy(WSCONFIG(ws)->banner, proxy_msg, msg_len);
          memcpy(WSCONFIG(ws)->banner + msg_len, proxy_ip, ip_len);
          WSCONFIG(ws)->banner[msg_len + ip_len] = '\0';
	} else {
          // If the message is too long, use a truncated version
          const char *fallback_msg = "Используйте назначенный proxy";
          memcpy(WSCONFIG(ws)->banner, fallback_msg, strlen(fallback_msg)+1);
	}
} else {
	bool has_banner = get_banner(username, banner, sizeof(banner));
	if (has_banner) {
	    memcpy(WSCONFIG(ws)->banner, banner, strlen(banner)+1);
	} else {
	    // something went wrong
	    goto fail;
	  }
}

is_proxy_allowed это небольшая обёртка, которую можно вставить в любом месте исходника:

bool is_proxy_allowed(const char *username, const char *remote_addr, char* proxy_ip) {
  return check_user_proxy(username, remote_addr, proxy_ip);
}

Процесс сборки ocserv:

Нужно модифицировать сборочные скрипты

В configure.ac:

# После всех основных проверок библиотек, но перед AC_CONFIG_FILES

# Проверка Zig-модуля
AC_ARG_WITH([zig-module],
  [AS_HELP_STRING([--with-zig-module=DIR],
    [path to Zig proxy check module @<:@default=../zig-module@:>@])],
  [zig_module_dir=$withval],
  [zig_module_dir=../zig-module]
)

AC_MSG_CHECKING([for Zig proxy check module])
if test -f "${zig_module_dir}/libbanner_check.a"; then
  AC_MSG_RESULT([yes])
  HAVE_ZIG_MODULE=yes
  ZIG_MODULE_LIBS="-L${zig_module_dir} -Wl,--whole-archive -lbanner_check -Wl,--no-whole-archive"
  ZIG_MODULE_CFLAGS="-I${zig_module_dir}"
  AC_SUBST([ZIG_MODULE_LIBS])
  AC_SUBST([ZIG_MODULE_CFLAGS])
else
  AC_MSG_RESULT([no])
  HAVE_ZIG_MODULE=no
fi

AM_CONDITIONAL([HAVE_ZIG_MODULE], [test "$HAVE_ZIG_MODULE" = "yes"])

В src/Makefile.am:

if HAVE_ZIG_MODULE
ocserv_LDADD += @ZIG_MODULE_LIBS@
ocserv_worker_LDADD += @ZIG_MODULE_LIBS@
AM_CPPFLAGS += @ZIG_MODULE_CFLAGS@
endif

Подводные камни при сборке:

А теперь внимание, хак по сборке: по умолчанию zig собирает бинарник под архитектуру процессора на котором запущен, как передать этот параметр через build.zig если честно не разобрался пока, поэтому передаём через строку сборки.

zig build install -Dcpu=baseline

При сборки библиотеке важно не забыть про bundle_compiler_rt = trueв build.zig, эта опция говорит компилятору что нужно включить рантайм компилятора в итоговый файл -- без этого либо не получиться собрать итоговый бинарник на C, либо он выйдет зависимым от компилятора zig.

Итого о пользе для бизнеса:

  1. Простота развёртывания:

    • Сократилось количество файлов

    • Задаём конфиг в TOML (можно и без него)

    • Запускаем

  2. Ресурсоёмкость:

    • Потребление памяти: ~5MB (весь ocserv) против ~50MB у PHP-FPM

    • Время отклика: 3-5ms против 50-100ms (без особой нагрузки)

  3. Надёжность:

    • Нет внезапных падений

    • Чёткое логирование всех ошибок

    • Предсказуемое поведение под нагрузкой

Послесловие

К сожалению Zig ещё не достиг состояния release, и скорее всего для сборки на более свежем компиляторе придётся обновлять код.

Я использовал Zig версии: 0.14

P.s. если понравился пост — подписвайтесь на обновления моего профиля на Хабр или на мой телеграмм-канал.

P.P.S. конструктивная критика и рекомендации всегда приветствуются

P.P.P.S Если нет конструктива — лучше не комментируйте.

Комментарии (74)