Легковесный и полностью автономный регистратор сетевых соединений для 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;
}