From 582ae5a294844d725f2896c3002b154895363f52 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Thu, 28 Aug 2025 23:07:12 +0300 Subject: [PATCH 1/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dub.json | 5 +---- dub.selections.json | 1 + source/app.d | 18 +++++++++++++++--- source/{fanotify_wrapper.d => dfanotify.d} | 5 ++--- 4 files changed, 19 insertions(+), 10 deletions(-) rename source/{fanotify_wrapper.d => dfanotify.d} (99%) diff --git a/dub.json b/dub.json index 4cf8d3a..60ff96a 100644 --- a/dub.json +++ b/dub.json @@ -9,9 +9,6 @@ "targetPath": "bin", "targetType": "executable", "dependencies": { - "fanotify": { - "repository": "git+https://git.zhirov.kz/dlang/fanotify.git", - "version": "97edc0d795c93ef773ff60d260951e5ff6ae6215" - } + "fanotify": "~>0.1.0" } } \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json index 322586b..3de4f99 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -1,5 +1,6 @@ { "fileVersion": 1, "versions": { + "fanotify": "0.1.0" } } diff --git a/source/app.d b/source/app.d index 2cc78c9..cd0dc67 100644 --- a/source/app.d +++ b/source/app.d @@ -5,24 +5,31 @@ // Импорты: fanotify_wrapper - обёртка, std.stdio для вывода, std.file для readLink (хотя здесь не используется из-за режима), // std.format для форматирования строк, core.sys.posix для констант. -import fanotify_wrapper; // Импорт обёртки для fanotify. +import dfanotify; // Импорт обёртки для fanotify. import std.stdio; // Импорт для writeln, writefln (вывод в консоль). import std.file : readLink; // Импорт readLink для чтения симлинков (не используется здесь, но оставлено для возможного расширения). import std.format : format; // Импорт format для форматирования строк (хотя здесь используется writefln напрямую). import core.sys.posix.fcntl : AT_FDCWD; // Импорт AT_FDCWD для текущей директории. import core.sys.posix.unistd : close; // Импорт close (не используется здесь, но для возможного расширения с fd). +import core.stdc.stdlib : exit; // Функция main: точка входа приложения. void main() { + Fanotify fan; // Инициализация объекта Fanotify с флагами: // FAN_CLASS_NOTIF - режим уведомлений (без контроля доступа), // FAN_CLOEXEC - закрытие дескриптора при exec, // FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME - отчёт с FID (идентификатор файла) вместо fd, плюс имя файла. // Это позволяет получать имя без реального fd (fd будет FAN_NOFD). - auto fan = new Fanotify( + try { + fan = new Fanotify( FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME); + } catch (Exception e) { + writeln(e.msg); + exit(1); + } // Определение маски событий: битовая OR флагов для мониторинга. // FAN_OPEN - открытие, FAN_MODIFY - модификация, FAN_CLOSE - закрытие (включает WRITE и NOWRITE), @@ -32,7 +39,12 @@ void main() // Маркировка директории /tmp/scripts: // FAN_MARK_ADD - добавить марку, FAN_MARK_ONLYDIR - только для директории (ошибка, если не директория). // AT_FDCWD - базовая директория текущая, путь "/tmp/scripts". - fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); + try { + fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); + } catch (Exception e) { + writeln(e.msg); + exit(1); + } writeln("Мониторинг запущен для /tmp/scripts..."); // Вывод сообщения о старте мониторинга. diff --git a/source/fanotify_wrapper.d b/source/dfanotify.d similarity index 99% rename from source/fanotify_wrapper.d rename to source/dfanotify.d index 067373f..120272e 100644 --- a/source/fanotify_wrapper.d +++ b/source/dfanotify.d @@ -7,7 +7,7 @@ // std.string и std.conv для работы со строками, core.stdc.errno для errno и strerror для детальных сообщений об ошибках, // core.stdc.stdint для типов вроде uint64_t. -module fanotify_wrapper; +module dfanotify; public import fanotify; // Импорт низкоуровневых определений fanotify (структуры, константы, функции вроде fanotify_init, fanotify_mark). @@ -174,8 +174,7 @@ class Fanotify if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) // Если тип - DFID + NAME (директория FID + имя). { // Расчёт смещения: пропускаем hdr, fsid (filesystem ID), затем file_handle. - size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t - .sizeof; + size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t.sizeof; auto handle = cast(file_handle*)(buffer.ptr + fidOffset); // Кастинг в структуру file_handle (содержит handle_bytes - размер handle). size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; // Конец handle в буфере. if (handleEnd < offset + meta.event_len) // Проверка, что за handle есть место для имени. From ed0d2780c3fea47631626ad75cae99641e614928 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Thu, 28 Aug 2025 23:21:15 +0300 Subject: [PATCH 2/8] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 59 +++++------------ source/dfanotify.d | 157 ++++++++++++++++----------------------------- 2 files changed, 72 insertions(+), 144 deletions(-) diff --git a/source/app.d b/source/app.d index cd0dc67..6a13c16 100644 --- a/source/app.d +++ b/source/app.d @@ -1,28 +1,15 @@ -// Модуль app.d -// Это основное приложение, демонстрирующее использование обёртки fanotify_wrapper. -// Инициализирует fanotify, маркирует директорию /tmp/scripts для мониторинга событий (открытие, модификация и т.д.), -// затем в бесконечном цикле читает события и выводит информацию о них в консоль. -// Импорты: fanotify_wrapper - обёртка, std.stdio для вывода, std.file для readLink (хотя здесь не используется из-за режима), -// std.format для форматирования строк, core.sys.posix для констант. +import dfanotify; -import dfanotify; // Импорт обёртки для fanotify. - -import std.stdio; // Импорт для writeln, writefln (вывод в консоль). -import std.file : readLink; // Импорт readLink для чтения симлинков (не используется здесь, но оставлено для возможного расширения). -import std.format : format; // Импорт format для форматирования строк (хотя здесь используется writefln напрямую). -import core.sys.posix.fcntl : AT_FDCWD; // Импорт AT_FDCWD для текущей директории. -import core.sys.posix.unistd : close; // Импорт close (не используется здесь, но для возможного расширения с fd). +import std.stdio; +import std.file : readLink; +import std.format : format; +import core.sys.posix.fcntl : AT_FDCWD; +import core.sys.posix.unistd : close; import core.stdc.stdlib : exit; -// Функция main: точка входа приложения. void main() { Fanotify fan; - // Инициализация объекта Fanotify с флагами: - // FAN_CLASS_NOTIF - режим уведомлений (без контроля доступа), - // FAN_CLOEXEC - закрытие дескриптора при exec, - // FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME - отчёт с FID (идентификатор файла) вместо fd, плюс имя файла. - // Это позволяет получать имя без реального fd (fd будет FAN_NOFD). try { fan = new Fanotify( FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME); @@ -31,14 +18,8 @@ void main() exit(1); } - // Определение маски событий: битовая OR флагов для мониторинга. - // FAN_OPEN - открытие, FAN_MODIFY - модификация, FAN_CLOSE - закрытие (включает WRITE и NOWRITE), - // FAN_CREATE - создание, FAN_DELETE - удаление, FAN_EVENT_ON_CHILD - события в поддиректориях. auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD; - // Маркировка директории /tmp/scripts: - // FAN_MARK_ADD - добавить марку, FAN_MARK_ONLYDIR - только для директории (ошибка, если не директория). - // AT_FDCWD - базовая директория текущая, путь "/tmp/scripts". try { fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); } catch (Exception e) { @@ -46,37 +27,29 @@ void main() exit(1); } - writeln("Мониторинг запущен для /tmp/scripts..."); // Вывод сообщения о старте мониторинга. + writeln("Мониторинг запущен для /tmp/scripts..."); - // Бесконечный цикл: постоянно читает события и обрабатывает их. while (true) { - auto events = fan.readEvents(); // Чтение событий (блокирующее, ждёт до появления событий). + auto events = fan.readEvents(); - foreach (ref e; events) // Цикл по каждому событию в массиве. + foreach (ref e; events) { - // Определение пути: если name извлечено (из FAN_REPORT_NAME), использовать его; иначе "unknown". - // name - относительное имя файла/директории относительно маркированной. - string path = e.name.length ? e.name : "unknown"; // Используем извлечённое имя (относительное) + string path = e.name.length ? e.name : "unknown"; - // Комментарий: в режиме с FAN_REPORT_FID fd = FAN_NOFD, так что нельзя использовать readLink("/proc/self/fd/" ~ to!string(e.eventFd)) для полного пути. - // fd теперь FAN_NOFD, так что пропускаем readLink/close - - // Вывод общей информации о событии: маска в hex, PID, путь/имя. writefln("Событие: mask=0x%x, pid=%d, name/path=%s", e.mask, e.pid, path); - // Проверки и вывод конкретных типов событий с использованием методов структуры. - if (e.isOpen) // Если открытие. + if (e.isOpen) writeln(" - Открытие файла"); - if (e.isModify) // Если модификация. + if (e.isModify) writeln(" - Модификация файла"); - if (e.isCloseWrite) // Если закрытие после записи. + if (e.isCloseWrite) writeln(" - Закрытие после записи"); - if (e.isCloseNoWrite) // Если закрытие без записи. + if (e.isCloseNoWrite) writeln(" - Закрытие без записи"); - if (e.isCreate) // Если создание. + if (e.isCreate) writeln(" - Создание файла/директории"); - if (e.isDelete) // Если удаление. + if (e.isDelete) writeln(" - Удаление файла/директории"); } } diff --git a/source/dfanotify.d b/source/dfanotify.d index 120272e..d678bc1 100644 --- a/source/dfanotify.d +++ b/source/dfanotify.d @@ -1,199 +1,154 @@ -// Модуль fanotify_wrapper.d -// Этот модуль предоставляет обёртку вокруг API fanotify для упрощения мониторинга событий файловой системы в Linux. -// Fanotify позволяет получать уведомления о действиях с файлами, такими как открытие, модификация, создание и удаление. -// Обёртка включает структуру для событий, класс для управления дескриптором и методы для инициализации, маркировки и чтения событий. -// Импорты: public import fanotify - предполагается, что это низкоуровневый модуль с определениями из . -// Другие импорты из core.sys.posix для системных вызовов (read, close и т.д.), std.exception для обработки ошибок, -// std.string и std.conv для работы со строками, core.stdc.errno для errno и strerror для детальных сообщений об ошибках, -// core.stdc.stdint для типов вроде uint64_t. - module dfanotify; -public import fanotify; // Импорт низкоуровневых определений fanotify (структуры, константы, функции вроде fanotify_init, fanotify_mark). +public import fanotify; -import core.sys.posix.unistd : read, close, ssize_t; // Импорт функций для чтения (read), закрытия (close) дескрипторов и типа ssize_t для возвращаемых значений. -import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; // Импорт флагов для open (O_RDONLY - только чтение, O_LARGEFILE - поддержка больших файлов) и AT_FDCWD для текущей директории. -import std.exception : enforce; // Импорт enforce для проверки условий и бросания исключений при ошибках. -import std.string : toStringz, fromStringz; // Импорт функций для конвертации строк D в C-строки (toStringz) и обратно (fromStringz). -import std.conv : to; // Импорт to для конвертации типов (например, int в string). -import core.stdc.errno : errno; // Импорт errno для получения кода последней ошибки. -import core.stdc.string : strerror; // Импорт strerror для получения строкового описания ошибки по errno. -import core.stdc.stdint; // Импорт стандартных целочисленных типов (uint64_t и т.п.). +import core.sys.posix.unistd : read, close, ssize_t; +import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; +import std.exception : enforce; +import std.string : toStringz, fromStringz; +import std.conv : to; +import core.stdc.errno : errno; +import core.stdc.string : strerror; +import core.stdc.stdint; -// Структура FanotifyEvent: представляет одно событие fanotify. -// Расширена по сравнению с базовой fanotify_event_metadata: добавлено поле name для извлечённого имени файла (если используются флаги FAN_REPORT_NAME). -// Также добавлены свойства для доступа к ключевым полям и методы для проверки конкретных типов событий. -// Это упрощает работу с событиями, делая код более читаемым (вместо прямого доступа к meta.mask и т.д.). struct FanotifyEvent { - fanotify_event_metadata meta; // Базовая структура метаданных события из fanotify (содержит mask, fd, pid, event_len и т.д.). - string name; // Извлечённое имя файла или директории (относительное имя, если FAN_REPORT_NAME включено; парсится из дополнительной информации в буфере). + fanotify_event_metadata meta; + string name; - // Свойство mask: возвращает маску событий (битовая маска, где каждый бит соответствует типу события, например FAN_OPEN). @property uint64_t mask() const { - return meta.mask; // Просто возвращает значение из meta; const гарантирует, что структура не модифицируется. + return meta.mask; } - // Свойство eventFd: возвращает дескриптор файла события (fd). В режиме FAN_REPORT_FID это FAN_NOFD (-1), иначе реальный fd. @property int eventFd() const { - return meta.fd; // Доступ к fd из meta; полезно, если нужно работать с файлом напрямую (но в этом коде используется режим без fd). + return meta.fd; } - // Свойство pid: возвращает PID процесса, который вызвал событие. @property int pid() const { - return meta.pid; // Доступ к pid из meta; помогает идентифицировать, какой процесс взаимодействовал с файлом. + return meta.pid; } - // Метод isOpen: проверяет, включает ли маска событие открытия файла (FAN_OPEN). - // Использует битовую операцию & для проверки наличия бита FAN_OPEN в mask. bool isOpen() const { - return (mask & FAN_OPEN) != 0; // Если бит установлен, возвращает true; это стандартный способ работы с битoвыми масками. + return (mask & FAN_OPEN) != 0; } - // Метод isModify: проверяет событие модификации файла (FAN_MODIFY, например, запись в файл). bool isModify() const { - return (mask & FAN_MODIFY) != 0; // Аналогично, проверка бита FAN_MODIFY. + return (mask & FAN_MODIFY) != 0; } - // Метод isCloseWrite: проверяет закрытие файла после записи (FAN_CLOSE_WRITE). bool isCloseWrite() const { - return (mask & FAN_CLOSE_WRITE) != 0; // Проверка бита для закрытия с модификацией. + return (mask & FAN_CLOSE_WRITE) != 0; } - // Метод isCloseNoWrite: проверяет закрытие файла без записи (FAN_CLOSE_NOWRITE, например, после чтения). bool isCloseNoWrite() const { - return (mask & FAN_CLOSE_NOWRITE) != 0; // Проверка бита для закрытия без модификации. + return (mask & FAN_CLOSE_NOWRITE) != 0; } - // Метод isAccess: проверяет событие доступа (FAN_ACCESS, например, чтение). bool isAccess() const { - return (mask & FAN_ACCESS) != 0; // Проверка бита FAN_ACCESS. + return (mask & FAN_ACCESS) != 0; } - // Метод isCreate: проверяет создание файла или директории (FAN_CREATE). bool isCreate() const { - return (mask & FAN_CREATE) != 0; // Проверка бита для создания. + return (mask & FAN_CREATE) != 0; } - // Метод isDelete: проверяет удаление файла или директории (FAN_DELETE). bool isDelete() const { - return (mask & FAN_DELETE) != 0; // Проверка бита для удаления. + return (mask & FAN_DELETE) != 0; } } -// Класс Fanotify: основной класс для работы с fanotify. -// Управляет дескриптором (fd), инициализирует его, маркирует пути для мониторинга, читает события и закрывает дескриптор. -// Расширен для парсинга дополнительной информации (FID, NAME) в методе readEvents. -// Использует RAII: дескриптор закрывается в деструкторе автоматически. class Fanotify { - private int fd = -1; // Приватный дескриптор fanotify; инициализирован -1 (недействительный), чтобы избежать использования до инициализации. + private int fd = -1; - // Конструктор: инициализирует fanotify с заданными флагами. - // initFlags: флаги для fanotify_init (например, FAN_CLASS_NOTIF для уведомлений без контроля доступа). - // eventFFlags: флаги для событий (по умолчанию O_RDONLY | O_LARGEFILE для чтения больших файлов). this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE) { - fd = fanotify_init(initFlags, eventFFlags); // Вызов системной функции fanotify_init для создания дескриптора. - enforce(fd >= 0, "Ошибка инициализации fanotify: " ~ to!string(fd)); // Проверка: если fd < 0, бросить исключение с сообщением; enforce упрощает обработку ошибок. + fd = fanotify_init(initFlags, eventFFlags); + enforce(fd >= 0, "Ошибка инициализации fanotify: " ~ to!string(fd)); } - // Деструктор: автоматически вызывается при уничтожении объекта. ~this() { - if (fd >= 0) // Проверка, валиден ли fd (чтобы избежать закрытия -1). + if (fd >= 0) { - close(fd); // Закрытие дескриптора через системный вызов close; освобождает ресурсы. - fd = -1; // Установка в -1 для безопасности (хотя объект уничтожается). + close(fd); + fd = -1; } } - // Метод mark: маркирует путь (файл или директорию) для мониторинга. - // markFlags: флаги маркировки (например, FAN_MARK_ADD для добавления, FAN_MARK_ONLYDIR для только директорий). - // eventMask: маска событий, которые нужно мониторить (битовая маска, например FAN_OPEN | FAN_MODIFY). - // dirFd: дескриптор директории (по умолчанию AT_FDCWD - текущая). - // path: путь к маркируемому объекту (null для текущей директории). void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null) { - const(char)* cPath = path ? path.toStringz() : null; // Конвертация пути в C-строку (toStringz добавляет null-терминатор); если path null, то null. - int res = fanotify_mark(fd, markFlags, eventMask, dirFd, cPath); // Вызов системной функции fanotify_mark для добавления марки. - if (res == -1) // Проверка на ошибку (-1 означает неудачу). + const(char)* cPath = path ? path.toStringz() : null; + int res = fanotify_mark(fd, markFlags, eventMask, dirFd, cPath); + if (res == -1) { - // Сборка детального сообщения об ошибке: включает res, errno и описание от strerror. string errMsg = "Ошибка маркировки fanotify: " ~ to!string( res) ~ " (errno: " ~ to!string(errno) ~ ", " ~ strerror(errno) .fromStringz.to!string ~ ")"; - throw new Exception(errMsg); // Бросить исключение для прерывания выполнения при ошибке. + throw new Exception(errMsg); } } - // Метод readEvents: читает события из дескриптора fanotify. - // bufferSize: размер буфера для чтения (по умолчанию 4096 байт - размер страницы памяти, достаточно для нескольких событий). - // Возвращает массив FanotifyEvent; расширен для парсинга дополнительной информации (FID, DFID, NAME). FanotifyEvent[] readEvents(size_t bufferSize = 4096) { - ubyte[] buffer = new ubyte[bufferSize]; // Выделение буфера unsigned byte[] для сырых данных (fanotify возвращает байты). - ssize_t len = read(fd, buffer.ptr, buffer.length); // Чтение данных из fd через системный вызов read; len - количество прочитанных байт (блокирующий вызов, ждёт событий). - if (len <= 0) // Если ничего не прочитано или ошибка, вернуть пустой массив. + ubyte[] buffer = new ubyte[bufferSize]; + ssize_t len = read(fd, buffer.ptr, buffer.length); + if (len <= 0) { - return []; // Нет событий или ошибка (не бросаем исключение, чтобы цикл мог продолжаться). + return []; } - FanotifyEvent[] events; // Массив для собранных событий. - size_t offset = 0; // Смещение в буфере для парсинга (события идут подряд). - while (offset + FAN_EVENT_METADATA_LEN <= len) // Цикл по буферу: пока хватает места для минимальной структуры metadata. + FanotifyEvent[] events; + size_t offset = 0; + while (offset + FAN_EVENT_METADATA_LEN <= len) { - auto meta = cast(fanotify_event_metadata*)(buffer.ptr + offset); // Кастинг байтов в структуру metadata (unsafe, но стандартно для C-API). - if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) // Проверка валидности: длина события должна быть >= минимальной и не выходить за буфер. + auto meta = cast(fanotify_event_metadata*)(buffer.ptr + offset); + if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) { - break; // Если некорректно, прервать цикл (защита от повреждённых данных). + break; } - FanotifyEvent ev = FanotifyEvent(*meta); // Создание структуры события на основе meta (копирует данные). + FanotifyEvent ev = FanotifyEvent(*meta); - // Парсинг дополнительной информации (info blocks): если флаги включают FAN_REPORT_FID/NAME, в буфере после meta идут блоки с FID, NAME и т.д. - size_t infoOffset = offset + fanotify_event_metadata.sizeof; // Смещение после meta. - while (infoOffset < offset + meta.event_len) // Цикл по блокам info внутри события. + size_t infoOffset = offset + fanotify_event_metadata.sizeof; + while (infoOffset < offset + meta.event_len) { - auto hdr = cast(fanotify_event_info_header*)(buffer.ptr + infoOffset); // Кастинг в заголовок info (содержит type и len). - if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) // Проверка валидности блока. + auto hdr = cast(fanotify_event_info_header*)(buffer.ptr + infoOffset); + if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) { - break; // Если некорректно, прервать. + break; } - if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) // Если тип - DFID + NAME (директория FID + имя). + if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) { - // Расчёт смещения: пропускаем hdr, fsid (filesystem ID), затем file_handle. size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t.sizeof; - auto handle = cast(file_handle*)(buffer.ptr + fidOffset); // Кастинг в структуру file_handle (содержит handle_bytes - размер handle). - size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; // Конец handle в буфере. - if (handleEnd < offset + meta.event_len) // Проверка, что за handle есть место для имени. + auto handle = cast(file_handle*)(buffer.ptr + fidOffset); + size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; + if (handleEnd < offset + meta.event_len) { - // Извлечение имени: null-terminated C-строка после handle; конвертируем в string D. ev.name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string; } } - infoOffset += hdr.len; // Переход к следующему блоку info. + infoOffset += hdr.len; } - events ~= ev; // Добавление parsed события в массив. - offset += meta.event_len; // Переход к следующему событию в буфере. + events ~= ev; + offset += meta.event_len; } - return events; // Возврат массива событий. + return events; } - // Свойство handle: возвращает дескриптор fd для низкоуровневого доступа (если нужно, например, для select или других вызовов). @property int handle() const { - return fd; // Просто геттер. + return fd; } } From cb3f40deeea788a7a3a6b430f7a35cdab07714e6 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 00:10:11 +0300 Subject: [PATCH 3/8] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 65 ++++++++++++------ source/dfanotify.d | 163 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 171 insertions(+), 57 deletions(-) diff --git a/source/app.d b/source/app.d index 6a13c16..4c08dd0 100644 --- a/source/app.d +++ b/source/app.d @@ -1,56 +1,77 @@ import dfanotify; import std.stdio; -import std.file : readLink; import std.format : format; import core.sys.posix.fcntl : AT_FDCWD; -import core.sys.posix.unistd : close; import core.stdc.stdlib : exit; void main() { Fanotify fan; - try { + try + { fan = new Fanotify( - FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME); - } catch (Exception e) { + FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME + ); + } + catch (Exception e) + { writeln(e.msg); exit(1); } - auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD; + auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD | FAN_ACCESS; - try { + try + { fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); - } catch (Exception e) { + } + catch (Exception e) + { writeln(e.msg); exit(1); } - writeln("Мониторинг запущен для /tmp/scripts..."); + writeln( + "Мониторинг с разрешениями запущен для /tmp/scripts..."); while (true) { auto events = fan.readEvents(); - foreach (ref e; events) + foreach (e; events) { string path = e.name.length ? e.name : "unknown"; writefln("Событие: mask=0x%x, pid=%d, name/path=%s", e.mask, e.pid, path); - if (e.isOpen) - writeln(" - Открытие файла"); - if (e.isModify) - writeln(" - Модификация файла"); - if (e.isCloseWrite) - writeln(" - Закрытие после записи"); - if (e.isCloseNoWrite) - writeln(" - Закрытие без записи"); - if (e.isCreate) - writeln(" - Создание файла/директории"); - if (e.isDelete) - writeln(" - Удаление файла/директории"); + // Обработка permission-события + if (e.isOpenPerm) + { + writeln(" - Запрос на открытие файла. Отклоняем (предполагаем попытку записи)."); + e.respond(FAN_DENY); // Отклонить (для FAN_ALLOW используйте FAN_ALLOW) + } + else + { + // Обычные уведомления (не permission) + if (e.isOpen) + writeln(" - Открытие файла"); + if (e.isAccess) + writeln(" - Доступ к файлу"); + if (e.isModify) + writeln(" - Модификация файла"); + if (e.isCloseWrite) + writeln(" - Закрытие после записи"); + if (e.isCloseNoWrite) + writeln(" - Закрытие без записи"); + if (e.isCreate) + writeln(" - Создание файла/директории"); + if (e.isDelete) + writeln(" - Удаление файла/директории"); + } + + // Опционально: вызов постобработки + e.postProcess(); } } } diff --git a/source/dfanotify.d b/source/dfanotify.d index d678bc1..8144d08 100644 --- a/source/dfanotify.d +++ b/source/dfanotify.d @@ -2,7 +2,7 @@ module dfanotify; public import fanotify; -import core.sys.posix.unistd : read, close, ssize_t; +import core.sys.posix.unistd : read, write, close, ssize_t; import core.sys.posix.fcntl : O_RDONLY, O_RDWR, O_LARGEFILE, AT_FDCWD; import std.exception : enforce; import std.string : toStringz, fromStringz; @@ -11,26 +11,53 @@ import core.stdc.errno : errno; import core.stdc.string : strerror; import core.stdc.stdint; -struct FanotifyEvent +// Класс для представления события fanotify (ООП-стиль, с методами для проверки и обработки) +class FanotifyEvent { - fanotify_event_metadata meta; - string name; + private fanotify_event_metadata meta_; + private string name_; + private int fanFd_; // Ссылка на fanotify fd для отправки response (копируется при создании) + // Конструктор (value semantics, копирует данные) + this(fanotify_event_metadata meta, string name, int fanFd) + { + meta_ = meta; + name_ = name; + fanFd_ = fanFd; + } + + // Деструктор: автоматически закрывает fd события, если он валиден (RAII) + ~this() + { + if (meta_.fd >= 0 && meta_.fd != FAN_NOFD) + { + close(meta_.fd); + meta_.fd = -1; // Избегаем повторного закрытия + } + } + + // Геттеры (value types) @property uint64_t mask() const { - return meta.mask; + return meta_.mask; } @property int eventFd() const { - return meta.fd; + return meta_.fd; } @property int pid() const { - return meta.pid; + return meta_.pid; } + @property string name() const + { + return name_; + } + + // Методы проверки событий (без ref) bool isOpen() const { return (mask & FAN_OPEN) != 0; @@ -65,44 +92,97 @@ struct FanotifyEvent { return (mask & FAN_DELETE) != 0; } -} -class Fanotify -{ - private int fd = -1; - - this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE) + bool isOpenPerm() const { - fd = fanotify_init(initFlags, eventFFlags); - enforce(fd >= 0, "Ошибка инициализации fanotify: " ~ to!string(fd)); + return (mask & FAN_OPEN_PERM) != 0; } + bool isAccessPerm() const + { + return (mask & FAN_ACCESS_PERM) != 0; + } + + bool isOpenExecPerm() const + { + return (mask & FAN_OPEN_EXEC_PERM) != 0; + } + + bool isOverflow() const + { + return (mask & FAN_Q_OVERFLOW) != 0; + } + + bool isFsError() const + { + return (mask & FAN_FS_ERROR) != 0; + } + + // Метод для постобработки события (виртуальный, можно override для кастомной логики) + void postProcess() + { + // По умолчанию ничего, но можно добавить логику, например, логирование + } + + // Метод для отправки response (для permission-событий), закрывает fd автоматически после + void respond(uint response) + { + if (eventFd < 0 || eventFd == FAN_NOFD) + { + return; // Нет fd для response + } + + fanotify_response resp; + resp.fd = eventFd; + resp.response = response; + + ssize_t res = write(fanFd_, &resp, fanotify_response.sizeof); + enforce(res == fanotify_response.sizeof, "Ошибка записи response: " ~ strerror(errno) + .fromStringz.to!string); + + // Закрываем fd сразу после response (не ждем деструктора, но деструктор на всякий случай) + close(meta_.fd); + meta_.fd = -1; + } +} + +// Основной ООП-класс для управления fanotify +class Fanotify +{ + private int fd_ = -1; + + // Конструктор: инициализация с флагами + this(uint initFlags, uint eventFFlags = O_RDONLY | O_LARGEFILE) + { + fd_ = fanotify_init(initFlags, eventFFlags); + enforce(fd_ >= 0, "Ошибка инициализации fanotify: " ~ strerror(errno) + .fromStringz.to!string); + } + + // Деструктор: автоматически закрывает fanotify fd ~this() { - if (fd >= 0) + if (fd_ >= 0) { - close(fd); - fd = -1; + close(fd_); + fd_ = -1; } } + // Метод для добавления/удаления/модификации меток (управление событиями) void mark(uint markFlags, uint64_t eventMask, int dirFd = AT_FDCWD, string path = null) { const(char)* cPath = path ? path.toStringz() : null; - int res = fanotify_mark(fd, markFlags, eventMask, dirFd, cPath); - if (res == -1) - { - string errMsg = "Ошибка маркировки fanotify: " ~ to!string( - res) ~ " (errno: " ~ to!string(errno) ~ ", " ~ strerror(errno) - .fromStringz.to!string ~ ")"; - throw new Exception(errMsg); - } + int res = fanotify_mark(fd_, markFlags, eventMask, dirFd, cPath); + enforce(res == 0, "Ошибка маркировки fanotify: " ~ strerror(errno) + .fromStringz.to!string); } + // Метод для чтения событий (возвращает массив объектов событий) FanotifyEvent[] readEvents(size_t bufferSize = 4096) { ubyte[] buffer = new ubyte[bufferSize]; - ssize_t len = read(fd, buffer.ptr, buffer.length); + ssize_t len = read(fd_, buffer.ptr, buffer.length); if (len <= 0) { return []; @@ -112,43 +192,56 @@ class Fanotify size_t offset = 0; while (offset + FAN_EVENT_METADATA_LEN <= len) { - auto meta = cast(fanotify_event_metadata*)(buffer.ptr + offset); + auto meta = *(cast(fanotify_event_metadata*)(buffer.ptr + offset)); if (meta.event_len < FAN_EVENT_METADATA_LEN || offset + meta.event_len > len) { break; } - FanotifyEvent ev = FanotifyEvent(*meta); - + string name; size_t infoOffset = offset + fanotify_event_metadata.sizeof; while (infoOffset < offset + meta.event_len) { - auto hdr = cast(fanotify_event_info_header*)(buffer.ptr + infoOffset); + auto hdr = *(cast(fanotify_event_info_header*)(buffer.ptr + infoOffset)); if (hdr.len == 0 || infoOffset + hdr.len > offset + meta.event_len) { break; } - if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME) + if (hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME || + hdr.info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME || + hdr.info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) { size_t fidOffset = infoOffset + fanotify_event_info_header.sizeof + __kernel_fsid_t.sizeof; - auto handle = cast(file_handle*)(buffer.ptr + fidOffset); + auto handle = *(cast(file_handle*)(buffer.ptr + fidOffset)); size_t handleEnd = fidOffset + file_handle.sizeof + handle.handle_bytes; if (handleEnd < offset + meta.event_len) { - ev.name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string; + name = (cast(char*)(buffer.ptr + handleEnd)).fromStringz.to!string; } } infoOffset += hdr.len; } + + auto ev = new FanotifyEvent(meta, name, fd_); events ~= ev; offset += meta.event_len; } return events; } + // Метод для постобработки всех событий (вызывает postProcess на каждом) + void postProcessEvents(FanotifyEvent[] events) + { + foreach (ev; events) + { + ev.postProcess(); + } + } + + // Геттер для fd (если нужно внешне, но лучше использовать методы) @property int handle() const { - return fd; + return fd_; } } From 7858416df993d0aaf95def1d8de50d40b650e3af Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 00:35:45 +0300 Subject: [PATCH 4/8] =?UTF-8?q?=D0=9C=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=20=D1=81=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=B8=D0=BC=D0=B8=20=D0=B4=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D1=80=D0=B0=D0=BC=D0=B8=20+=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=20depoll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 262 +++++++++++++++++++++++++++++++++++++++--------- source/depoll.d | 73 ++++++++++++++ 2 files changed, 285 insertions(+), 50 deletions(-) create mode 100644 source/depoll.d diff --git a/source/app.d b/source/app.d index 4c08dd0..977c0ca 100644 --- a/source/app.d +++ b/source/app.d @@ -1,77 +1,239 @@ -import dfanotify; +module app; -import std.stdio; -import std.format : format; -import core.sys.posix.fcntl : AT_FDCWD; -import core.stdc.stdlib : exit; +import dfanotify; // твой модуль сверху +import depoll; // новый модуль для epoll -void main() +import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE; +import core.stdc.errno : errno; +import core.stdc.string : strerror; +import std.string : fromStringz, toStringz, join; +import std.conv : to; +import std.stdio : writeln, writefln; +import std.getopt; +import std.algorithm : canFind; + +import core.stdc.stdint; +import std.exception; + +// Удобная печать маски (для живых логов) +string maskToStr(uint64_t m) { - Fanotify fan; - try - { - fan = new Fanotify( - FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME - ); - } - catch (Exception e) - { - writeln(e.msg); - exit(1); - } + string[] parts; + if (m & FAN_OPEN) + parts ~= "OPEN"; + if (m & FAN_ACCESS) + parts ~= "ACCESS"; + if (m & FAN_MODIFY) + parts ~= "MODIFY"; + if (m & FAN_CLOSE_WRITE) + parts ~= "CLOSE_WRITE"; + if (m & FAN_CLOSE_NOWRITE) + parts ~= "CLOSE_NOWRITE"; + if (m & FAN_CREATE) + parts ~= "CREATE"; + if (m & FAN_DELETE) + parts ~= "DELETE"; + if (m & FAN_MOVED_FROM) + parts ~= "MOVED_FROM"; + if (m & FAN_MOVED_TO) + parts ~= "MOVED_TO"; + if (m & FAN_OPEN_PERM) + parts ~= "OPEN_PERM"; + if (m & FAN_ACCESS_PERM) + parts ~= "ACCESS_PERM"; + if (m & FAN_OPEN_EXEC) + parts ~= "OPEN_EXEC"; + if (m & FAN_OPEN_EXEC_PERM) + parts ~= "OPEN_EXEC_PERM"; + if (m & FAN_Q_OVERFLOW) + parts ~= "Q_OVERFLOW"; + if (m & FAN_FS_ERROR) + parts ~= "FS_ERROR"; + return parts.length ? parts.join("|") : "0"; +} - auto eventMask = FAN_OPEN | FAN_MODIFY | FAN_CLOSE | FAN_CREATE | FAN_DELETE | FAN_EVENT_ON_CHILD | FAN_ACCESS; +void main(string[] args) +{ + string watchPath = "/tmp/scripts"; + bool markMount = false; + + getopt(args, + "path|p", &watchPath, + "mount", &markMount, + ); + + writefln("[*] Watch: %s (mode: %s)", watchPath, markMount ? "MOUNT" : "DIR"); + + Fanotify pre, notif; + + // --------------------------- + // 1) Создаём две группы + // --------------------------- + + // PRE_CONTENT: решаем "до доступа" + // Важно: для permission-событий обязательно включить REPORT_* для имени + // и DFID (иначе имя не извлечёшь). + uint preInitFlags = + FAN_CLASS_PRE_CONTENT + | FAN_CLOEXEC + | FAN_NONBLOCK; try { - fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, eventMask, AT_FDCWD, "/tmp/scripts"); + pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE); } catch (Exception e) { - writeln(e.msg); - exit(1); + writeln("pre: ", e.msg); } - writeln( - "Мониторинг с разрешениями запущен для /tmp/scripts..."); + // NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п. + uint notifInitFlags = + FAN_CLASS_NOTIF + | FAN_CLOEXEC + | FAN_NONBLOCK + | FAN_REPORT_FID + | FAN_REPORT_DIR_FID + | FAN_REPORT_NAME; + + try + { + notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE); + } + catch (Exception e) + { + writeln("notif", e.msg); + } + + // --------------------------- + // 2) Ставим метки + // --------------------------- + + // Что именно хотим ловить + enum uint64_t PERM_MASK = + FAN_OPEN_PERM + | FAN_ACCESS_PERM + | FAN_OPEN_EXEC_PERM; + + enum uint64_t NOTIF_MASK = + FAN_OPEN + | FAN_OPEN_EXEC + | FAN_MODIFY + | FAN_CLOSE_WRITE + | FAN_CLOSE_NOWRITE + | FAN_CREATE + | FAN_DELETE + | FAN_MOVED_FROM + | FAN_MOVED_TO; + + // Важно: для слежения за потомками каталога добавляем FAN_EVENT_ON_CHILD. + auto preMask = PERM_MASK | FAN_EVENT_ON_CHILD; + auto notifMask = NOTIF_MASK | FAN_EVENT_ON_CHILD; + + uint baseFlags = FAN_MARK_ADD /*| FAN_MARK_ONLYDIR*/ ; + + // Если нужно «видеть всё на файловой системе/монтировании», используй MOUNT: + // (на многих сценариях это предпочтительно, иначе будет только каталог + его прямые дети) + if (markMount) + { + baseFlags |= FAN_MARK_MOUNT; + } + + // Метки на обе группы + pre.mark(baseFlags, preMask, /*dirFd*/ 0, watchPath); + notif.mark(baseFlags, notifMask, /*dirFd*/ 0, watchPath); + + // --------------------------- + // 3) Готовим epoll (теперь через ООП-класс) + // --------------------------- + + Epoll ep = new Epoll(); + + enum uint TAG_PRE = 1; + enum uint TAG_NOTIF = 2; + + ep.add(pre.handle, TAG_PRE); + ep.add(notif.handle, TAG_NOTIF); + + // --------------------------- + // 4) Главный цикл + // --------------------------- while (true) { - auto events = fan.readEvents(); + auto epEvents = ep.wait(); - foreach (e; events) + foreach (epEv; epEvents) { - string path = e.name.length ? e.name : "unknown"; + auto tag = epEv.tag; - writefln("Событие: mask=0x%x, pid=%d, name/path=%s", e.mask, e.pid, path); - - // Обработка permission-события - if (e.isOpenPerm) + // Считываем пачку событий с соответствующего fd + FanotifyEvent[] events; + if (tag == TAG_PRE) { - writeln(" - Запрос на открытие файла. Отклоняем (предполагаем попытку записи)."); - e.respond(FAN_DENY); // Отклонить (для FAN_ALLOW используйте FAN_ALLOW) + events = pre.readEvents(64 * 1024); + } + else if (tag == TAG_NOTIF) + { + events = notif.readEvents(64 * 1024); } else { - // Обычные уведомления (не permission) - if (e.isOpen) - writeln(" - Открытие файла"); - if (e.isAccess) - writeln(" - Доступ к файлу"); - if (e.isModify) - writeln(" - Модификация файла"); - if (e.isCloseWrite) - writeln(" - Закрытие после записи"); - if (e.isCloseNoWrite) - writeln(" - Закрытие без записи"); - if (e.isCreate) - writeln(" - Создание файла/директории"); - if (e.isDelete) - writeln(" - Удаление файла/директории"); + continue; } - // Опционально: вызов постобработки - e.postProcess(); + foreach (ev; events) + { + // Лог + writefln("[%s] pid=%s mask=%s name=%s", + (tag == TAG_PRE) ? "PRE" : "NOTIF", + ev.pid.to!string, + maskToStr(ev.mask), + ev.name.length ? ev.name : "(unknown)"); + + // --- ВЕТКА РЕШЕНИЯ ДЛЯ PERM-СОБЫТИЙ --- + if (tag == TAG_PRE && + (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)) + { + // Пример «политики»: запрещать выполнение из временных директорий + // (поменяй на свои условия) + bool deny = false; + + // простейший сэмпл-правил + auto nm = ev.name; + if (nm.length) + { + if (nm.canFind("/tmp/") && ev.isOpenExecPerm) + { + deny = true; + } + } + + // Отправляем решение ядру + ev.respond(deny ? FAN_DENY : FAN_ALLOW); + + writefln(" -> %s", deny ? "DENY" : "ALLOW"); + } + + // --- ПОСТ-ФАКТУМ УВЕДОМЛЕНИЯ --- + if (tag == TAG_NOTIF) + { + // Реакции в реальном времени + if (ev.isCloseWrite) + { + // Напр., дернуть диф/бэкап/индексацию + // snag --config ... create --comment "CLOSE_WRITE - " ... + writefln(" -> HANDLE CLOSE_WRITE for: %s", ev.name); + } + else if (ev.isModify) + { + writefln(" -> HANDLE MODIFY for: %s", ev.name); + } + } + + // Кастомный postProcess если надо + ev.postProcess(); + } } } } diff --git a/source/depoll.d b/source/depoll.d new file mode 100644 index 0000000..8b85272 --- /dev/null +++ b/source/depoll.d @@ -0,0 +1,73 @@ +module depoll; + +import core.sys.linux.epoll; +import core.sys.posix.unistd : close; +import core.stdc.errno : errno, EINTR; +import core.stdc.string : strerror; +import std.string : fromStringz; +import std.exception : enforce; + +class Epoll +{ + private int epfd_ = -1; + + this(int flags = 0) + { + epfd_ = epoll_create1(flags); + enforce(epfd_ >= 0, "epoll_create1: " ~ strerror(errno).fromStringz.idup); + } + + ~this() + { + if (epfd_ >= 0) + { + close(epfd_); + epfd_ = -1; + } + } + + void add(int fd, uint tag, uint events = EPOLLIN | EPOLLERR | EPOLLHUP) + { + epoll_event ev; + ev.events = events; + ev.data.u32 = tag; + enforce(epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev) == 0, + "epoll_ctl ADD: " ~ strerror(errno).fromStringz.idup); + } + + // Опционально: метод для удаления fd + void remove(int fd) + { + enforce(epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null) == 0, + "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup); + } + + struct Event + { + uint tag; + uint events; + } + + Event[] wait(int maxevents = 16, int timeout = -1) + { + epoll_event[] evs = new epoll_event[maxevents]; + int n = epoll_wait(epfd_, evs.ptr, maxevents, timeout); + if (n <= 0) + { + // Игнорируем EINTR и другие (как в оригинале: continue) + return []; + } + + Event[] res; + foreach (i; 0 .. n) + { + res ~= Event(evs[i].data.u32, evs[i].events); + } + return res; + } + + @property int handle() const + { + return epfd_; + } +} From 1f78d8eb83c4bfea833b45357ddc84ed4f500bbc Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 01:17:03 +0300 Subject: [PATCH 5/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20PERM=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0,=20=D0=BF=D0=BE=D0=B7=D0=B2=D0=BE=D0=BB=D1=8F=D1=8E?= =?UTF-8?q?=D1=89=D0=B0=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=83=D1=82=D1=8C=20=D0=BA=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D1=83=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 67 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/source/app.d b/source/app.d index 977c0ca..3b56af9 100644 --- a/source/app.d +++ b/source/app.d @@ -11,10 +11,30 @@ import std.conv : to; import std.stdio : writeln, writefln; import std.getopt; import std.algorithm : canFind; +import core.sys.posix.unistd : readlink; import core.stdc.stdint; import std.exception; +import std.path : baseName; + +/// Вернуть путь для fd из события PRE_CONTENT. +/// Может вернуть "(deleted)" или пустую строку, если объект без имени. +string pathFromEventFd(int fd) +{ + // Конструируем путь в /proc/self/fd + auto linkPath = "/proc/self/fd/" ~ fd.to!string; + char[4096] buf; + + // readlink не добавляет '\0' + auto n = readlink(linkPath.toStringz, buf.ptr, buf.length); + if (n < 0) + { + return ""; + } + return cast(string) buf[0 .. n].idup; +} + // Удобная печать маски (для живых логов) string maskToStr(uint64_t m) { @@ -184,6 +204,15 @@ void main(string[] args) foreach (ev; events) { + if (tag == TAG_PRE && (ev.isOpenPerm || ev.isAccessPerm)) + { + string path = pathFromEventFd(ev.eventFd); + writeln("Попытка доступа к: ", path.length ? path + : "<неизвестно>"); + // ev.respond(FAN_ALLOW); // или FAN_DENY по своей политике + baseName(path) == "test" ? ev.respond(FAN_DENY) : ev.respond(FAN_ALLOW); + } + // Лог writefln("[%s] pid=%s mask=%s name=%s", (tag == TAG_PRE) ? "PRE" : "NOTIF", @@ -192,28 +221,28 @@ void main(string[] args) ev.name.length ? ev.name : "(unknown)"); // --- ВЕТКА РЕШЕНИЯ ДЛЯ PERM-СОБЫТИЙ --- - if (tag == TAG_PRE && - (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)) - { - // Пример «политики»: запрещать выполнение из временных директорий - // (поменяй на свои условия) - bool deny = false; + // if (tag == TAG_PRE && + // (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)) + // { + // // Пример «политики»: запрещать выполнение из временных директорий + // // (поменяй на свои условия) + // bool deny = false; - // простейший сэмпл-правил - auto nm = ev.name; - if (nm.length) - { - if (nm.canFind("/tmp/") && ev.isOpenExecPerm) - { - deny = true; - } - } + // // простейший сэмпл-правил + // auto nm = ev.name; + // if (nm.length) + // { + // if (nm.canFind("/tmp/") && ev.isOpenExecPerm) + // { + // deny = true; + // } + // } - // Отправляем решение ядру - ev.respond(deny ? FAN_DENY : FAN_ALLOW); + // // Отправляем решение ядру + // ev.respond(deny ? FAN_DENY : FAN_ALLOW); - writefln(" -> %s", deny ? "DENY" : "ALLOW"); - } + // writefln(" -> %s", deny ? "DENY" : "ALLOW"); + // } // --- ПОСТ-ФАКТУМ УВЕДОМЛЕНИЯ --- if (tag == TAG_NOTIF) From bc02ac8518c34ee9cab2d5afab67663cd4bacb52 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 01:27:33 +0300 Subject: [PATCH 6/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=86=D0=B5=D1=81=D1=81=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 87 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/source/app.d b/source/app.d index 3b56af9..fffdac5 100644 --- a/source/app.d +++ b/source/app.d @@ -6,7 +6,7 @@ import depoll; // новый модуль для epoll import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE; import core.stdc.errno : errno; import core.stdc.string : strerror; -import std.string : fromStringz, toStringz, join; +import std.string : fromStringz, toStringz, join, strip; import std.conv : to; import std.stdio : writeln, writefln; import std.getopt; @@ -18,6 +18,8 @@ import std.exception; import std.path : baseName; +import std.file : readText, exists, read; + /// Вернуть путь для fd из события PRE_CONTENT. /// Может вернуть "(deleted)" или пустую строку, если объект без имени. string pathFromEventFd(int fd) @@ -35,6 +37,76 @@ string pathFromEventFd(int fd) return cast(string) buf[0 .. n].idup; } +/// Возвращает "имя процесса" для события fanotify. +/// Приоритет: /proc//comm → имя ссылки /proc//exe → первый токен из cmdline. +/// Если всё сломалось — "pid:". +string eventProcessName(int pid) +{ + // 1) /proc//comm — самое точное короткое имя + const commPath = "/proc/" ~ pid.to!string ~ "/comm"; + try + { + if (exists(commPath)) + { + auto s = readText(commPath).strip; // там обычно одна строка с \n + if (s.length) + return s; + } + } + catch (Exception e) + { + } + + // 2) /proc//exe — полный путь к исполняемому файлу + try + { + char[4096] buf; + auto link = "/proc/" ~ pid.to!string ~ "/exe"; + auto n = readlink(link.toStringz, buf.ptr, buf.length); + if (n > 0) + { + auto exePath = cast(string) buf[0 .. n].idup; + auto name = baseName(exePath); + if (name.length) + return name; + } + } + catch (Exception e) + { + } + + // 3) /proc//cmdline — первый NUL-разделённый аргумент + try + { + const cmdPath = "/proc/" ~ pid.to!string ~ "/cmdline"; + if (exists(cmdPath)) + { + auto bytes = cast(const(ubyte)[]) read(cmdPath); + if (bytes.length) + { + // до первого \0 + size_t i = 0; + for (; i < bytes.length && bytes[i] != 0; ++i) + { + } + auto first = cast(string) bytes[0 .. i].idup; + if (first.length) + { + auto name = baseName(first); + if (name.length) + return name; + } + } + } + } + catch (Exception e) + { + } + + // fallback + return "pid:" ~ pid.to!string; +} + // Удобная печать маски (для живых логов) string maskToStr(uint64_t m) { @@ -204,6 +276,9 @@ void main(string[] args) foreach (ev; events) { + auto pname = eventProcessName(ev.pid); + writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); + if (tag == TAG_PRE && (ev.isOpenPerm || ev.isAccessPerm)) { string path = pathFromEventFd(ev.eventFd); @@ -214,11 +289,11 @@ void main(string[] args) } // Лог - writefln("[%s] pid=%s mask=%s name=%s", - (tag == TAG_PRE) ? "PRE" : "NOTIF", - ev.pid.to!string, - maskToStr(ev.mask), - ev.name.length ? ev.name : "(unknown)"); + // writefln("[%s] pid=%s mask=%s name=%s", + // (tag == TAG_PRE) ? "PRE" : "NOTIF", + // ev.pid.to!string, + // maskToStr(ev.mask), + // ev.name.length ? ev.name : "(unknown)"); // --- ВЕТКА РЕШЕНИЯ ДЛЯ PERM-СОБЫТИЙ --- // if (tag == TAG_PRE && From 2876a45a85652ff3a505dd6d1caac11367fb5d68 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 02:33:55 +0300 Subject: [PATCH 7/8] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8F,=20=D0=BD=D0=BE=20=D0=BD=D0=B5=20100%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 16 ++++- source/dfanotify.d | 5 ++ source/event_actor.d | 139 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 source/event_actor.d diff --git a/source/app.d b/source/app.d index fffdac5..6b67df9 100644 --- a/source/app.d +++ b/source/app.d @@ -2,6 +2,7 @@ module app; import dfanotify; // твой модуль сверху import depoll; // новый модуль для epoll +import event_actor; import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE; import core.stdc.errno : errno; @@ -277,7 +278,16 @@ void main(string[] args) foreach (ev; events) { auto pname = eventProcessName(ev.pid); - writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); + // writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); + writefln("[%s] pid = %s proc = %s mask = %s", + (tag == TAG_PRE) ? "PRE" : "NOTIF", + ev.pid, pname, + maskToStr(ev.mask) + ); + auto actor = eventActorFromPid(ev.pid); + writeln("user=", actor.user, " uid=", actor.uid, + " ruid=", actor.ruid, "(", actor.ruser, ")", + " euid=", actor.euid, "(", actor.euser, ")"); if (tag == TAG_PRE && (ev.isOpenPerm || ev.isAccessPerm)) { @@ -329,6 +339,10 @@ void main(string[] args) // snag --config ... create --comment "CLOSE_WRITE - " ... writefln(" -> HANDLE CLOSE_WRITE for: %s", ev.name); } + else if (ev.isMoved) + { + writefln(" -> HANDLE MOVED for: %s", ev.name); + } else if (ev.isModify) { writefln(" -> HANDLE MODIFY for: %s", ev.name); diff --git a/source/dfanotify.d b/source/dfanotify.d index 8144d08..b4d4d30 100644 --- a/source/dfanotify.d +++ b/source/dfanotify.d @@ -83,6 +83,11 @@ class FanotifyEvent return (mask & FAN_ACCESS) != 0; } + bool isMoved() const + { + return (mask & FAN_MOVED_FROM) != 0; + } + bool isCreate() const { return (mask & FAN_CREATE) != 0; diff --git a/source/event_actor.d b/source/event_actor.d new file mode 100644 index 0000000..5ac27de --- /dev/null +++ b/source/event_actor.d @@ -0,0 +1,139 @@ +module event_actor; + +import core.sys.posix.pwd : passwd, getpwuid_r; +import core.sys.posix.sys.types : uid_t; +import core.stdc.stdlib : malloc, free; +import core.stdc.string : strlen; +import std.file : readText, exists; +import std.string : splitLines, strip; +import std.conv : to; + +// для токенизации по пробельным +import std.ascii : isWhite; +import std.algorithm.iteration : splitter; +import std.array : array; + +struct EventActor +{ + uid_t uid; // выбранный UID (loginuid если есть, иначе ruid) + string user; // имя пользователя для uid (может быть пустым) + uid_t ruid, euid, suid, fsuid; + string ruser, euser, suser, fsuser; + bool hasLoginuid; + uid_t loginuid; + string loginuser; +} + +private string userName(uid_t uid) +{ + enum BUF = 4096; + auto buf = cast(char*) malloc(BUF); + scope (exit) + if (buf) + free(buf); + passwd pwd; + passwd* outp; + auto rc = getpwuid_r(uid, &pwd, buf, BUF, &outp); + if (rc == 0 && outp !is null) + { + return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup; + } + return ""; +} + +/// Парсит строку вида: "Uid:\t\t\t\t" +private bool parseUidLine(string line, out uid_t r, out uid_t e, out uid_t s, out uid_t f) +{ + // разобьём по любым пробельным символам + auto toks = line.splitter!isWhite.array; // ["Uid:", "1000", "1000", "1000", "1000"] + if (toks.length >= 5 && toks[0] == "Uid:") + { + try + { + r = toks[1].to!uid_t; + e = toks[2].to!uid_t; + s = toks[3].to!uid_t; + f = toks[4].to!uid_t; + return true; + } + catch (Exception e) + { + } + } + return false; +} + +/// Главная функция: кто инициировал событие (по PID из fanotify_event_metadata) +EventActor eventActorFromPid(int pid) +{ + EventActor a; + if (pid <= 0) + return a; + + const base = "/proc/" ~ pid.to!string ~ "/"; + const statusPath = base ~ "status"; + + if (exists(statusPath)) + { + try + { + auto txt = readText(statusPath); + uid_t r = 0, e = 0, s = 0, f = 0; + foreach (line; txt.splitLines()) + { + // ищем строку "Uid:" + if (line.length >= 4 && line[0 .. 4] == "Uid:") + { + if (parseUidLine(line, r, e, s, f)) + { + a.ruid = r; + a.euid = e; + a.suid = s; + a.fsuid = f; + a.ruser = userName(r); + a.euser = userName(e); + a.suser = userName(s); + a.fsuser = userName(f); + } + break; + } + } + } + catch (Exception e) + { + } + } + + // loginuid — владелец сессии (если audit включён) + const loginPath = base ~ "loginuid"; + if (exists(loginPath)) + { + try + { + auto s = readText(loginPath).strip; + if (s.length) + { + a.loginuid = s.to!uid_t; + a.loginuser = userName(a.loginuid); + a.hasLoginuid = true; + } + } + catch (Exception e) + { + } + } + + // выбираем «итогового» пользователя + if (a.hasLoginuid) + { + a.uid = a.loginuid; + a.user = a.loginuser.length ? a.loginuser : ("uid:" ~ a.loginuid.to!string); + } + else + { + a.uid = a.ruid; + a.user = a.ruser.length ? a.ruser : ("uid:" ~ a.ruid.to!string); + } + + return a; +} From 377d5c1f941d3433c9ca103f5e8fc0c901f95c2e Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 23:43:22 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D0=92=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=B4?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20NOTIFY=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/app.d b/source/app.d index 6b67df9..5a85ec0 100644 --- a/source/app.d +++ b/source/app.d @@ -279,10 +279,11 @@ void main(string[] args) { auto pname = eventProcessName(ev.pid); // writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); - writefln("[%s] pid = %s proc = %s mask = %s", + writefln("[%s] pid = %s proc = %s mask = %s name = %s", (tag == TAG_PRE) ? "PRE" : "NOTIF", ev.pid, pname, - maskToStr(ev.mask) + maskToStr(ev.mask), + ev.name.length ? ev.name : "<неизвестно>" ); auto actor = eventActorFromPid(ev.pid); writeln("user=", actor.user, " uid=", actor.uid,