Перехват и обработка событий в файловой системе Linux

    Введение

    В предыдущей статье мы рассмотрели сборку и установку пакета на Linux системах, в которой упомянули про Linux Kernel Module (LKM) и обещали раскрыть позднее подробности о пути к нему и его создании. Ну что ж, настало его время. LKM – мы выбираем тебя.

    Необходимость реализации

    "Windows драйвер мы заменили на Linux Kernel Module LKM…" итак, вернёмся мысленно к самому началу пути. Мы имеем Windows драйвер, который обеспечивает отслеживание и перехват событий обращения к файлу. Как его перенести или чем заменить в Linux системах? Покопавшись в архитектуре, почитав про перехват и реализацию подобных технологий в Linux – мы поняли, что задача абсолютно нетривиальная, содержащая кучу подводных камней.

    Inotify

    Закинув удочки на пару форумов, посоветовавшись с коллегами, было принято решение «копать» в сторону Inotify. Inotify – файловый монитор, который логирует события в системе уже после того, как они произошли. Но у него есть «брат» – fanotify. В нём мы можем добавить ограничение доступности на события открытия, копирования файла. Но нам необходимо иметь такую же возможность и для событий удаления, переименования, перемещения, а, следовательно, fanotify нам в этом не поможет. Хочу заметить, что fanotify – это userspace утилита, соответственно при её использовании нет проблем с платформопереносимостью.

    Virtual File System

    Следующим этапом изучения стала возможность реализации перехвата обращений при помощи VFS.

    После анализа VFS на основе Dtrace, eBPF и bcc, стало понятно, что при использовании данной технологии возможно выполнять мониторинг событий, происходящих в системе. В данном случае, перехват осуществляется через LKM. В рамках изучения реализации различных модулей под разные ядра выявлено следующее:

    • перехват не всегда позволяет отследить полный путь к файлу;

    • при перехвате обращения к файлу через открытое приложение, а не из проводника, отсутствует путь к файлу в аргументах;

    • для каждого ядра необходима своя реализация.

    Janus, SElinux и AppArmor

    В ходе исследования, была найдена статья по расширению функциональности системы безопасности ядра Linux. Отсюда следует, что на рынке существует достаточное количество решений. Самым легко реализуемым является Janus. Минусом решения выступает отсутствие поддержки свежих ядер и все вышеописанные проблемы LKM хука. Реализация SELinux и AppArmor представляет квинтэссенцию всего описанного и изученного ранее. Модуль SELinux включает в себя основные компоненты:

    • сервер безопасности;
    • кэш вектора доступа (англ. Access Vector Cache, AVC);
    • таблицы сетевых интерфейсов;
    • код сигнала сетевого уведомления;
    • свою виртуальную файловую систему (selinuxfs) и реализацию функций-перехватчиков.

    Долгожданное решение

    После всех этих бесконечных «но», на помощь нам пришёл Хабр! Наткнувшись на статью, стало ясно, что это наш случай.

    Обработка перехвата

    Изучив предложенные данные по ftrace и реализации из самой статьи, сделали аналогичный LKM модуль на базе ftrace. Данная утилита, в свою очередь, работает на базе файловой системы debugfs, которая в большинстве современных дистрибутивов Linux смонтирована по умолчанию. Hook'и добавили на события к уже имеющимся clone и open:

    • openat,
    • rename,
    • unlink,
    • unlinkat.

    Таким образом, удалось обработать открытие, переименование, перемещение, копирование, удаление файла.

    Взаимодействие

    Теперь нам нужно реализовать связь между модулем ядра и приложением userspace. Для решения данной задачи существуют разные подходы, но в основном выделяют два:

    • socket между kernel и userspace;
    • запись/чтение в системной директории в файл.

    В итоге, мы выбрали netlink socket, так как в Windows мы используем аналогичный интерфейс - FltSendMessage. Можно было использовать inet socket, но это наименее защищённое решение. Также столкнулись с такой проблемой, что на .Net Core, на которой реализовано userspace приложение, отсутствует реализация netlink.

    Поэтому пришлось реализовывать динамическую библиотеку с реализацией netlink и уже её подключать в проект.

    
    int open_netlink_connection(void)
    {
        //initialize our variables
        int sock;
        struct sockaddr_nl addr;
        int group = NETLINK_GROUP;
    
        //open a new socket connection
        sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);
    
        //if the socket failed to open,
        if (sock < 0) 
        {
            //inform the user
            printf("Socket failed to initialize.\n");
            //return the error value
            return sock;
        }
    
        //initialize our addr structure by filling it with zeros
        memset((void *) &addr, 0, sizeof(addr));
        //specify the protocol family
        addr.nl_family = AF_NETLINK;
        //set the process id to the current process id
        addr.nl_pid = getpid();
    
        //bind the address to the socket created, and if it failed,
        if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) 
        {
            //inform the user
            printf("bind < 0.\n");
            //return the function with a symbolic error code
            return -1;
        }
    
        //set the option so that we can receive packets whose destination
        //is the group address specified (so that we can receive the message broadcasted by the kernel)
        if (setsockopt(sock, 270, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) 
        {
            //if it failed, inform the user
            printf("setsockopt < 0\n");
            //return the function with a symbolic error code
            return -1;
        }
    
        //if we got thus far, then everything
        //went fine. Return our socket.
        return sock;
    }
    
    char* read_kernel_message(int sock)
    {
        //initialize the variables
        //that we are going to need
        struct sockaddr_nl nladdr;
        struct msghdr msg;
        struct iovec iov;
        char* buffer[CHUNK_SIZE];
        char* kernelMessage;
        int ret;
    
        memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));
        memset(&nladdr, 0, sizeof(nladdr));
        memset(&iov, 0, sizeof(iov));
        //specify the buffer to save the message
        iov.iov_base = (void *) &buffer;
        //specify the length of our buffer
        iov.iov_len = sizeof(buffer);
    
        //pass the pointer of our sockaddr structure
        //that will save the source IP and port of the connection
        msg.msg_name = (void *) &(dest_addr);
        //give the size of our structure
        msg.msg_namelen = sizeof(dest_addr);
        //pass our scatter/gather I/O structure pointer
        msg.msg_iov = &iov;
        //we will pass only one buffer array,
        //therefore we will specify that here
        msg.msg_iovlen = 1;
    
        //listen/wait for new data
        ret = recvmsg(sock, &msg, 0);
    
        //if message was received successfully,
        if(ret >= 0)
        {
            //get the string data and save them to a local variable
            char* buf = NLMSG_DATA((struct nlmsghdr *) &buffer);
    
            //allocate memory for our kernel message
            kernelMessage = (char*)malloc(CHUNK_SIZE);
    
            //copy the kernel data to our allocated space
            strcpy(kernelMessage, buf);
    
            //return the pointer that points to the kernel data
            return kernelMessage;
        }
        
        //if we got that far, reading the message failed,
        //so we inform the user and return a NULL pointer
        printf("Message could not received.\n");
        return NULL;
    }
    
    int send_kernel_message(int sock, char* kernelMessage)
    {
        //initialize the variables
        //that we are going to need
        struct msghdr msg;
        struct iovec iov;
        char* buffer[CHUNK_SIZE];    
        int ret;
    
        memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));
        memset(&iov, 0, sizeof(iov));
    
        nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
        memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
        nlh->nlmsg_pid = getpid();
        nlh->nlmsg_flags = 0;
    
        char buff[160];
        snprintf(buff, sizeof(buff), "From:DSSAgent;Action:return;Message:%s;", kernelMessage);
        strcpy(NLMSG_DATA(nlh), buff);
    
        iov.iov_base = (void *)nlh;
        iov.iov_len = nlh->nlmsg_len;
        //pass the pointer of our sockaddr structure
        //that will save the source IP and port of the connection
        msg.msg_name = (void *) &(dest_addr);
        //give the size of our structure
        msg.msg_namelen = sizeof(dest_addr);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        printf("Sending message to kernel (%s)\n",(char *)NLMSG_DATA(nlh));
        ret = sendmsg(sock, &msg, 0);
        return ret;
    }
    
    int sock_netlink_connection()
    {
    	sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
        if (sock_fd < 0)
            return -1;
    
    
        memset(&src_addr, 0, sizeof(src_addr));
        src_addr.nl_family = AF_NETLINK;
        src_addr.nl_pid = getpid(); /* self pid */
    
    
        bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));
    
    
        memset(&dest_addr, 0, sizeof(dest_addr));
        dest_addr.nl_family = AF_NETLINK;
        dest_addr.nl_pid = 0; /* For Linux Kernel */
        dest_addr.nl_groups = 0; /* unicast */
    
    
        nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
        memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
        nlh->nlmsg_pid = getpid();
        nlh->nlmsg_flags = 0;
    
    
        strcpy(NLMSG_DATA(nlh), "From:DSSAgent;Action:hello;");
    
    
        iov.iov_base = (void *)nlh;
        iov.iov_len = nlh->nlmsg_len;
        msg.msg_name = (void *)&dest_addr;
        msg.msg_namelen = sizeof(dest_addr);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
    
    
        printf("Sending message to kernel\n");
        sendmsg(sock_fd, &msg, 0);
        printf("Waiting for message from kernel\n");
    
    
        /* Read message from kernel */
        recvmsg(sock_fd, &msg, 0);
        printf("Received message payload: %s\n", (char *)NLMSG_DATA(nlh));
    	
    	return sock_fd;
    }
    void sock_netlink_disconnection(int sock)
    {
    	close(sock);
        free(nlh);
    }

    Также, в дальнейшем оказалось, что некоторые функции отсутствуют в Net.Core – например поиск по pid процесса имени пользователя, которому принадлежит этот процесс. Примеров данной реализации оказалась масса, но, в рамках нашего приложения, не удалось их реализовать. Поэтому реализовали в той же библиотеке свою функцию нахождения uid пользователя, по которому используя системные функции можно найти имя.

    char* get_username_by_pid(int pid)
    { 
      register struct passwd *pw;
      register uid_t uid;
      int c;
      FILE *fp;
      char filename[255];
      sprintf(filename, "/proc/%d/loginuid", pid);
      char cc[8];
      
      // чтение из файла
      if((fp= fopen(filename, "r"))==NULL)
        {
            perror("Error occured while opening file");
            return "";
        }
      // считываем, пока не дойдем до конца
      while((fgets(cc, 8, fp))!=NULL) {}
         
      fclose(fp);
      
      uid = atoi(cc);
    
      pw = getpwuid (uid);
      if (pw)
      {
          return pw->pw_name;
      }
      else
      {
          return "";
      }
    }

    Доработка модуля

    По итогу добавили соединение по netlink в инициализацию LKM.

    static int fh_init(void)
    {
        int err;
    	struct netlink_kernel_cfg cfg =
    	{
    #if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 6, 0)
    		.groups = 1,
    #endif
    		.input = nl_recv_msg,
    	};
    
    #if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 36)
    	nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
    #elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 32)
    	nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, 0, nl_recv_msg, NULL, THIS_MODULE);
    #else
    	nl_sk = netlink_kernel_create(NETLINK_USER, 0, nl_recv_msg, THIS_MODULE);
    #endif
    
    	if (!nl_sk)
    	{
    		printk(KERN_ERR "%s Could not create netlink socket\n", __func__);
    		return 1;
    	}
    
    	err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
    	if (err)
    		return err;
    
    	p_list_hook_files = (tNode *)kmalloc(sizeof(tNode), GFP_KERNEL);
    	p_list_hook_files->next = NULL;
    	p_list_hook_files->value = 0;
    
    	pr_info("module loaded\n");
    
    	return 0;
    }
    module_init(fh_init);
    
    static void fh_exit(void)
    {
    	delete_list(p_list_hook_files);
    	fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
    	netlink_kernel_release(nl_sk);
    	pr_info("module unloaded\n");
    }
    module_exit(fh_exit);

    Socket ожидает перехвата события обращения к файлу. Модуль, перехватывая событие, передаёт имя файла, pid и имя процесса. Userspace приложение, получая данную информацию, обрабатывает её и отвечает, что делать с файлом (блокировать или разрешать доступ). Впоследствии модуль возвращает соответствующий системный вызов.

    static void send_msg_to_user(const char *msgText)
    {
    	int msgLen = strlen(msgText);
    
    	struct sk_buff *skb = nlmsg_new(NLMSG_ALIGN(msgLen), GFP_KERNEL);
    
    	if (!skb)
    	{
    		printk(KERN_ERR "%s Allocation skb failure.\n", __func__);
    		return;
    	}
    
    	struct nlmsghdr *nlh = nlmsg_put(skb, 0, 1, NLMSG_DONE, msgLen, 0);
    
    	if (!nlh)
    	{
    		printk(KERN_ERR "%s Create nlh failure.\n", __func__);
    		nlmsg_free(skb);
    		return;
    	}
    
    	NETLINK_CB(skb).dst_group = 0;
    	strncpy(nlmsg_data(nlh), msgText, msgLen);
    
    	int errorVal = nlmsg_unicast(nl_sk, skb, pid);
    
    	if (errorVal < 0)
    		printk(KERN_ERR "%s nlmsg_unicast() error: %d\n", __func__, errorVal);
    }
    
    static void return_msg_to_user(struct nlmsghdr *nlh)
    {
    	pid = nlh->nlmsg_pid;
    
    	const char *msg = "Init socket from kernel";
    	const int msg_size = strlen(msg);
    
    	struct sk_buff *skb = nlmsg_new(msg_size, 0);
    	if (!skb)
    	{
    		printk(KERN_ERR "%s Failed to allocate new skb\n", __func__);
    		return;
    	}
    
    	nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, msg_size, 0);
    	NETLINK_CB(skb).dst_group = 0;
    	strncpy(nlmsg_data(nlh), msg, msg_size);
    
    	int res = nlmsg_unicast(nl_sk, skb, pid);
    	if (res < 0)
    		printk(KERN_ERR "%s Error while sending back to user (%i)\n", __func__, res);
    }

    Позднее, для работы другого приложения в данный модуль, добавили возможность блокирования доступа к определённому файлу (по его полному пути) для всех процессов, кроме определённого (определяем по pid процесса).

    static void parse_return_from_user(char *return_msg)
    {
    	char *msg = np_extract_value(return_msg, "Message", ';');
    	const char *file_name = strsep(&msg, "|");
    
    	printk(KERN_INFO "%s Name:(%s) Permiss:(%s)\n", __func__, file_name, msg);
    
    	if (strstr(msg, "Deny"))
    		reload_name_list(p_list_hook_files, file_name, Deny);
    	else
    		reload_name_list(p_list_hook_files, file_name, Allow);
    }
    
    static void free_guards(void)
    {
    	// Possibly unpredictable behavior during cleaning
    	memset(&guards, 0, sizeof(struct process_guards));
    }
    
    static void change_guards(char *msg)
    {
    	char *path = np_extract_value(msg, "Path", ';');
    	char *count_str = np_extract_value(msg, "Count", ';');
    
    	if (path && strlen(path) && count_str && strlen(count_str))
    	{
    		int i, found = -1;
    
    		for (i = 0; i < guards.count; ++i)
    			if (guards.process[i].file_path && !strcmp(path, guards.process[i].file_path))
    				found = i;
    
    		guards.is_busy = 1;
    
    		int count;
    		kstrtoint(count_str, 10, &count);
    
    		if (count > 0)
    		{
    			if (found == -1)
    			{
    				strcpy(guards.process[guards.count].file_path, path);
    				found = guards.count;
    				guards.count++;
    			}
    
    			for (i = 0; i < count; ++i)
    			{
    				char buff[8];
    				snprintf(buff, sizeof(buff), "Pid%d", i + 1);
    				char *pid = np_extract_value(msg, buff, ';');
    				if (pid && strlen(pid))
    					kstrtoint(pid, 10, &guards.process[found].allow_pids[i]);
    				else
    					guards.process[found].allow_pids[i] = 0;
    			}
    
    			guards.process[found].allow_pids[count] = 0;
    		}
    		else
    		{
    			if (found >= 0)
    			{
    				for (i = found; i < guards.count - 1; ++i)
    					guards.process[i] = guards.process[i + 1];
    
    				guards.count--;
    			}
    		}
    
    		guards.is_busy = 0;
    	}
    }
    
    // Example message is "From:CryptoCli;Action:clear;" or "From:DSSAgent;Action:init;"
    static void nl_recv_msg(struct sk_buff *skb)
    {
    	printk(KERN_INFO "%s <--\n", __func__);
    
    	struct nlmsghdr *nlh = (struct nlmsghdr *)skb->data;
    
    	printk(KERN_INFO "%s Netlink received msg payload:%s\n", __func__, (char *)nlmsg_data(nlh));
    
    	char *msg = (char *)nlmsg_data(nlh);
    
    	if (msg && strlen(msg))
    	{
    		char *from = np_extract_value(msg, "From", ';');
    		char *action = np_extract_value(msg, "Action", ';');
    
    		if (from && strlen(from) && action && strlen(action))
    		{
    			
    			if (!strcmp(from, "DSSAgent"))
    			{
    				if (!strcmp(action, "init"))
    				{
    					return_msg_to_user(nlh);
    				}
    				else if (!strcmp(action, "return"))
    				{
    					parse_return_from_user(msg);
    				}
    				else
    				{
    					printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);
    				}
    			}
    			else if (!strcmp(from, "CryptoCli"))
    			{
    				if (!strcmp(action, "clear"))
    				{
    					free_guards();
    				}
    				else if (!strcmp(action, "change"))
    				{
    					change_guards(msg);
    				}
    				else
    				{
    					printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);
    				}
    			}
    			else
    			{
    				printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);
    			}
    
    		}
    		else
    		{
    			printk(KERN_ERR "%s Failed parse msg, don`t found \"From\" and \"Action\" (%s)\n", __func__, msg);
    		}
    	}
    	else
    	{
    		printk(KERN_ERR "%s Failed parse struct nlmsg_data, msg is empty\n", __func__);
    	}
    
    	printk(KERN_INFO "%s -->\n", __func__);
    }
    
    static bool check_file_access(char *fname, int processPid)
    {
    	if (fname && strlen(fname))
    	{
    		int i;
    
    		for (i = 0; i < guards.count; ++i)
    		{
    			if (!strcmp(fname, guards.process[i].file_path) && guards.process[i].allow_pids[0] != 0)
    			{
    				int j;
    				
    				for (j = 0; guards.process[i].allow_pids[j] != 0; ++j)
    					if (processPid == guards.process[i].allow_pids[j])
    						return true;
    
    				return false;
    			}
    		}
    		
    		// Not found filename in guards
    		if (strstr(fname, filetype))
    		{
    			char *processName = current->comm;
    
    			printk(KERN_INFO "%s service pid = %d\n", __func__, pid);
    			printk(KERN_INFO "%s file name = %s, process pid: %d, , process name = %s\n", __func__, fname, processPid, processName);
    
    			if (processPid == pid)
    			{
    				return true;
    			}
    			else
    			{
    				add_list(p_list_hook_files, processPid, fname, None);
    
    				char *buffer = kmalloc(4096, GFP_KERNEL);
    				sprintf(buffer, "%s|%s|%d", fname, processName, processPid);
    				send_msg_to_user(buffer);
    				kfree(buffer);
    
    				ssleep(5);
    
    				bool ret = true;
    
    				if (find_list(p_list_hook_files, fname) == Deny)
    					ret = false;
    
    				delete_node(p_list_hook_files, fname);
    
    				return ret;
    			}
    		}
    	}
    
    	return true;
    }

    Интеграция в процесс установки

    Так как первые два минуса LKM удалось преодолеть через реализацию ftrace, третий никто не отменял. Мало того, что под каждое ядро нужна сборка модуля, уже в процессе использования он может «протухнуть». Было принято решение добавить его пересборку перед каждым запуском userspace приложения. В статье по сборке Linux пакетов было описано, что «службу», для которой мы реализовываем обработку перехвата обращения к файлу, мы «демонизировали» путём добавления в system. Поэтому для демона.service добавляем два дополнительных пункта, помимо ExecStart и ExecStop будут:

    ExecStartPre=/bin/sh /путь_до_расположения/prestart.sh
    ExecStopPost=/sbin/rmmod имя_модуля.ko

    а в сам prestart.sh:

    #!/bin/sh
    
    MOD_VAL=$(lsmod | grep имя_модуля | wc -l)
    
    cd /путь_до_расположения_модуля
    make clean
    make all
    
    if [ $MOD_VAL = 1 ]
    then
        for proc in $(ps aux | grep DSS.Agent | awk '{print $2}'); do kill -9 $proc; done
    else
        /sbin/insmod / путь_до_расположения_модуля/имя_модуля.ko
    fi

    Заключение

    В завершение, хочется отметить: возможно, путь, по которому мы пошли, не самый «красивый и элегантный», но, он содержит отработанную и проверенную логику работы на ОС Windows. Было бы полезно услышать в комментариях мнение читателей статьи. Возможно, есть более разумное решение задачи. Например, наш DevOps, в тот момент, когда мы автоматизировали сборку пакета Linux и обрабатывали/добавляли LKM, предложил реализовать логику с использованием Access Control List (ACL). Скорее всего, в дальнейшем мы займёмся переработкой нашего продукта под Linux. И, да, скоро будет новая статья, о том, как мы переносили MS Forms на Avalonia и его интеграции в Linux.

    Ссылки которые нам помогли

    Cross Technologies
    Системный интегратор и разработчик ПО

    Комментарии 8

      +1
      Мало того, что под каждое ядро нужна сборка модуля, уже в процессе использования он может «протухнуть». Было принято решение добавить его пересборку перед каждым запуском userspace приложения.

      Почему не штатный DKMS, который будет пересобирать модуль при обновлениях ядра?

        0
        Здравствуйте! Даже без обновления ядра, при изменении окружения и прочего, система говорила, что модуль не валиден.
          0

          Без технических подробностей мне нечего подсказать. Модули просто так не "протухают в процессе использования".


          Пересборка в сервисе — опасный костыль. Например, DKMS версионирует модули и при установке копирует исходники и бинарник модуля в папку конкретного ядра, чтобы при откате обновления получить то же ядро с теми же версиями модулей. Или у вас можно обновить ядро при выключенном сервисе и только потом обнаружить, что он не может собрать модуль. Каталог с исходниками придется защищать от изменений. Изменяемый бинарный код плохо сочетается с подписью, аудитом, сертификацией.

            0
            Здравствуйте! Спасибо за комментарий, действительно пересборка перед запуском демона не является штатным механизмом, но она защищает нас от большинства ошибок при работе. Предложенное решение является лишь одним из вариантов, позволивший решить задачу, и в нашем случае — достаточным.
        0

        Подскажите пожалуйста, почему не подошёл inotify? Пробежался ещё раз по статье и не смог найти.

          +1
          Здравствуйте! Inotify — отличный инструмент для мониторинга в ФС, но не подошел из-за того, что он логирует события уже после того, как они произошли. Для нашего же продукта необходима реакция в момент совершения события. Перехват можно обработать при помощи fanotify. Нам он не подошёл, из-за того, что отсутствовала возможность обработки событий удаления, переименования, перемещения.
            0
            Спасибо большое за ответ и статью! Сам недавно занимался созданием мониторинга файлов, и найти комплексно собранную информацию по этому вопросу как в вашей статье, мне не удалось.
              0
              Спасибо, за тёплые слова, всегда рад помочь.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое