From 7858416df993d0aaf95def1d8de50d40b650e3af Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 29 Aug 2025 00:35:45 +0300 Subject: [PATCH] =?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_; + } +}