/* * hp_net_monitor.c * Network anomaly detector: port scan, SYN flood. * * Uses a raw TCP socket (LWIP) to inspect incoming SYN packets. * Maintains a per-IP tracking table (max 32 entries) with sliding * window counters. Sends HP| events when thresholds are exceeded. * * Note: ARP monitoring requires LWIP netif hooks (layer 2) and is * not possible via raw sockets. May be added via etharp callback later. */ #include "sdkconfig.h" #ifdef CONFIG_MODULE_HONEYPOT #include #include #include #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "lwip/sockets.h" #include "utils.h" #include "event_format.h" #include "hp_config.h" #include "hp_net_monitor.h" #define TAG "HP_NET" #define NET_MON_STACK 4096 #define NET_MON_PRIO 4 #define NET_MON_CORE 1 /* Tracking table */ #define MAX_TRACKED_IPS 32 #define WINDOW_SEC 10 /* Sliding window for detections */ /* ============================================================ * IP tracker entry * ============================================================ */ typedef struct { uint32_t ip; /* Network byte order */ uint32_t first_seen; /* Tick count (ms) */ uint32_t last_seen; uint16_t unique_ports[32]; /* Ring buffer of destination ports */ uint8_t port_idx; uint8_t port_count; uint32_t syn_count; /* SYN packets in window */ bool portscan_alerted; bool synflood_alerted; } ip_tracker_t; /* ============================================================ * State * ============================================================ */ static atomic_bool net_running = false; static atomic_bool net_stop_req = false; static TaskHandle_t net_task = NULL; static SemaphoreHandle_t tracker_mutex = NULL; static ip_tracker_t trackers[MAX_TRACKED_IPS]; static int tracker_count = 0; static uint32_t total_port_scans = 0; static uint32_t total_syn_floods = 0; /* ============================================================ * Tracker helpers * ============================================================ */ static uint32_t now_ms(void) { return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); } static void ip_to_str(uint32_t ip_nbo, char *buf, size_t len) { uint8_t *b = (uint8_t *)&ip_nbo; snprintf(buf, len, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); } static ip_tracker_t *find_or_create_tracker(uint32_t ip) { uint32_t now = now_ms(); /* Search existing */ for (int i = 0; i < tracker_count; i++) { if (trackers[i].ip == ip) return &trackers[i]; } /* Evict oldest if full */ if (tracker_count >= MAX_TRACKED_IPS) { int oldest_idx = 0; uint32_t oldest_time = trackers[0].last_seen; for (int i = 1; i < tracker_count; i++) { if (trackers[i].last_seen < oldest_time) { oldest_time = trackers[i].last_seen; oldest_idx = i; } } if (oldest_idx < tracker_count - 1) trackers[oldest_idx] = trackers[tracker_count - 1]; tracker_count--; } ip_tracker_t *t = &trackers[tracker_count++]; memset(t, 0, sizeof(*t)); t->ip = ip; t->first_seen = now; t->last_seen = now; return t; } static void expire_trackers(void) { uint32_t now = now_ms(); uint32_t window = WINDOW_SEC * 1000; for (int i = 0; i < tracker_count; ) { if ((now - trackers[i].last_seen) > window * 3) { if (i < tracker_count - 1) trackers[i] = trackers[tracker_count - 1]; tracker_count--; } else { if ((now - trackers[i].first_seen) > window) { trackers[i].syn_count = 0; trackers[i].port_count = 0; trackers[i].port_idx = 0; trackers[i].first_seen = now; trackers[i].portscan_alerted = false; trackers[i].synflood_alerted = false; } i++; } } } static bool port_already_seen(ip_tracker_t *t, uint16_t port) { for (int i = 0; i < t->port_count && i < 32; i++) { if (t->unique_ports[i] == port) return true; } return false; } /* ============================================================ * Event recording * ============================================================ */ static void record_syn(uint32_t src_ip, uint16_t dst_port) { if (!tracker_mutex || xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) != pdTRUE) return; ip_tracker_t *t = find_or_create_tracker(src_ip); t->last_seen = now_ms(); t->syn_count++; /* Track unique ports for portscan detection */ if (!port_already_seen(t, dst_port)) { t->unique_ports[t->port_idx % 32] = dst_port; t->port_idx++; t->port_count++; } /* Snapshot values before releasing mutex */ uint8_t port_count = t->port_count; uint32_t syn_count = t->syn_count; bool ps_alerted = t->portscan_alerted; bool sf_alerted = t->synflood_alerted; int ps_thresh = hp_config_get_threshold("portscan"); if (port_count >= (uint8_t)ps_thresh && !ps_alerted) { t->portscan_alerted = true; total_port_scans++; } int sf_thresh = hp_config_get_threshold("synflood"); if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) { t->synflood_alerted = true; total_syn_floods++; } xSemaphoreGive(tracker_mutex); /* Send events outside mutex to avoid blocking */ if (port_count >= (uint8_t)ps_thresh && !ps_alerted) { char ip_str[16]; ip_to_str(src_ip, ip_str, sizeof(ip_str)); char detail[64]; snprintf(detail, sizeof(detail), "unique_ports=%d window=%ds", port_count, WINDOW_SEC); event_send("PORT_SCAN", "HIGH", "00:00:00:00:00:00", ip_str, 0, 0, detail, NULL); } if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) { char ip_str[16]; ip_to_str(src_ip, ip_str, sizeof(ip_str)); char detail[64]; snprintf(detail, sizeof(detail), "syn_count=%lu window=%ds", (unsigned long)syn_count, WINDOW_SEC); event_send("SYN_FLOOD", "CRITICAL", "00:00:00:00:00:00", ip_str, 0, 0, detail, NULL); } } /* ============================================================ * Raw socket listener task * ============================================================ */ static void net_monitor_task(void *arg) { (void)arg; int raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if (raw_fd < 0) { ESP_LOGE(TAG, "raw socket failed: %d", errno); goto done; } struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; setsockopt(raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); ESP_LOGI(TAG, "Network monitor started"); net_running = true; uint8_t pkt_buf[128]; while (!net_stop_req) { struct sockaddr_in src_addr; socklen_t addr_len = sizeof(src_addr); int n = recvfrom(raw_fd, pkt_buf, sizeof(pkt_buf), 0, (struct sockaddr *)&src_addr, &addr_len); if (n > 0) { uint8_t ihl = (pkt_buf[0] & 0x0F) * 4; if (ihl < 20 || n < ihl + 20) goto next; uint8_t *tcp = pkt_buf + ihl; uint16_t dst_port = (tcp[2] << 8) | tcp[3]; uint8_t flags = tcp[13]; /* SYN set, ACK not set → connection initiation */ if ((flags & 0x02) && !(flags & 0x10)) { record_syn(src_addr.sin_addr.s_addr, dst_port); } } next: if (tracker_mutex && xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { expire_trackers(); xSemaphoreGive(tracker_mutex); } } close(raw_fd); done: net_running = false; net_stop_req = false; ESP_LOGI(TAG, "Network monitor stopped"); net_task = NULL; vTaskDelete(NULL); } /* ============================================================ * Public API * ============================================================ */ void hp_net_monitor_start(void) { if (net_running || net_task) { ESP_LOGW(TAG, "Network monitor already running"); return; } if (!tracker_mutex) tracker_mutex = xSemaphoreCreateMutex(); tracker_count = 0; total_port_scans = total_syn_floods = 0; memset(trackers, 0, sizeof(trackers)); net_stop_req = false; BaseType_t ret = xTaskCreatePinnedToCore(net_monitor_task, "hp_net", NET_MON_STACK, NULL, NET_MON_PRIO, &net_task, NET_MON_CORE); if (ret != pdPASS) { ESP_LOGE(TAG, "Failed to create net monitor task"); net_task = NULL; } } void hp_net_monitor_stop(void) { if (!net_running && !net_task) { ESP_LOGW(TAG, "Network monitor not running"); return; } net_stop_req = true; ESP_LOGI(TAG, "Network monitor stop requested"); } bool hp_net_monitor_running(void) { return net_running; } int hp_net_monitor_status(char *buf, size_t len) { int count = 0; if (tracker_mutex && xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { count = tracker_count; xSemaphoreGive(tracker_mutex); } return snprintf(buf, len, "running=%s tracked_ips=%d port_scans=%lu syn_floods=%lu", net_running ? "yes" : "no", count, (unsigned long)total_port_scans, (unsigned long)total_syn_floods); } #endif /* CONFIG_MODULE_HONEYPOT */