434 lines
15 KiB
C
434 lines
15 KiB
C
#include "stats.h"
|
|
#include "nftables.h"
|
|
#include "log.h"
|
|
#include "geo.h"
|
|
#include <ctype.h>
|
|
|
|
void show_active_bans(void) {
|
|
msg(C_CYAN, "=== 🔥 活跃封禁列表 (最新 5 条) ===");
|
|
|
|
/* 解析并提取IP和过期时间 */
|
|
char command[MAX_COMMAND_LEN];
|
|
snprintf(command, sizeof(command),
|
|
"{ nft list set %s %s 2>/dev/null; nft list set %s %s 2>/dev/null; } | "
|
|
"grep -E 'expires [0-9]+(s|m|h|d|ms)' | "
|
|
"awk '{ip=\"\"; time=\"\"; for(i=1;i<=NF;i++) { if($i==\"expires\") time=$(i+1); else if(index($i,\".\")>0 || index($i,\":\")>0) ip=$i } if(ip && time) print ip\" \"time}' | "
|
|
"sort -t' ' -k2 | tail -n 5",
|
|
NFT_TABLE, NFT_SET, NFT_TABLE, NFT_SET_V6);
|
|
|
|
FILE *fp = popen(command, "r");
|
|
if (!fp) {
|
|
printf("(无法获取数据)\n\n");
|
|
return;
|
|
}
|
|
|
|
char line[MAX_LINE_LEN];
|
|
int count = 0;
|
|
|
|
printf("%s %-20s %-15s%s\n", C_YELLOW, "IP 地址", "剩余时间", C_RESET);
|
|
printf("-------------------------------------\n");
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
line[strcspn(line, "\n")] = 0;
|
|
if (strlen(line) > 0) {
|
|
char ip[MAX_IP_LEN] = {0};
|
|
char time_raw[64] = {0};
|
|
if (sscanf(line, "%s %s", ip, time_raw) == 2) {
|
|
// 解析nft时间格式:86394588ms, 23h59m54s等
|
|
long long total_s = 0;
|
|
char *p = time_raw;
|
|
long long num = 0;
|
|
|
|
while (*p) {
|
|
if (isdigit(*p)) {
|
|
num = num * 10 + (*p - '0');
|
|
} else {
|
|
if (*p == 'd') total_s += num * 86400;
|
|
else if (*p == 'h') total_s += num * 3600;
|
|
else if (*p == 'm' && *(p+1) == 's') { total_s += num / 1000; p++; }
|
|
else if (*p == 'm') total_s += num * 60;
|
|
else if (*p == 's') total_s += num;
|
|
num = 0;
|
|
}
|
|
p++;
|
|
}
|
|
|
|
long long h = total_s / 3600;
|
|
long long m = (total_s % 3600) / 60;
|
|
long long s = total_s % 60;
|
|
|
|
char time_str[64];
|
|
if (h > 0) {
|
|
snprintf(time_str, sizeof(time_str), "%lldh%lldm%llds", h, m, s);
|
|
} else if (m > 0) {
|
|
snprintf(time_str, sizeof(time_str), "%lldm%llds", m, s);
|
|
} else {
|
|
snprintf(time_str, sizeof(time_str), "%llds", s);
|
|
}
|
|
|
|
printf(" - %-20s %s\n", ip, time_str);
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
pclose(fp);
|
|
|
|
if (count == 0) {
|
|
printf("(目前没有被封禁的 IP)\n");
|
|
}
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
/* 检查段idx是否会被更精确的段取代(相同count但更小mask) */
|
|
static inline bool is_agg_replaced(void *agg_array, int agg_count, int idx) {
|
|
struct agg_entry { char subnet[64]; int count; int mask; };
|
|
struct agg_entry *agg = (struct agg_entry *)agg_array;
|
|
|
|
for (int j = 0; j < agg_count; ++j) {
|
|
if (agg[j].mask > agg[idx].mask && agg[j].count == agg[idx].count &&
|
|
strncmp(agg[j].subnet, agg[idx].subnet, strlen(agg[idx].subnet)) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void show_subnet_aggregation(void) {
|
|
msg(C_CYAN, "=== 📊 攻击源聚合统计 (IP 段归类) ===");
|
|
|
|
FILE *fp = fopen(PERSIST_FILE, "r");
|
|
if (!fp) {
|
|
printf("(无数据)\n\n");
|
|
return;
|
|
}
|
|
|
|
/* 统计各级网段 */
|
|
struct { char subnet[64]; int count; int mask; } agg[256];
|
|
int agg_count = 0;
|
|
int v6_count = 0;
|
|
char line[MAX_LINE_LEN];
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
line[strcspn(line, "\n")] = 0;
|
|
if (strlen(line) == 0) continue;
|
|
|
|
/* 提取IP部分 */
|
|
char *pipe = strchr(line, '|');
|
|
if (pipe) *pipe = '\0';
|
|
|
|
if (strchr(line, ':')) {
|
|
v6_count++;
|
|
continue;
|
|
}
|
|
|
|
/* 解析IPv4,统计/8, /16, /24 */
|
|
unsigned int a, b, c, d;
|
|
if (sscanf(line, "%u.%u.%u.%u", &a, &b, &c, &d) == 4) {
|
|
char subnet_24[64], subnet_16[64], subnet_8[64];
|
|
snprintf(subnet_24, sizeof(subnet_24), "%u.%u.%u", a, b, c);
|
|
snprintf(subnet_16, sizeof(subnet_16), "%u.%u", a, b);
|
|
snprintf(subnet_8, sizeof(subnet_8), "%u", a);
|
|
|
|
/* 统计/24 */
|
|
int found = 0;
|
|
for (int i = 0; i < agg_count; ++i) {
|
|
if (agg[i].mask == 24 && strcmp(agg[i].subnet, subnet_24) == 0) {
|
|
agg[i].count++;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found && agg_count < 256) {
|
|
snprintf(agg[agg_count].subnet, sizeof(agg[agg_count].subnet), "%s", subnet_24);
|
|
agg[agg_count].count = 1;
|
|
agg[agg_count].mask = 24;
|
|
agg_count++;
|
|
}
|
|
|
|
/* 统计/16 */
|
|
found = 0;
|
|
for (int i = 0; i < agg_count; ++i) {
|
|
if (agg[i].mask == 16 && strcmp(agg[i].subnet, subnet_16) == 0) {
|
|
agg[i].count++;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found && agg_count < 256) {
|
|
snprintf(agg[agg_count].subnet, sizeof(agg[agg_count].subnet), "%s", subnet_16);
|
|
agg[agg_count].count = 1;
|
|
agg[agg_count].mask = 16;
|
|
agg_count++;
|
|
}
|
|
|
|
/* 统计/8 */
|
|
found = 0;
|
|
for (int i = 0; i < agg_count; ++i) {
|
|
if (agg[i].mask == 8 && strcmp(agg[i].subnet, subnet_8) == 0) {
|
|
agg[i].count++;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found && agg_count < 256) {
|
|
snprintf(agg[agg_count].subnet, sizeof(agg[agg_count].subnet), "%s", subnet_8);
|
|
agg[agg_count].count = 1;
|
|
agg[agg_count].mask = 8;
|
|
agg_count++;
|
|
}
|
|
}
|
|
}
|
|
fclose(fp);
|
|
|
|
/* 第一步:按count降序排序 */
|
|
for (int i = 0; i < agg_count - 1; ++i) {
|
|
for (int j = i + 1; j < agg_count; ++j) {
|
|
if (agg[j].count > agg[i].count ||
|
|
(agg[j].count == agg[i].count && strcmp(agg[j].subnet, agg[i].subnet) < 0)) {
|
|
char tmp_subnet[64];
|
|
int tmp_count = agg[i].count, tmp_mask = agg[i].mask;
|
|
strcpy(tmp_subnet, agg[i].subnet);
|
|
agg[i].count = agg[j].count;
|
|
agg[i].mask = agg[j].mask;
|
|
strcpy(agg[i].subnet, agg[j].subnet);
|
|
agg[j].count = tmp_count;
|
|
agg[j].mask = tmp_mask;
|
|
strcpy(agg[j].subnet, tmp_subnet);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 第二步:将子网段移到父网段后面形成层级 */
|
|
for (int i = 0; i < agg_count; ++i) {
|
|
/* 查找i的所有直接子网段(下一级),移到i后面 */
|
|
int insert_pos = i + 1;
|
|
|
|
/* 先跳过已经在正确位置的子网 */
|
|
while (insert_pos < agg_count &&
|
|
agg[insert_pos].mask > agg[i].mask &&
|
|
strncmp(agg[insert_pos].subnet, agg[i].subnet, strlen(agg[i].subnet)) == 0) {
|
|
insert_pos++;
|
|
}
|
|
|
|
/* 从insert_pos后面查找其他子网 */
|
|
for (int j = insert_pos; j < agg_count; ++j) {
|
|
/* 检查j是否是i的子网(前缀完全匹配且mask更大) */
|
|
size_t prefix_len = strlen(agg[i].subnet);
|
|
if (agg[j].mask > agg[i].mask &&
|
|
strncmp(agg[j].subnet, agg[i].subnet, prefix_len) == 0 &&
|
|
(agg[j].subnet[prefix_len] == '.' || agg[j].subnet[prefix_len] == '\0')) {
|
|
/* j是i的子网,移动到insert_pos */
|
|
char tmp_subnet[64];
|
|
int tmp_count = agg[j].count, tmp_mask = agg[j].mask;
|
|
strcpy(tmp_subnet, agg[j].subnet);
|
|
|
|
/* 将insert_pos到j-1的元素向后移动 */
|
|
for (int k = j; k > insert_pos; --k) {
|
|
agg[k].count = agg[k-1].count;
|
|
agg[k].mask = agg[k-1].mask;
|
|
strcpy(agg[k].subnet, agg[k-1].subnet);
|
|
}
|
|
|
|
/* 插入到insert_pos */
|
|
agg[insert_pos].count = tmp_count;
|
|
agg[insert_pos].mask = tmp_mask;
|
|
strcpy(agg[insert_pos].subnet, tmp_subnet);
|
|
insert_pos++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 输出聚合结果,只显示count>=2的,并去重:如果大段和小段数量相同则只显示小段 */
|
|
bool has_output = false;
|
|
int show_count = 0;
|
|
int aggregated_count = 0;
|
|
|
|
for (int i = 0; i < agg_count && show_count < 10; ++i) {
|
|
if (agg[i].count < 2 || is_agg_replaced(agg, agg_count, i)) {
|
|
continue;
|
|
}
|
|
|
|
has_output = true;
|
|
|
|
/* 检查是否是子网(用于缩进显示和重复计数检测) */
|
|
bool is_child = false;
|
|
for (int k = 0; k < i; ++k) {
|
|
if (agg[k].count >= 2 && agg[k].mask < agg[i].mask &&
|
|
!is_agg_replaced(agg, agg_count, k)) {
|
|
size_t prefix_len = strlen(agg[k].subnet);
|
|
if (strncmp(agg[i].subnet, agg[k].subnet, prefix_len) == 0 &&
|
|
(agg[i].subnet[prefix_len] == '.' || agg[i].subnet[prefix_len] == '\0')) {
|
|
is_child = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 非子网段才计入aggregated_count(避免重复计数) */
|
|
if (!is_child) {
|
|
aggregated_count += agg[i].count;
|
|
}
|
|
|
|
/* 显示段信息,子网段增加缩进 */
|
|
char display_subnet[64];
|
|
const char *suffix = (agg[i].mask == 8) ? ".0.0.0/8" : (agg[i].mask == 16) ? ".0.0/16" : ".0/24";
|
|
snprintf(display_subnet, sizeof(display_subnet), "%s%s", agg[i].subnet, suffix);
|
|
|
|
if (is_child) {
|
|
printf(" └─ %-19s %s(%d 个)%s\n", display_subnet, C_RED, agg[i].count, C_RESET);
|
|
} else {
|
|
printf(" - %-22s %s(%d 个)%s\n", display_subnet, C_RED, agg[i].count, C_RESET);
|
|
}
|
|
show_count++;
|
|
}
|
|
|
|
/* 计算散乱IP数量 */
|
|
int total_ipv4 = 0;
|
|
FILE *fp_count = fopen(PERSIST_FILE, "r");
|
|
if (fp_count) {
|
|
char line_tmp[MAX_LINE_LEN];
|
|
while (fgets(line_tmp, sizeof(line_tmp), fp_count)) {
|
|
line_tmp[strcspn(line_tmp, "\n")] = 0;
|
|
if (strlen(line_tmp) > 0) {
|
|
char *pipe = strchr(line_tmp, '|');
|
|
if (pipe) *pipe = '\0';
|
|
if (!strchr(line_tmp, ':')) total_ipv4++;
|
|
}
|
|
}
|
|
fclose(fp_count);
|
|
}
|
|
int scattered_count = total_ipv4 - aggregated_count;
|
|
|
|
/* 显示散乱IP */
|
|
if (scattered_count > 0 || (!has_output && total_ipv4 > 0)) {
|
|
if (scattered_count > 0) {
|
|
printf(" - (散乱 IPv4) (%d 个)\n", scattered_count);
|
|
} else {
|
|
printf(" - (散乱 IPv4)\n");
|
|
}
|
|
}
|
|
|
|
if (v6_count > 0) {
|
|
printf(" - (IPv6 地址) (%d 个)\n", v6_count);
|
|
}
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
void show_country_stats(void) {
|
|
msg(C_CYAN, "=== 🌍 攻击源国家/地区统计 ===");
|
|
FILE *fp = fopen(PERSIST_FILE, "r");
|
|
if (!fp) {
|
|
printf("(暂无数据)\n\n");
|
|
return;
|
|
}
|
|
struct { char code[MAX_COUNTRY_CODE]; int count; } stats[128];
|
|
int stat_count = 0;
|
|
char line[MAX_LINE_LEN];
|
|
bool has_data = false;
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
line[strcspn(line, "\n")] = 0;
|
|
char *pipe = strchr(line, '|');
|
|
if (pipe && strlen(pipe + 1) > 0) {
|
|
has_data = true;
|
|
char *code = pipe + 1;
|
|
int found = 0;
|
|
for (int i = 0; i < stat_count; ++i) {
|
|
if (strcmp(stats[i].code, code) == 0) {
|
|
stats[i].count++;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found && stat_count < 128) {
|
|
strncpy(stats[stat_count].code, code, MAX_COUNTRY_CODE-1);
|
|
stats[stat_count].code[MAX_COUNTRY_CODE-1] = 0;
|
|
stats[stat_count].count = 1;
|
|
stat_count++;
|
|
}
|
|
}
|
|
}
|
|
fclose(fp);
|
|
if (!has_data) {
|
|
printf("(暂无国家信息)\n\n");
|
|
return;
|
|
}
|
|
// 排序
|
|
for (int i = 0; i < stat_count-1; ++i) {
|
|
for (int j = i+1; j < stat_count; ++j) {
|
|
if (stats[j].count > stats[i].count) {
|
|
char tmp_code[MAX_COUNTRY_CODE];
|
|
int tmp_count = stats[i].count;
|
|
strcpy(tmp_code, stats[i].code);
|
|
stats[i].count = stats[j].count;
|
|
strncpy(stats[i].code, stats[j].code, MAX_COUNTRY_CODE);
|
|
stats[j].count = tmp_count;
|
|
strncpy(stats[j].code, tmp_code, MAX_COUNTRY_CODE);
|
|
}
|
|
}
|
|
}
|
|
int show_n = stat_count < 9 ? stat_count : 9;
|
|
for (int i = 0; i < show_n; ++i) {
|
|
printf(" - %s %s(%d 个)%s\n", get_country_name(stats[i].code), C_RED, stats[i].count, C_RESET);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
void show_statistics(void) {
|
|
int nft_v4_count = nft_get_set_count(NFT_SET);
|
|
int nft_v6_count = nft_get_set_count(NFT_SET_V6);
|
|
int nft_count = nft_v4_count + nft_v6_count;
|
|
|
|
int local_count = 0;
|
|
FILE *fp = fopen(PERSIST_FILE, "r");
|
|
if (fp) {
|
|
char line[MAX_LINE_LEN];
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
if (strlen(line) > 1) local_count++;
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
msg(C_CYAN, "=== 🛡️ BIP 防护概览 ===");
|
|
printf("当前生效: %s%d%s 条 | 本地记录: %s%d%s 条\n\n",
|
|
C_GREEN, nft_count, C_RESET,
|
|
C_YELLOW, local_count, C_RESET);
|
|
|
|
show_active_bans();
|
|
show_subnet_aggregation();
|
|
show_country_stats();
|
|
|
|
msg(C_CYAN, "=== 📝 最新拦截日志 (Last 10) ===");
|
|
log_show_recent(10);
|
|
printf("\n");
|
|
}
|
|
|
|
void show_statistics_watch(bool watch_mode) {
|
|
if (!watch_mode) {
|
|
show_statistics();
|
|
return;
|
|
}
|
|
|
|
/* 动态监控模式 */
|
|
printf("\033[?25l"); /* 隐藏光标 */
|
|
|
|
while (1) {
|
|
printf("\033[2J\033[H"); /* 清屏并移到顶部 */
|
|
|
|
/* 显示时间戳 */
|
|
time_t now = time(NULL);
|
|
struct tm *t = localtime(&now);
|
|
printf("%s[实时监控] 刷新时间: %04d-%02d-%02d %02d:%02d:%02d (按 Ctrl+C 退出)%s\n\n",
|
|
C_YELLOW, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
|
t->tm_hour, t->tm_min, t->tm_sec, C_RESET);
|
|
|
|
show_statistics();
|
|
|
|
sleep(2); /* 每2秒刷新一次 */
|
|
}
|
|
|
|
printf("\033[?25h"); /* 恢复光标 */
|
|
}
|