?
/**
@file unix_socket_log.h
@brief UNIX socket log capturing
@details Copyright (c) 2025 Acronis International GmbH
@author Denis Kopyrin (denis.kopyrin@acronis.com)
@since $Id: $
*/
#include "unix_socket_log.h"
#include "memory.h"
#include "net_events.h"
#include "network/net_compat.h"
#include "string_view.h"
#include "transport.h"
#include <linux/string.h>
#include <linux/types.h>
#include <linux/un.h>
static const string_view_t k_dev_logs[] = { STRING_VIEW_CONST("/dev/log"), STRING_VIEW_CONST("/run/systemd/journal/dev-log") };
static const string_view_t k_keywords_failed[] = { STRING_VIEW_CONST("Connection ") // "reset" or ".... timed out" or " from ...: refusing"
, STRING_VIEW_CONST("Failed ")
, STRING_VIEW_CONST("Disconnecting ")
// 'Disconnecting' covers all the cases, 'Disconnected' is usually a logout message
// , STRING_VIEW_CONST("Disconnected ")
, STRING_VIEW_CONST("Unable to negotiate with ") };
static const string_view_t k_keywords_success[] = { STRING_VIEW_CONST("Accepted ") };
static bool is_dev_log(const string_view_t *sv)
{
int i;
for (i = 0; i < (int) ARRAY_SIZE(k_dev_logs); i++) {
if (string_view_equal(&k_dev_logs[i], sv))
return true;
}
return false;
}
static bool check_if_log_line_starts_with_keywords(const string_view_t* sv, const string_view_t* keywords, int keywords_amount)
{
int i;
for (i = 0; i < keywords_amount; i++) {
if (string_view_starts_with(sv, &keywords[i]))
return true;
}
return false;
}
static bool check_if_log_line_failed(const string_view_t* sv)
{
return check_if_log_line_starts_with_keywords(sv, k_keywords_failed, sizeof(k_keywords_failed) / sizeof(*k_keywords_failed));
}
static bool check_if_log_line_success(const string_view_t* sv)
{
return check_if_log_line_starts_with_keywords(sv, k_keywords_success, sizeof(k_keywords_success) / sizeof(*k_keywords_success));
}
static bool check_if_auth_log(string_view_t* sv)
{
if (sv->str[0] != '<')
return false;
if (sv->str[1] == '8') {
// Levels from 80 to 86 are non-debug authpriv logs
if ('0' > sv->str[2] || sv->str[2] > '6') {
return false;
}
} else if (sv->str[1] == '3') {
// Levels from 32 to 38 are non-debug auth logs
if ('2' > sv->str[2] || sv->str[2] > '8') {
return false;
}
} else {
return false;
}
if (sv->str[3] != '>')
return false;
string_view_advance(sv, 4);
return true;
}
// musl memmem
static const unsigned char *threebyte_memmem(const unsigned char *h, size_t k, const unsigned char *n)
{
uint32_t nw = (uint32_t)n[0]<<24 | n[1]<<16 | n[2]<<8;
uint32_t hw = (uint32_t)h[0]<<24 | h[1]<<16 | h[2]<<8;
for (h+=3, k-=3; k; k--, hw = (hw|*h++)<<8)
if (hw == nw) return h-3;
return hw == nw ? h-3 : NULL;
}
typedef struct
{
// check if 'cut_log.str' to check if message is useful
string_view_t cut_log;
bool is_success;
} log_analysis_result_t;
#define LOG_ANALYSIS_RESULT_OK(res) ((res).cut_log.str)
// Lines that arrive here look like "<36> Jan 20 19:09:58 sshd[44171]: Accepted password for user from 192.168.223.1 port 59064 ssh2"
// Our target is to extract line that looks like "Accepted password for user from 192.168.223.1 port 59064 ssh2"
static log_analysis_result_t analyze_log_msg(string_view_t log_msg)
{
log_analysis_result_t result;
char* log_start;
bool any_known = false;
string_view_init_empty(&result.cut_log);
result.is_success = false;
if (!check_if_auth_log(&log_msg))
return result;
string_view_advance(&log_msg, sizeof(" Jan 20 19:09:58 ") - 1);
// Find the "]: " pattern when actual event starts. We are guaranteed that such event pattern will exist:
// LogTag is expected to be equal to argv0 or __progname - not NULL. LOG_PID flag is enabled for sshd.
/*
if (LogStat & LOG_PID)
fprintf (f, "[%d]", (int) __getpid ());
if (LogTag != NULL) {
__putc_unlocked (':', f);
__putc_unlocked (' ', f);
}
*/
if (log_msg.len < 3)
return result;
log_start = (char*) threebyte_memmem((unsigned char*) log_msg.str, log_msg.len, (const unsigned char*) "]: ");
if (!log_start)
return result;
string_view_advance(&log_msg, log_start - log_msg.str + 3);
// Check if prefix matches well-known line prefixes
if (!any_known && check_if_log_line_failed(&log_msg)) {
any_known = true;
result.is_success = false;
}
if (!any_known && check_if_log_line_success(&log_msg)) {
any_known = true;
result.is_success = true;
}
if (any_known)
result.cut_log = log_msg;
return result;
}
#define MIN_LEN ((int) sizeof("<86>Jan 20 14:53:50 []: Failed "))
#define SNIFF_LEN 512
static void handle_log_msg(task_info_t *task_info, string_view_t log_msg)
{
transport_ids_t transport_ids;
int event;
uint64_t generatedEventsMask;
log_analysis_result_t result = analyze_log_msg(log_msg);
if (!LOG_ANALYSIS_RESULT_OK(result))
return;
event = result.is_success ? FP_SI_OT_NOTIFY_SOCKET_AUTH_LOG_SUCCESS : FP_SI_OT_NOTIFY_SOCKET_AUTH_LOG_FAILED;
generatedEventsMask = MSG_TYPE_TO_EVENT_MASK(event);
if (!(generatedEventsMask & transport_global_get_combined_mask()))
return;
transport_global_get_ids(&transport_ids, generatedEventsMask);
if (task_info_can_skip(task_info, &transport_ids, generatedEventsMask))
return;
net_event_auth_log(task_info, result.cut_log, result.is_success);
}
void unix_socket_log_sendmsg(task_info_t *task_info, struct socket *sock, struct msghdr *msg, int size)
{
struct sockaddr_storage storage;
int sniff_amount;
size_t copied_size;
string_view_t target_path;
char* log_msg;
if (size < MIN_LEN)
return;
string_view_init_empty(&target_path);
if (msg->msg_name) {
if (msg->msg_namelen > (int) (sizeof(sa_family_t) + 1)) {
struct sockaddr_un* sun = (struct sockaddr_un*) msg->msg_name;
string_view_init(&target_path, sun->sun_path, msg->msg_namelen - sizeof(sa_family_t));
}
} else {
int len = sock_to_addr(sock, &storage, 2 /*peer*/);
if (len > (int) (sizeof(sa_family_t) + 1)) {
struct sockaddr_un* sun = (struct sockaddr_un*) &storage;
string_view_init(&target_path, sun->sun_path, len - sizeof(sa_family_t));
}
}
if (!target_path.str)
return;
target_path.len = strnlen(target_path.str, target_path.len);
if (!target_path.len)
return;
if (!is_dev_log(&target_path))
return;
sniff_amount = size;
if (sniff_amount > SNIFF_LEN)
sniff_amount = SNIFF_LEN;
log_msg = (char*) mem_alloc(sniff_amount);
if (!log_msg)
return;
copied_size = msg_data_peek(msg, log_msg, sniff_amount);
if (copied_size > MIN_LEN) {
string_view_t log_msg_sv;
string_view_init(&log_msg_sv, log_msg, copied_size);
handle_log_msg(task_info, log_msg_sv);
}
mem_free(log_msg);
}