Механизм отслеживания записи в файл (без фиксации изменений в журнал)
This commit is contained in:
parent
377d5c1f94
commit
aa214dcf96
5 changed files with 383 additions and 446 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
@ -20,7 +20,7 @@
|
||||||
"type": "cppdbg",
|
"type": "cppdbg",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/bin/dwatch",
|
"program": "${workspaceFolder}/bin/dwatch",
|
||||||
"args": [], // Аргументы командной строки для программы, если нужны
|
"args": ["-d", "/tmp/scripts"], // Аргументы командной строки для программы, если нужны
|
||||||
"stopAtEntry": false, // Остановить на входе в main
|
"stopAtEntry": false, // Остановить на входе в main
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"environment": [],
|
"environment": [],
|
||||||
|
|
74
scheme.md
Normal file
74
scheme.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
%%{init: { 'theme': 'default', 'themeVariables': {
|
||||||
|
'primaryColor': '#ff0000',
|
||||||
|
'nodeTextColor': '#ffffff',
|
||||||
|
'edgeLabelBackground': '#f0f0f0'
|
||||||
|
}}}%%
|
||||||
|
autonumber
|
||||||
|
participant Watcher as Демон-наблюдатель
|
||||||
|
participant Kernel as Ядро (fanotify)
|
||||||
|
participant Editor as Процесс-редактор (PID X)
|
||||||
|
participant FS as Файловая система (целевой каталог/файл)
|
||||||
|
participant Ep as epoll
|
||||||
|
participant Pfd as pidfd(PID X)
|
||||||
|
participant Map as Карта соответствий (в памяти)
|
||||||
|
participant ChLog as Журнал изменений
|
||||||
|
|
||||||
|
Note over Watcher: Инициализация
|
||||||
|
Watcher->>Kernel: fanotify_init(PRE_CONTENT|NONBLOCK|CLOEXEC, O_RDONLY|O_CLOEXEC|O_LARGEFILE)
|
||||||
|
Watcher->>Kernel: fanotify_mark(ADD|ONLYDIR, EVENT_ON_CHILD | OPEN[_EXEC]_PERM | OPEN | CLOSE_WRITE, watchDir)
|
||||||
|
Watcher->>Ep: add(fanotify_fd, tag=FAN)
|
||||||
|
|
||||||
|
Note over Editor: Запрос доступа к файлу
|
||||||
|
Editor->>FS: open(target, O_RDONLY/O_RDWR/EXEC)
|
||||||
|
FS-->>Kernel: системный вызов доступа
|
||||||
|
Kernel-->>Watcher: PERM-событие (+fd, mask, pid)
|
||||||
|
|
||||||
|
Note over Watcher: Сбор контекста
|
||||||
|
Watcher->>FS: fstat(fd) → (inode, dev)
|
||||||
|
FS-->>Watcher: inode, dev
|
||||||
|
Watcher->>Watcher: readlink(/proc/self/fd/FD) → file_path_open
|
||||||
|
Watcher->>Watcher: read /proc/PID/comm → proc_name
|
||||||
|
Watcher->>Watcher: read /proc/PID/status(Uid:) → RUID
|
||||||
|
Watcher->>Watcher: read /proc/PID/loginuid → UID(loginuid)
|
||||||
|
|
||||||
|
%% Кладём связку
|
||||||
|
Watcher->>Map: PUT key=(PID,inode,dev), val={proc_name, RUID, UID, file_path_open, changed=false}
|
||||||
|
Note over Map: Добавлена связка key=(PID,inode,dev)
|
||||||
|
|
||||||
|
%% pidfd и epoll
|
||||||
|
Watcher->>Pfd: pidfd_open(PID, flags=0)
|
||||||
|
Pfd-->>Watcher: pidfd
|
||||||
|
Watcher->>Ep: add(pidfd, tag=PID, events=IN|HUP|RDHUP|ERR)
|
||||||
|
|
||||||
|
%% Разрешаем доступ
|
||||||
|
Watcher->>Kernel: fanotify_response{fd, FAN_ALLOW}
|
||||||
|
Watcher->>FS: close(fd)
|
||||||
|
|
||||||
|
Note over Editor: Работа с файлом
|
||||||
|
Editor->>FS: write(...)
|
||||||
|
Editor->>FS: close()
|
||||||
|
Kernel-->>Watcher: CLOSE_WRITE (+fd)
|
||||||
|
|
||||||
|
Note over Watcher: Фиксация изменения
|
||||||
|
Watcher->>FS: fstat(fd) → (inode, dev)
|
||||||
|
FS-->>Watcher: inode, dev
|
||||||
|
Watcher->>Map: GET key=(PID,inode,dev)
|
||||||
|
Map-->>Watcher: {…}
|
||||||
|
Watcher->>Map: SET changed=true
|
||||||
|
Watcher->>FS: close(fd)
|
||||||
|
|
||||||
|
Note over Editor: Завершение процесса
|
||||||
|
Editor->>Editor: exit()
|
||||||
|
Pfd-->>Ep: EPOLLIN/HUP (процесс завершён)
|
||||||
|
Ep-->>Watcher: событие(tag=PID)
|
||||||
|
|
||||||
|
%% Пишем журнал и чистим
|
||||||
|
Watcher->>Map: GET_ALL where pid=PID
|
||||||
|
Map-->>Watcher: список записей
|
||||||
|
Watcher->>ChLog: APPEND для changed=true → {proc=proc_name, file=file_path_open, uid=UID(loginuid), ruid=RUID}
|
||||||
|
Watcher->>Ep: remove(pidfd); Watcher->>Pfd: close(pidfd)
|
||||||
|
Watcher->>Map: DELETE_ALL where pid=PID
|
||||||
|
Note over Map: Удалены все связки для PID
|
||||||
|
```
|
603
source/app.d
603
source/app.d
|
@ -1,357 +1,346 @@
|
||||||
module app;
|
module app;
|
||||||
|
|
||||||
import dfanotify; // твой модуль сверху
|
import dfanotify;
|
||||||
import depoll; // новый модуль для epoll
|
import fanotify;
|
||||||
import event_actor;
|
import depoll;
|
||||||
|
|
||||||
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE;
|
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD;
|
||||||
import core.stdc.errno : errno;
|
import core.sys.posix.unistd : read, close;
|
||||||
|
import core.sys.posix.sys.stat : fstat, stat_t;
|
||||||
|
import core.stdc.errno : errno, EINTR;
|
||||||
import core.stdc.string : strerror;
|
import core.stdc.string : strerror;
|
||||||
import std.string : fromStringz, toStringz, join, strip;
|
import std.stdio : writeln, writefln, stderr;
|
||||||
|
import std.file : isDir, readText, exists;
|
||||||
|
import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split;
|
||||||
import std.conv : to;
|
import std.conv : to;
|
||||||
import std.stdio : writeln, writefln;
|
import std.exception : enforce;
|
||||||
import std.getopt;
|
import std.getopt : getopt, defaultGetoptPrinter;
|
||||||
import std.algorithm : canFind;
|
import std.format : format;
|
||||||
import core.sys.posix.unistd : readlink;
|
import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC;
|
||||||
|
|
||||||
import core.stdc.stdint;
|
// Переписать
|
||||||
import std.exception;
|
import core.sys.posix.sys.types : uid_t;
|
||||||
|
import core.sys.posix.pwd : passwd, getpwuid_r;
|
||||||
|
import core.stdc.stdlib : malloc, free;
|
||||||
|
import core.stdc.string : strlen;
|
||||||
|
|
||||||
import std.path : baseName;
|
/// ---- syscall + pidfd_open ----
|
||||||
|
extern (C) long syscall(long number, ...);
|
||||||
import std.file : readText, exists, read;
|
version (X86_64) enum __NR_pidfd_open = 434;
|
||||||
|
extern (C) int pidfd_open(int pid, uint flags)
|
||||||
/// Вернуть путь для fd из события PRE_CONTENT.
|
|
||||||
/// Может вернуть "(deleted)" или пустую строку, если объект без имени.
|
|
||||||
string pathFromEventFd(int fd)
|
|
||||||
{
|
{
|
||||||
// Конструируем путь в /proc/self/fd
|
return cast(int) syscall(__NR_pidfd_open, pid, flags);
|
||||||
auto linkPath = "/proc/self/fd/" ~ fd.to!string;
|
}
|
||||||
char[4096] buf;
|
|
||||||
|
|
||||||
// readlink не добавляет '\0'
|
/// ---- readlink по fd ----
|
||||||
auto n = readlink(linkPath.toStringz, buf.ptr, buf.length);
|
extern (C) long readlink(const char* path, char* buf, size_t bufsiz);
|
||||||
if (n < 0)
|
string readlinkFdPath(int fd)
|
||||||
|
{
|
||||||
|
char[4_096] buf;
|
||||||
|
auto link = format("/proc/self/fd/%d", fd);
|
||||||
|
auto n = readlink(link.toStringz, buf.ptr, buf.length.to!int - 1);
|
||||||
|
if (n <= 0)
|
||||||
|
return "";
|
||||||
|
buf[n] = 0;
|
||||||
|
return buf.ptr.fromStringz.idup.strip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ----
|
||||||
|
struct ProcIds
|
||||||
|
{
|
||||||
|
uint ruid;
|
||||||
|
uint uid;
|
||||||
|
} // uid = loginuid
|
||||||
|
|
||||||
|
string readProcComm(int pid)
|
||||||
|
{
|
||||||
|
auto p = format("/proc/%s/comm", pid);
|
||||||
|
string s;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
s = readText(p);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return cast(string) buf[0 .. n].idup;
|
return s.strip;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает "имя процесса" для события fanotify.
|
uint readLoginUid(int pid)
|
||||||
/// Приоритет: /proc/<pid>/comm → имя ссылки /proc/<pid>/exe → первый токен из cmdline.
|
|
||||||
/// Если всё сломалось — "pid:<pid>".
|
|
||||||
string eventProcessName(int pid)
|
|
||||||
{
|
{
|
||||||
// 1) /proc/<pid>/comm — самое точное короткое имя
|
auto p = format("/proc/%s/loginuid", pid);
|
||||||
const commPath = "/proc/" ~ pid.to!string ~ "/comm";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (exists(commPath))
|
auto s = readText(p).strip;
|
||||||
{
|
// loginuid может быть "-1" (unset). В этом случае вернём 4294967295 или 0 — выбери политику.
|
||||||
auto s = readText(commPath).strip; // там обычно одна строка с \n
|
if (s.length && s[0] == '-')
|
||||||
if (s.length)
|
return uint.max; // помечаем как "нет"
|
||||||
return s;
|
return s.to!uint;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
return uint.max; // нет файла или отказ — считаем не установленным
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) /proc/<pid>/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/<pid>/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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удобная печать маски (для живых логов)
|
ProcIds readProcIds(int pid)
|
||||||
string maskToStr(uint64_t m)
|
|
||||||
{
|
{
|
||||||
string[] parts;
|
uint ruid = 0;
|
||||||
if (m & FAN_OPEN)
|
auto p = format("/proc/%s/status", pid);
|
||||||
parts ~= "OPEN";
|
string s;
|
||||||
if (m & FAN_ACCESS)
|
try
|
||||||
parts ~= "ACCESS";
|
{
|
||||||
if (m & FAN_MODIFY)
|
s = readText(p);
|
||||||
parts ~= "MODIFY";
|
}
|
||||||
if (m & FAN_CLOSE_WRITE)
|
catch (Exception e)
|
||||||
parts ~= "CLOSE_WRITE";
|
{
|
||||||
if (m & FAN_CLOSE_NOWRITE)
|
return ProcIds(0, readLoginUid(pid));
|
||||||
parts ~= "CLOSE_NOWRITE";
|
}
|
||||||
if (m & FAN_CREATE)
|
foreach (line; s.splitLines)
|
||||||
parts ~= "CREATE";
|
{
|
||||||
if (m & FAN_DELETE)
|
if (line.startsWith("Uid:"))
|
||||||
parts ~= "DELETE";
|
{
|
||||||
if (m & FAN_MOVED_FROM)
|
// Uid: RUID EUID SUID FSUID
|
||||||
parts ~= "MOVED_FROM";
|
auto parts = line.split;
|
||||||
if (m & FAN_MOVED_TO)
|
if (parts.length >= 5)
|
||||||
parts ~= "MOVED_TO";
|
{
|
||||||
if (m & FAN_OPEN_PERM)
|
ruid = parts[1].to!uint;
|
||||||
parts ~= "OPEN_PERM";
|
}
|
||||||
if (m & FAN_ACCESS_PERM)
|
break;
|
||||||
parts ~= "ACCESS_PERM";
|
}
|
||||||
if (m & FAN_OPEN_EXEC)
|
}
|
||||||
parts ~= "OPEN_EXEC";
|
return ProcIds(ruid, readLoginUid(pid));
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void main(string[] args)
|
/// ---- ключ карты соответствий: (pid, dev, ino) ----
|
||||||
|
struct DevIno
|
||||||
{
|
{
|
||||||
string watchPath = "/tmp/scripts";
|
ulong dev;
|
||||||
bool markMount = false;
|
ulong ino;
|
||||||
|
int pid;
|
||||||
getopt(args,
|
bool opEquals(const DevIno rhs) const @safe nothrow
|
||||||
"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
|
|
||||||
{
|
{
|
||||||
pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE);
|
return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid;
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
writeln("pre: ", e.msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п.
|
size_t toHash() const @safe nothrow
|
||||||
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);
|
return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
}
|
||||||
|
|
||||||
|
bool getDevIno(int fd, out DevIno di, int pid)
|
||||||
|
{
|
||||||
|
stat_t st;
|
||||||
|
if (fstat(fd, &st) != 0)
|
||||||
|
return false;
|
||||||
|
di = DevIno(cast(ulong) st.st_dev, cast(ulong) st.st_ino, pid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- сессия файла для конкретного PID ----
|
||||||
|
struct Session
|
||||||
|
{
|
||||||
|
string procName;
|
||||||
|
uint ruid; // из /proc/PID/status (Uid:)
|
||||||
|
uint uid; // LOGINUID из /proc/PID/loginuid
|
||||||
|
string filePathOpen; // путь, снятый на PERM
|
||||||
|
bool changed; // был CLOSE_WRITE
|
||||||
|
}
|
||||||
|
|
||||||
|
__gshared Session[DevIno] gMap; // (pid,dev,ino) -> Session
|
||||||
|
__gshared int[int] gPidfdByPid; // pid -> pidfd
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
writeln("notif", e.msg);
|
return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- журнал: proc, file, uid(loginuid), ruid ----
|
||||||
|
void logChange(string procName, string filePath, uint uid, uint ruid)
|
||||||
|
{
|
||||||
|
writefln(`proc="%s" file="%s" user=%s(%s) realUser=%s(%s)`,
|
||||||
|
procName, filePath, userName(uid), uid, userName(ruid), ruid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- маска на КАТАЛОГ ----
|
||||||
|
ulong dirMask()
|
||||||
|
{
|
||||||
|
return FAN_EVENT_ON_CHILD |
|
||||||
|
FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM | FAN_ACCESS_PERM |
|
||||||
|
FAN_OPEN | FAN_CLOSE_WRITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- обработчики ----
|
||||||
|
void onOpenPerm(FanotifyEvent ev, Epoll ep)
|
||||||
|
{
|
||||||
|
const pid = ev.pid;
|
||||||
|
auto procName = readProcComm(pid);
|
||||||
|
auto ids = readProcIds(pid); // <-- ruid + loginuid
|
||||||
|
|
||||||
|
DevIno key;
|
||||||
|
if (!getDevIno(ev.eventFd, key, pid))
|
||||||
|
{
|
||||||
|
ev.respond(FAN_ALLOW);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
auto path = readlinkFdPath(ev.eventFd);
|
||||||
// 2) Ставим метки
|
|
||||||
// ---------------------------
|
|
||||||
|
|
||||||
// Что именно хотим ловить
|
if (auto ps = key in gMap)
|
||||||
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 epEvents = ep.wait();
|
|
||||||
|
|
||||||
foreach (epEv; epEvents)
|
|
||||||
{
|
|
||||||
auto tag = epEv.tag;
|
|
||||||
|
|
||||||
// Считываем пачку событий с соответствующего fd
|
|
||||||
FanotifyEvent[] events;
|
|
||||||
if (tag == TAG_PRE)
|
|
||||||
{
|
|
||||||
events = pre.readEvents(64 * 1024);
|
|
||||||
}
|
|
||||||
else if (tag == TAG_NOTIF)
|
|
||||||
{
|
|
||||||
events = notif.readEvents(64 * 1024);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
gMap[key] = Session(procName, ids.ruid, ids.uid, path, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(pid in gPidfdByPid))
|
||||||
|
{
|
||||||
|
int pfd = -1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pfd = pidfd_open(pid, 0); // на твоей системе так ок
|
||||||
|
if (pfd < 0)
|
||||||
|
writeln("pidfd_open failed: ", strerror(errno).fromStringz);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
pfd = -1;
|
||||||
|
}
|
||||||
|
if (pfd >= 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ep.add(pfd, cast(uint) pid, EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR);
|
||||||
|
gPidfdByPid[pid] = pfd;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.respond(FAN_ALLOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCloseWrite(FanotifyEvent ev)
|
||||||
|
{
|
||||||
|
DevIno key;
|
||||||
|
if (!getDevIno(ev.eventFd, key, ev.pid))
|
||||||
|
return;
|
||||||
|
if (auto ps = key in gMap)
|
||||||
|
ps.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushAndClearForPid(int pid, Epoll ep)
|
||||||
|
{
|
||||||
|
auto keys = gMap.keys.dup;
|
||||||
|
foreach (k; keys)
|
||||||
|
{
|
||||||
|
if (k.pid != pid)
|
||||||
|
continue;
|
||||||
|
auto s = gMap[k];
|
||||||
|
if (s.changed)
|
||||||
|
logChange(s.procName, s.filePathOpen, s.uid, s.ruid);
|
||||||
|
gMap.remove(k);
|
||||||
|
}
|
||||||
|
if (auto p = pid in gPidfdByPid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ep.remove(*p);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
close(*p);
|
||||||
|
gPidfdByPid.remove(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---- main ----
|
||||||
|
enum uint FAN_TAG = 0;
|
||||||
|
|
||||||
|
void main(string[] args)
|
||||||
|
{
|
||||||
|
string watchDir;
|
||||||
|
|
||||||
|
auto help = getopt(args, "dir|d", &watchDir);
|
||||||
|
if (help.helpWanted || watchDir.length == 0)
|
||||||
|
{
|
||||||
|
defaultGetoptPrinter("Usage: app -d <dir-to-watch>", help.options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)");
|
||||||
|
|
||||||
|
enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK;
|
||||||
|
auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
stderr.writefln("fanotify_mark failed: %s", e.msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ep = new Epoll(EPOLL_CLOEXEC);
|
||||||
|
ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP);
|
||||||
|
|
||||||
|
writeln("watching dir: ", watchDir);
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
auto evs = ep.wait(256, -1);
|
||||||
|
if (evs.length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (e; evs)
|
||||||
|
{
|
||||||
|
if (e.tag == FAN_TAG)
|
||||||
|
{
|
||||||
|
auto list = fan.readEvents(8_192);
|
||||||
|
foreach (fev; list)
|
||||||
|
{
|
||||||
|
if (fev.isOverflow)
|
||||||
|
{
|
||||||
|
writeln(
|
||||||
|
"FAN_Q_OVERFLOW — возможна потеря корреляции");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm)
|
||||||
foreach (ev; events)
|
|
||||||
{
|
{
|
||||||
auto pname = eventProcessName(ev.pid);
|
onOpenPerm(fev, ep);
|
||||||
// writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask));
|
continue;
|
||||||
writefln("[%s] pid = %s proc = %s mask = %s name = %s",
|
|
||||||
(tag == TAG_PRE) ? "PRE" : "NOTIF",
|
|
||||||
ev.pid, pname,
|
|
||||||
maskToStr(ev.mask),
|
|
||||||
ev.name.length ? ev.name : "<неизвестно>"
|
|
||||||
);
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
if (fev.isCloseWrite)
|
||||||
// Лог
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
// Реакции в реальном времени
|
onCloseWrite(fev);
|
||||||
if (ev.isCloseWrite)
|
continue;
|
||||||
{
|
|
||||||
// Напр., дернуть диф/бэкап/индексацию
|
|
||||||
// snag --config ... create --comment "CLOSE_WRITE - <name>" ...
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Кастомный postProcess если надо
|
else
|
||||||
ev.postProcess();
|
{
|
||||||
|
auto pid = cast(int) e.tag;
|
||||||
|
flushAndClearForPid(pid, ep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ module depoll;
|
||||||
|
|
||||||
import core.sys.linux.epoll;
|
import core.sys.linux.epoll;
|
||||||
import core.sys.posix.unistd : close;
|
import core.sys.posix.unistd : close;
|
||||||
import core.stdc.errno : errno, EINTR;
|
import core.stdc.errno : errno, EINTR, EEXIST;
|
||||||
import core.stdc.string : strerror;
|
import core.stdc.string : strerror;
|
||||||
import std.string : fromStringz;
|
import std.string : fromStringz;
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
|
@ -26,39 +26,52 @@ class Epoll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Добавление дескриптора с произвольным tag (например, tag=0 для fanotify, tag=pid для pidfd)
|
||||||
void add(int fd, uint tag, uint events = EPOLLIN | EPOLLERR | EPOLLHUP)
|
void add(int fd, uint tag, uint events = EPOLLIN | EPOLLERR | EPOLLHUP)
|
||||||
{
|
{
|
||||||
epoll_event ev;
|
epoll_event ev;
|
||||||
ev.events = events;
|
ev.events = events;
|
||||||
ev.data.u32 = tag;
|
ev.data.u32 = tag;
|
||||||
enforce(epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev) == 0,
|
auto rc = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
|
||||||
"epoll_ctl ADD: " ~ strerror(errno).fromStringz.idup);
|
if (rc != 0 && errno == EEXIST)
|
||||||
|
{
|
||||||
|
// Если уже добавлен — делаем MOD
|
||||||
|
rc = epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev);
|
||||||
|
}
|
||||||
|
enforce(rc == 0, "epoll_ctl ADD/MOD: " ~ strerror(errno).fromStringz.idup);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Опционально: метод для удаления fd
|
/// Опционально: явное удаление
|
||||||
void remove(int fd)
|
void remove(int fd)
|
||||||
{
|
{
|
||||||
enforce(epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null) == 0,
|
auto rc = epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null);
|
||||||
"epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup);
|
enforce(rc == 0, "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Event
|
struct Event
|
||||||
{
|
{
|
||||||
uint tag;
|
uint tag; // то самое, что передавали в add()
|
||||||
uint events;
|
uint events; // EPOLL* биты
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Возвращает события; пустой массив — если таймаут/прерывание
|
||||||
Event[] wait(int maxevents = 16, int timeout = -1)
|
Event[] wait(int maxevents = 16, int timeout = -1)
|
||||||
{
|
{
|
||||||
epoll_event[] evs = new epoll_event[maxevents];
|
epoll_event[] evs = new epoll_event[maxevents];
|
||||||
int n = epoll_wait(epfd_, evs.ptr, maxevents, timeout);
|
int n;
|
||||||
if (n <= 0)
|
// Терпим EINTR и повторяем
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
// Игнорируем EINTR и другие (как в оригинале: continue)
|
n = epoll_wait(epfd_, evs.ptr, maxevents, timeout);
|
||||||
return [];
|
if (n < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
if (n <= 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
Event[] res;
|
Event[] res;
|
||||||
|
res.reserve(n);
|
||||||
foreach (i; 0 .. n)
|
foreach (i; 0 .. n)
|
||||||
{
|
{
|
||||||
res ~= Event(evs[i].data.u32, evs[i].events);
|
res ~= Event(evs[i].data.u32, evs[i].events);
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
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<ruid>\t<euid>\t<suid>\t<fsuid>"
|
|
||||||
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;
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue