Легковесный и полностью автономный регистратор сетевых соединений для Linux, написанный на GNU C.

Программа перехватывает события таблицы сопряжений ядра Linux (conntrack) через низкоуровневый интерфейс Netlink Sockets (NETLINK_NETFILTER), благодаря чему фиксирует создание (NEW) и закрытие (DESTROY) TCP, UDP и ICMP соединений с минимальной нагрузкой на CPU и память.

Особенности

  • Нулевые внешние зависимости: Код использует только стандартные системные заголовочные файлы Linux. Не требует установки libnetfilter_conntrack, libmnl или libnl.
  • Полная портабельность (Static Binary): Легко собирается в один исполняемый ELF-файл без динамических библиотек (statically linked).
  • Определение направления трафика: Автоматически считывает IP-адреса локальных интерфейсов системы и классифицирует соединения:
  • [OUT ] — исходящие соединения (==>)
  • [IN ] — входящие соединения (<==)
  • [LOOP] — локальные соединения внутри loopback (<=>)
  • [FWD ] — транзитный/широковещательный трафик (-->)
  • Поддержка протоколов: Детальный разбор TCP, UDP (с портами) и ICMP (с типами/кодами пакетов, включая ping).
  • Дублирование логов: Одновременный вывод в стандартный поток (stdout) и атомарная запись в файл network_connections.log.

---

Требования к системе

  • ОС: Linux (ядро 2.6.14+ с включенным модулем nf_conntrack).
  • Привилегии: Для открытия raw-сокетов Netlink требуются права root или права CAP_NET_ADMIN.

---

Сборка

1. Стандартная статическая сборка (glibc)

Для сборки статически сцепленного бинарника выполните:

gcc -Wall -Wextra -O2 -static conn_logger.c -o conn_logger

> Примечание: Если компилятор выдает ошибку о falta libc.a, установите пакет статической стандартной библиотеки:
> * Debian / Ubuntu / Kali: apt install libc6-dev
> * RHEL / CentOS / Fedora: dnf install glibc-static
> * Arch Linux / Manjaro: входит в состав base-devel
>
>

2. Максимально портабельная сборка (musl-libc)

Для запуска на любых дистрибутивах (включая Alpine Linux, минималистичные контейнеры или старые ядры) рекомендуется использовать musl:

musl-gcc -O2 -static conn_logger.c -o conn_logger

Проверить статическую линковку получившегося файла можно командой:

file conn_logger
# Вывод: conn_logger: ELF 64-bit LSB executable, ..., statically linked

---

Использование

Запустите скомпилированный бинарный файл с правами root:

sudo ./conn_logger

Завершение работы выполняется корректно по сигналам SIGINT (Ctrl+C) или SIGTERM.

---

Формат лога

Записи сохраняются в файл network_connections.log в следующем формате:

[2026-09-21 12:35:10] [NEW]     [OUT ] UDP  192.168.174.38:52513 ==> 192.168.174.1:53
[2026-09-21 12:35:11] [NEW]     [OUT ] ICMP 192.168.174.38 ==> 194.164.200.164 (type:8, code:0)
[2026-09-21 12:35:12] [NEW]     [IN  ] TCP  192.168.174.38:80 <== 185.220.101.5:41230
[2026-09-21 12:35:15] [NEW]     [LOOP] TCP  127.0.0.1:39396 <=> 127.0.0.1:22
[2026-09-21 12:35:20] [NEW]     [FWD ] UDP  192.168.88.11:5353 --> 224.0.0.251:5353
[2026-09-21 12:35:40] [DESTROY] [OUT ] ICMP 192.168.174.38 ==> 194.164.200.164 (type:8, code:0)

Поля записи:

1. [YYYY-MM-DD HH:MM:SS] — метка времени события.
2. [NEW / DESTROY] — открытие нового соединения или закрытие/истечение таймаута в сессии.
3. [OUT / IN / LOOP / FWD] — направление относительно локального хоста.
4. Протокол — TCP, UDP или ICMP.
5. Сокеты и разделители:

  • ==> — исходящий пакет
  • <== — входящий пакет
  • <=> — loopback трафик
  • --> — транзитный / широковещательный трафик
# cat conn_logger.c 
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <errno.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_conntrack.h>

#define LOG_FILE "network_connections.log"
#define BUFFER_SIZE 16384
#define MAX_LOCAL_IPS 32

/* Макросы Netlink Attributes для автономной сборки без внешних библиотек */
#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + 3) & ~3)
#endif

#ifndef NLA_OK
#define NLA_OK(nla, len) ((len) >= (int)sizeof(struct nlattr) && \
                          (nla)->nla_len >= sizeof(struct nlattr) && \
                          (nla)->nla_len <= (len))
#endif

#ifndef NLA_NEXT
#define NLA_NEXT(nla, len) ((len) -= NLA_ALIGN((nla)->nla_len), \
                            (struct nlattr *)((char *)(nla) + NLA_ALIGN((nla)->nla_len)))
#endif

#ifndef NLA_DATA
#define NLA_DATA(nla) ((void *)((char *)(nla) + NLA_ALIGN(sizeof(struct nlattr))))
#endif

#ifndef NLA_PAYLOAD
#define NLA_PAYLOAD(nla) ((int)((nla)->nla_len - NLA_ALIGN(sizeof(struct nlattr))))
#endif

static volatile int keep_running = 1;
static char local_ips[MAX_LOCAL_IPS][INET_ADDRSTRLEN];
static int local_ip_count = 0;

void handle_signal(int sig) {
    (void)sig;
    keep_running = 0;
}

// Загрузка всех IPv4-адресов локальных интерфейсов
void load_local_ips(void) {
    struct ifaddrs *ifaddr, *ifa;
    local_ip_count = 0;

    if (getifaddrs(&ifaddr) == -1) {
        perror("getifaddrs");
        return;
    }

    for (ifa = ifaddr; ifa != NULL && local_ip_count < MAX_LOCAL_IPS; ifa = ifa->ifa_next) {
        if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET)
            continue;

        struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr;
        inet_ntop(AF_INET, &(sa->sin_addr), local_ips[local_ip_count], INET_ADDRSTRLEN);
        local_ip_count++;
    }

    freeifaddrs(ifaddr);
}

// Проверка, является ли IP-адрес локальным для этого хоста
int is_local_ip(const char *ip) {
    for (int i = 0; i < local_ip_count; i++) {
        if (strcmp(ip, local_ips[i]) == 0) {
            return 1;
        }
    }
    return 0;
}

// Форматированная метка времени
void get_timestamp(char *buf, size_t len) {
    time_t now = time(NULL);
    struct tm tm_info;
    localtime_r(&now, &tm_info);
    strftime(buf, len, "%Y-%m-%d %H:%M:%S", &tm_info);
}

// Поиск inode сокета в /proc/net/{tcp,udp} по локальному порту
static unsigned long get_socket_inode(uint8_t proto, uint16_t local_port) {
    const char *net_file = (proto == IPPROTO_TCP) ? "/proc/net/tcp" :
                           (proto == IPPROTO_UDP) ? "/proc/net/udp" : NULL;
    if (!net_file) return 0;

    FILE *f = fopen(net_file, "r");
    if (!f) return 0;

    char line[512];
    // Пропускаем заголовок
    if (!fgets(line, sizeof(line), f)) {
        fclose(f);
        return 0;
    }

    unsigned long inode = 0;
    while (fgets(line, sizeof(line), f)) {
        unsigned int lport;
        unsigned long ino;
        
        if (sscanf(line, "%*d: %*X:%X %*X:%*X %*X %*X:%*X %*X:%*X %*X %*d %*d %lu", &lport, &ino) == 2) {
            if (lport == local_port) {
                inode = ino;
                break;
            }
        }
    }

    fclose(f);
    return inode;
}

// Поиск имени процесса и PID по inode сокета через /proc
static void get_process_info_by_inode(unsigned long inode, pid_t *out_pid, char *out_comm, size_t comm_len) {
    *out_pid = -1;
    snprintf(out_comm, comm_len, "unknown");

    if (inode == 0) return;

    DIR *proc_dir = opendir("/proc");
    if (!proc_dir) return;

    char target_fd[64];
    snprintf(target_fd, sizeof(target_fd), "socket:[%lu]", inode);

    struct dirent *entry;
    while ((entry = readdir(proc_dir)) != NULL) {
        if (!isdigit((unsigned char)entry->d_name[0])) continue;

        pid_t pid = atoi(entry->d_name);
        
        // Буфер link_path выделяется с запасом для исключения предупреждений truncation
        char link_path[PATH_MAX + 256];
        int base_len = snprintf(link_path, sizeof(link_path), "/proc/%d/fd/", pid);
        if (base_len < 0 || (size_t)base_len >= sizeof(link_path)) continue;

        DIR *fd_dir = opendir(link_path);
        if (!fd_dir) continue;

        struct dirent *fd_entry;
        int found = 0;
        while ((fd_entry = readdir(fd_dir)) != NULL) {
            if (fd_entry->d_name[0] == '.') continue;

            // Безопасно дополняем имя файла файлового дескриптора в конец пути
            snprintf(link_path + base_len, sizeof(link_path) - base_len, "%s", fd_entry->d_name);

            char target_path[PATH_MAX];
            ssize_t len = readlink(link_path, target_path, sizeof(target_path) - 1);
            if (len != -1) {
                target_path[len] = '\0';
                if (strcmp(target_path, target_fd) == 0) {
                    found = 1;
                    *out_pid = pid;
                    
                    char comm_path[PATH_MAX];
                    snprintf(comm_path, sizeof(comm_path), "/proc/%d/comm", pid);
                    FILE *fcomm = fopen(comm_path, "r");
                    if (fcomm) {
                        if (fgets(out_comm, comm_len, fcomm)) {
                            out_comm[strcspn(out_comm, "\r\n")] = 0;
                        }
                        fclose(fcomm);
                    }
                    break;
                }
            }
        }
        closedir(fd_dir);

        if (found) break;
    }

    closedir(proc_dir);
}

struct nlattr *find_nla(struct nlattr *head, int len, unsigned short type) {
    while (NLA_OK(head, len)) {
        if ((head->nla_type & NLA_TYPE_MASK) == type) {
            return head;
        }
        head = NLA_NEXT(head, len);
    }
    return NULL;
}

void parse_tuple(struct nlattr *tuple_attr, char *src_ip, char *dst_ip, 
                 uint16_t *src_port, uint16_t *dst_port, uint8_t *proto) {
    int len = NLA_PAYLOAD(tuple_attr);
    struct nlattr *head = (struct nlattr *)NLA_DATA(tuple_attr);

    struct nlattr *ip_attr = find_nla(head, len, CTA_TUPLE_IP);
    if (ip_attr) {
        int ip_len = NLA_PAYLOAD(ip_attr);
        struct nlattr *ip_head = (struct nlattr *)NLA_DATA(ip_attr);
        
        struct nlattr *saddr = find_nla(ip_head, ip_len, CTA_IP_V4_SRC);
        struct nlattr *daddr = find_nla(ip_head, ip_len, CTA_IP_V4_DST);

        if (saddr) inet_ntop(AF_INET, NLA_DATA(saddr), src_ip, INET_ADDRSTRLEN);
        if (daddr) inet_ntop(AF_INET, NLA_DATA(daddr), dst_ip, INET_ADDRSTRLEN);
    }

    struct nlattr *proto_attr = find_nla(head, len, CTA_TUPLE_PROTO);
    if (proto_attr) {
        int pr_len = NLA_PAYLOAD(proto_attr);
        struct nlattr *pr_head = (struct nlattr *)NLA_DATA(proto_attr);

        struct nlattr *num = find_nla(pr_head, pr_len, CTA_PROTO_NUM);
        if (num) *proto = *(uint8_t *)NLA_DATA(num);

        if (*proto == IPPROTO_TCP || *proto == IPPROTO_UDP) {
            struct nlattr *sport = find_nla(pr_head, pr_len, CTA_PROTO_SRC_PORT);
            struct nlattr *dport = find_nla(pr_head, pr_len, CTA_PROTO_DST_PORT);

            if (sport) *src_port = ntohs(*(uint16_t *)NLA_DATA(sport));
            if (dport) *dst_port = ntohs(*(uint16_t *)NLA_DATA(dport));
        } else if (*proto == IPPROTO_ICMP) {
            struct nlattr *type = find_nla(pr_head, pr_len, CTA_PROTO_ICMP_TYPE);
            struct nlattr *code = find_nla(pr_head, pr_len, CTA_PROTO_ICMP_CODE);

            if (type) *src_port = *(uint8_t *)NLA_DATA(type);
            if (code) *dst_port = *(uint8_t *)NLA_DATA(code);
        }
    }
}

void process_msg(struct nlmsghdr *nlh, FILE *log_fp) {
    uint8_t type = nlh->nlmsg_type & 0xFF;
    const char *action = NULL;

    if (type == IPCTNL_MSG_CT_NEW) {
        action = "NEW";
    } else if (type == IPCTNL_MSG_CT_DELETE) {
        action = "DESTROY";
    } else {
        return;
    }

    int len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct nfgenmsg));
    struct nlattr *head = (struct nlattr *)((char *)NLMSG_DATA(nlh) + sizeof(struct nfgenmsg));

    struct nlattr *tuple_orig = find_nla(head, len, CTA_TUPLE_ORIG);
    if (!tuple_orig) return;

    char src_ip[INET_ADDRSTRLEN] = "0.0.0.0";
    char dst_ip[INET_ADDRSTRLEN] = "0.0.0.0";
    uint16_t src_port = 0, dst_port = 0;
    uint8_t proto = 0;

    parse_tuple(tuple_orig, src_ip, dst_ip, &src_port, &dst_port, &proto);

    const char *proto_str = (proto == IPPROTO_TCP)  ? "TCP"  : 
                            (proto == IPPROTO_UDP)  ? "UDP"  : 
                            (proto == IPPROTO_ICMP) ? "ICMP" : "OTHER";

    int src_local = is_local_ip(src_ip);
    int dst_local = is_local_ip(dst_ip);

    const char *dir_tag = "FWD";
    const char *arrow   = "-->";

    if (src_local && dst_local) {
        dir_tag = "LOOP";
        arrow   = "<=>";
    } else if (src_local) {
        dir_tag = "OUT ";
        arrow   = "==>";
    } else if (dst_local) {
        dir_tag = "IN  ";
        arrow   = "<==";
    }

    char proc_info[64] = "";
    if ((proto == IPPROTO_TCP || proto == IPPROTO_UDP) && (src_local || dst_local)) {
        uint16_t local_port = src_local ? src_port : dst_port;
        unsigned long inode = get_socket_inode(proto, local_port);
        
        pid_t pid = -1;
        char comm[32] = "unknown";
        get_process_info_by_inode(inode, &pid, comm, sizeof(comm));

        if (pid != -1) {
            snprintf(proc_info, sizeof(proc_info), " [app:%s pid:%d]", comm, pid);
        } else {
            snprintf(proc_info, sizeof(proc_info), " [app:unknown]");
        }
    }

    char timestamp[32];
    get_timestamp(timestamp, sizeof(timestamp));

    char log_buf[512];

    if (proto == IPPROTO_ICMP) {
        snprintf(log_buf, sizeof(log_buf), 
                 "[%s] [%s] [%s] ICMP %s %s %s (type:%u, code:%u)",
                 timestamp, action, dir_tag, src_ip, arrow, dst_ip, src_port, dst_port);
    } else {
        snprintf(log_buf, sizeof(log_buf), 
                 "[%s] [%s] [%s] %-4s %s:%u %s %s:%u%s",
                 timestamp, action, dir_tag, proto_str, src_ip, src_port, arrow, dst_ip, dst_port, proc_info);
    }

    fprintf(log_fp, "%s\n", log_buf);
    fflush(log_fp);

    printf("%s\n", log_buf);
}

int main(void) {
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);

    load_local_ips();

    FILE *log_fp = fopen(LOG_FILE, "a");
    if (!log_fp) {
        perror("fopen log file");
        return EXIT_FAILURE;
    }

    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER);
    if (fd < 0) {
        perror("socket(NETLINK_NETFILTER)");
        fclose(log_fp);
        return EXIT_FAILURE;
    }

    struct sockaddr_nl sa;
    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;
    sa.nl_groups = NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY;

    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("bind");
        close(fd);
        fclose(log_fp);
        return EXIT_FAILURE;
    }

    int rcvbuf = 1024 * 1024;
    setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

    char *buffer = malloc(BUFFER_SIZE);
    if (!buffer) {
        perror("malloc");
        close(fd);
        fclose(log_fp);
        return EXIT_FAILURE;
    }

    printf("Connection logger started. Logging to '%s'...\n", LOG_FILE);

    while (keep_running) {
        ssize_t ret = recv(fd, buffer, BUFFER_SIZE, 0);
        if (ret < 0) {
            if (errno == EINTR) continue;
            perror("recv");
            break;
        }

        struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
        while (NLMSG_OK(nlh, ret)) {
            process_msg(nlh, log_fp);
            nlh = NLMSG_NEXT(nlh, ret);
        }
    }

    printf("\nShutting down logger...\n");
    free(buffer);
    close(fd);
    fclose(log_fp);

    return EXIT_SUCCESS;
}