c
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
#include "ban.h"
|
||||
#if defined(__unix__) || defined(__linux__)
|
||||
#include <fcntl.h> // O_CREAT, O_RDWR
|
||||
#include <sys/file.h> // flock, LOCK_EX, LOCK_UN
|
||||
#include <signal.h> // SIGCHLD, SIG_IGN
|
||||
#else
|
||||
#define O_CREAT 0x0100
|
||||
#define O_RDWR 0x0002
|
||||
#define LOCK_EX 2
|
||||
#define LOCK_UN 8
|
||||
#define SIGCHLD 17
|
||||
#define SIG_IGN ((void (*)(int))1)
|
||||
#endif
|
||||
#include "nftables.h"
|
||||
#include "whitelist.h"
|
||||
#include "geo.h"
|
||||
#include "log.h"
|
||||
|
||||
|
||||
int ban_ip(const char *ip, bool save_to_disk) {
|
||||
if (!ip || !validate_ip_format(ip)) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 检查白名单 */
|
||||
if (is_in_whitelist(ip)) {
|
||||
log_write("[白名单保护] IP=%s 在白名单中,拒绝封禁", ip);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* 解析IP信息 */
|
||||
ip_info_t info;
|
||||
if (parse_ip_info(ip, &info) != SUCCESS) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 立即添加到nftables(关键操作,不能延迟) */
|
||||
if (nft_add_to_blacklist(&info) != SUCCESS) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
/* 先保存到磁盘(不查询国家) */
|
||||
if (save_to_disk) {
|
||||
persist_add_ip(ip, "");
|
||||
log_write("[执行封禁] IP=%s 已封禁", ip);
|
||||
}
|
||||
|
||||
/* 异步查询国家信息(耗时操作,放在后台执行) */
|
||||
bool should_query = save_to_disk && !is_ipv6(ip) && !is_cidr(ip);
|
||||
if (should_query) {
|
||||
/* 设置忽略SIGCHLD信号,防止僵尸进程 */
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
/* 子进程:执行耗时的网络查询 */
|
||||
char country_code[MAX_COUNTRY_CODE] = {0};
|
||||
if (query_country_code(ip, country_code, sizeof(country_code)) == SUCCESS) {
|
||||
/* 更新持久化文件中的国家信息 */
|
||||
update_ip_country(ip, country_code);
|
||||
log_write("[地理查询] IP=%s 国家=%s", ip, get_country_name(country_code));
|
||||
}
|
||||
|
||||
/* 补充其他IP的国家信息 */
|
||||
supplement_country_info(ip);
|
||||
_exit(0);
|
||||
}
|
||||
/* 父进程:立即返回 */
|
||||
} else if (!save_to_disk) {
|
||||
log_write("[执行封禁] IP=%s 已封禁", ip);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int unban_ip(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 从nftables移除 */
|
||||
nft_remove_from_blacklist(ip);
|
||||
|
||||
/* 从持久化文件移除 */
|
||||
persist_remove_ip(ip);
|
||||
|
||||
log_write("[手动解封] IP=%s", ip);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int persist_add_ip(const char *ip, const char *country_code) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 加锁 */
|
||||
char lock_file[MAX_PATH_LEN];
|
||||
snprintf(lock_file, sizeof(lock_file), "%s.lock", PERSIST_FILE);
|
||||
|
||||
int lock_fd = open(lock_file, O_CREAT | O_RDWR, 0600);
|
||||
if (lock_fd < 0) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
flock(lock_fd, LOCK_EX);
|
||||
|
||||
/* 检查是否已存在 */
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
bool exists = false;
|
||||
|
||||
if (fp) {
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
/* 提取IP部分 */
|
||||
char *pipe = strchr(line, '|');
|
||||
if (pipe) *pipe = '\0';
|
||||
|
||||
if (strcmp(line, ip) == 0) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* 添加到文件 */
|
||||
if (!exists) {
|
||||
fp = fopen(PERSIST_FILE, "a");
|
||||
if (fp) {
|
||||
if (country_code && strlen(country_code) > 0) {
|
||||
fprintf(fp, "%s|%s\n", ip, country_code);
|
||||
} else {
|
||||
fprintf(fp, "%s\n", ip);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
flock(lock_fd, LOCK_UN);
|
||||
close(lock_fd);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int persist_remove_ip(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", PERSIST_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
if (!temp_fp) {
|
||||
fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char line_copy[MAX_LINE_LEN];
|
||||
snprintf(line_copy, sizeof(line_copy), "%s", line);
|
||||
line_copy[strcspn(line_copy, "\n")] = 0;
|
||||
|
||||
/* 提取IP部分 */
|
||||
char *pipe = strchr(line_copy, '|');
|
||||
if (pipe) *pipe = '\0';
|
||||
|
||||
if (strcmp(line_copy, ip) != 0) {
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
rename(temp_file, PERSIST_FILE);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int update_ip_country(const char *ip, const char *country_code) {
|
||||
if (!ip || !country_code) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", PERSIST_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
if (!temp_fp) {
|
||||
fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
bool found = false;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char line_copy[MAX_LINE_LEN];
|
||||
snprintf(line_copy, sizeof(line_copy), "%s", line);
|
||||
line_copy[strcspn(line_copy, "\n")] = 0;
|
||||
|
||||
/* 提取IP部分 */
|
||||
char *pipe = strchr(line_copy, '|');
|
||||
if (pipe) *pipe = '\0';
|
||||
|
||||
if (strcmp(line_copy, ip) == 0 && !found) {
|
||||
/* 找到目标IP,更新国家信息 */
|
||||
fprintf(temp_fp, "%s|%s\n", ip, country_code);
|
||||
found = true;
|
||||
} else {
|
||||
/* 保持原样 */
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
rename(temp_file, PERSIST_FILE);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int restore_from_persist(void) {
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) {
|
||||
return SUCCESS; /* 文件不存在 */
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
int count = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
/* 提取IP部分 */
|
||||
char ip[MAX_IP_LEN];
|
||||
char *pipe = strchr(line, '|');
|
||||
if (pipe) {
|
||||
*pipe = '\0';
|
||||
strncpy(ip, line, sizeof(ip) - 1);
|
||||
} else {
|
||||
strncpy(ip, line, sizeof(ip) - 1);
|
||||
}
|
||||
ip[sizeof(ip) - 1] = '\0';
|
||||
|
||||
/* 恢复到nftables(不保存到磁盘) */
|
||||
ip_info_t info;
|
||||
if (parse_ip_info(ip, &info) == SUCCESS) {
|
||||
if (nft_add_to_blacklist(&info) == SUCCESS) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
log_write("[系统恢复] 已从磁盘恢复 %d 个黑名单 IP", count);
|
||||
|
||||
char message[MAX_LINE_LEN];
|
||||
snprintf(message, sizeof(message), "✅ 已从磁盘恢复 %d 个黑名单 IP", count);
|
||||
msg(C_GREEN, message);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
void show_persist_list(void) {
|
||||
msg(C_CYAN, "=== 📋 本地持久化封禁列表 ===");
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp || fseek(fp, 0, SEEK_END) == 0) {
|
||||
if (fp) fclose(fp);
|
||||
printf("(暂无持久化记录)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
rewind(fp);
|
||||
|
||||
/* 统计 */
|
||||
int total = 0, ipv4_count = 0, ipv6_count = 0;
|
||||
char line[MAX_LINE_LEN];
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
total++;
|
||||
if (strchr(line, ':')) {
|
||||
ipv6_count++;
|
||||
} else {
|
||||
ipv4_count++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("总计: %s%d%s 条 | IPv4: %s%d%s 条 | IPv6: %s%d%s 条\n\n",
|
||||
C_GREEN, total, C_RESET,
|
||||
C_CYAN, ipv4_count, C_RESET,
|
||||
C_YELLOW, ipv6_count, C_RESET);
|
||||
|
||||
printf("%s%-45s%s\n", C_YELLOW, "IP 地址", C_RESET);
|
||||
printf("---------------------------------------------\n");
|
||||
|
||||
rewind(fp);
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
if (strlen(line) > 0) {
|
||||
/* 只显示IP部分 */
|
||||
char *pipe = strchr(line, '|');
|
||||
if (pipe) *pipe = '\0';
|
||||
printf("%-45s\n", line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
printf("\n");
|
||||
printf("%s📌 文件位置: %s%s\n", C_CYAN, PERSIST_FILE, C_RESET);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
#include "common.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
void msg(const char *color, const char *message) {
|
||||
printf("%s%s%s\n", color, message, C_RESET);
|
||||
}
|
||||
|
||||
int check_root(void) {
|
||||
if (getuid() != 0) {
|
||||
msg(C_RED, "❌ 需要 root 权限");
|
||||
return ERROR_PERMISSION;
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
void get_timestamp(char *buffer, size_t size) {
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm_info = localtime(&now);
|
||||
strftime(buffer, size, "%Y-%m-%d %H:%M:%S", tm_info);
|
||||
}
|
||||
|
||||
const char* get_ban_time_from_config(void) {
|
||||
static char ban_time[32] = {0};
|
||||
|
||||
FILE *fp = fopen(CONFIG_FILE, "r");
|
||||
if (!fp) {
|
||||
/* 配置文件不存在,返回默认值 */
|
||||
return DEFAULT_BAN_TIME;
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
/* 跳过注释和空行 */
|
||||
if (line[0] == '#' || line[0] == '\n') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 解析 BAN_TIME=xxx */
|
||||
if (strncmp(line, "BAN_TIME=", 9) == 0) {
|
||||
char *value = line + 9;
|
||||
/* 去除换行符和空白 */
|
||||
char *p = value;
|
||||
while (*p && *p != '\n' && *p != '\r') p++;
|
||||
*p = '\0';
|
||||
|
||||
/* 去除前导空白 */
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
|
||||
if (strlen(value) > 0) {
|
||||
strncpy(ban_time, value, sizeof(ban_time) - 1);
|
||||
ban_time[sizeof(ban_time) - 1] = '\0';
|
||||
fclose(fp);
|
||||
return ban_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return DEFAULT_BAN_TIME;
|
||||
}
|
||||
|
||||
int save_ban_time_to_config(const char *ban_time) {
|
||||
if (!ban_time) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 创建配置目录 */
|
||||
mkdir(CONFIG_DIR, 0700);
|
||||
|
||||
/* 读取现有配置 */
|
||||
FILE *fp = fopen(CONFIG_FILE, "r");
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", CONFIG_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
|
||||
if (!temp_fp) {
|
||||
if (fp) fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
bool has_header = false;
|
||||
|
||||
if (fp) {
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (line[0] == '#') {
|
||||
has_header = true;
|
||||
}
|
||||
|
||||
if (strncmp(line, "BAN_TIME=", 9) == 0) {
|
||||
fprintf(temp_fp, "BAN_TIME=%s\n", ban_time);
|
||||
found = true;
|
||||
} else {
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* 如果没有找到 BAN_TIME,添加新配置 */
|
||||
if (!found) {
|
||||
/* 如果是新文件,添加完整头部 */
|
||||
if (!has_header && access(CONFIG_FILE, F_OK) != 0) {
|
||||
fprintf(temp_fp, "# Block-IP Configuration\n");
|
||||
fprintf(temp_fp, "# Ban time format: Xh (hours), Xm (minutes), or empty for permanent\n");
|
||||
fprintf(temp_fp, "# Examples: 24h, 12h, 1h, 30m, or empty string for permanent ban\n");
|
||||
fprintf(temp_fp, "# Max retries: 1-10, default is 3\n\n");
|
||||
}
|
||||
fprintf(temp_fp, "BAN_TIME=%s\n", ban_time);
|
||||
/* 如果是新文件,也添加默认的 MAX_RETRIES */
|
||||
if (!has_header && access(CONFIG_FILE, F_OK) != 0) {
|
||||
fprintf(temp_fp, "MAX_RETRIES=%d\n", DEFAULT_MAX_RETRIES);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(temp_fp);
|
||||
chmod(temp_file, 0600);
|
||||
rename(temp_file, CONFIG_FILE);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int get_max_retries_from_config(void) {
|
||||
FILE *fp = fopen(CONFIG_FILE, "r");
|
||||
if (!fp) {
|
||||
return DEFAULT_MAX_RETRIES; /* 返回默认值 */
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (line[0] == '#' || line[0] == '\n') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp(line, "MAX_RETRIES=", 12) == 0) {
|
||||
int retries = atoi(line + 12);
|
||||
fclose(fp);
|
||||
return (retries > 0 && retries <= 10) ? retries : DEFAULT_MAX_RETRIES;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return DEFAULT_MAX_RETRIES;
|
||||
}
|
||||
|
||||
int save_max_retries_to_config(int max_retries) {
|
||||
if (max_retries <= 0 || max_retries > 10) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
mkdir(CONFIG_DIR, 0700);
|
||||
|
||||
FILE *fp = fopen(CONFIG_FILE, "r");
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", CONFIG_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
|
||||
if (!temp_fp) {
|
||||
if (fp) fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
bool has_header = false;
|
||||
|
||||
if (fp) {
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (line[0] == '#') {
|
||||
has_header = true;
|
||||
}
|
||||
|
||||
if (strncmp(line, "MAX_RETRIES=", 12) == 0) {
|
||||
fprintf(temp_fp, "MAX_RETRIES=%d\n", max_retries);
|
||||
found = true;
|
||||
} else {
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* 如果没有找到 MAX_RETRIES,添加新配置 */
|
||||
if (!found) {
|
||||
/* 如果是新文件,添加完整头部 */
|
||||
if (!has_header && access(CONFIG_FILE, F_OK) != 0) {
|
||||
fprintf(temp_fp, "# Block-IP Configuration\n");
|
||||
fprintf(temp_fp, "# Ban time format: Xh (hours), Xm (minutes), or empty for permanent\n");
|
||||
fprintf(temp_fp, "# Max retries: 1-10, default is 3\n\n");
|
||||
fprintf(temp_fp, "BAN_TIME=%s\n", DEFAULT_BAN_TIME);
|
||||
}
|
||||
fprintf(temp_fp, "MAX_RETRIES=%d\n", max_retries);
|
||||
}
|
||||
|
||||
fclose(temp_fp);
|
||||
chmod(temp_file, 0600);
|
||||
rename(temp_file, CONFIG_FILE);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "geo.h"
|
||||
#include "log.h"
|
||||
#include <ctype.h>
|
||||
|
||||
/* 国家代码映射表 */
|
||||
static const struct {
|
||||
const char *code;
|
||||
const char *name;
|
||||
} country_map[] = {
|
||||
{"CN", "中国"},
|
||||
{"US", "美国"},
|
||||
{"RU", "俄罗斯"},
|
||||
{"MY", "马来西亚"},
|
||||
{"NL", "荷兰"},
|
||||
{"DE", "德国"},
|
||||
{"GB", "英国"},
|
||||
{"FR", "法国"},
|
||||
{"JP", "日本"},
|
||||
{"KR", "韩国"},
|
||||
{"SG", "新加坡"},
|
||||
{"HK", "香港"},
|
||||
{"TW", "台湾"},
|
||||
{"IN", "印度"},
|
||||
{"BR", "巴西"},
|
||||
{"CA", "加拿大"},
|
||||
{"AU", "澳大利亚"},
|
||||
{"IT", "意大利"},
|
||||
{"ES", "西班牙"},
|
||||
{"SE", "瑞典"},
|
||||
{"PL", "波兰"},
|
||||
{"UA", "乌克兰"},
|
||||
{"TR", "土耳其"},
|
||||
{"ID", "印度尼西亚"},
|
||||
{"TH", "泰国"},
|
||||
{"VN", "越南"},
|
||||
{"MX", "墨西哥"},
|
||||
{"AR", "阿根廷"},
|
||||
{"CL", "智利"},
|
||||
{"RO", "罗马尼亚"},
|
||||
{"CZ", "捷克"}
|
||||
};
|
||||
|
||||
int query_country_code(const char *ip, char *country_code, size_t size) {
|
||||
if (!ip || !country_code) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"curl -s --max-time 2 \"https://ipinfo.io/%s/country\" 2>/dev/null | tr -d '\\n\\r '",
|
||||
ip);
|
||||
|
||||
FILE *fp = popen(command, "r");
|
||||
if (!fp) {
|
||||
return ERROR_NETWORK;
|
||||
}
|
||||
|
||||
char result[16] = {0};
|
||||
if (fgets(result, sizeof(result), fp)) {
|
||||
/* 去除空白字符 */
|
||||
char *p = result;
|
||||
while (*p && isspace(*p)) p++;
|
||||
|
||||
if (strlen(p) == 2 && isalpha(p[0]) && isalpha(p[1])) {
|
||||
strncpy(country_code, p, size - 1);
|
||||
country_code[size - 1] = '\0';
|
||||
pclose(fp);
|
||||
return SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
pclose(fp);
|
||||
return ERROR_NETWORK;
|
||||
}
|
||||
|
||||
const char* get_country_name(const char *country_code) {
|
||||
if (!country_code) return country_code;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(country_map); i++) {
|
||||
if (strcmp(country_map[i].code, country_code) == 0) {
|
||||
return country_map[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
return country_code;
|
||||
}
|
||||
|
||||
void supplement_country_info(const char *current_ip) {
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) return;
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
int update_count = 0;
|
||||
const int MAX_UPDATES = 3;
|
||||
|
||||
/* 创建临时文件 */
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", PERSIST_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
if (!temp_fp) {
|
||||
fclose(fp);
|
||||
return;
|
||||
}
|
||||
|
||||
while (fgets(line, sizeof(line), fp) && update_count < MAX_UPDATES) {
|
||||
/* 去除换行符 */
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
/* 检查是否已有国家信息 */
|
||||
if (strchr(line, '|')) {
|
||||
fprintf(temp_fp, "%s\n", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 检查是否是IPv6或CIDR */
|
||||
if (strchr(line, ':') || strchr(line, '/')) {
|
||||
fprintf(temp_fp, "%s\n", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 跳过当前正在处理的IP */
|
||||
if (current_ip && strcmp(line, current_ip) == 0) {
|
||||
fprintf(temp_fp, "%s\n", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 查询国家信息 */
|
||||
char country_code[MAX_COUNTRY_CODE];
|
||||
if (query_country_code(line, country_code, sizeof(country_code)) == SUCCESS) {
|
||||
fprintf(temp_fp, "%s|%s\n", line, country_code);
|
||||
log_write("[补充地区] IP=%s 国家=%s", line, get_country_name(country_code));
|
||||
update_count++;
|
||||
} else {
|
||||
fprintf(temp_fp, "%s\n", line);
|
||||
}
|
||||
}
|
||||
|
||||
/* 复制剩余内容 */
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
/* 替换原文件 */
|
||||
rename(temp_file, PERSIST_FILE);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "install.h"
|
||||
#include "nftables.h"
|
||||
#include "ban.h"
|
||||
#include "whitelist.h"
|
||||
#include "log.h"
|
||||
|
||||
int setup_pam_hooks(void) {
|
||||
const char *pam_file = "/etc/pam.d/sshd";
|
||||
|
||||
/* 备份原文件 */
|
||||
char backup_cmd[MAX_COMMAND_LEN];
|
||||
snprintf(backup_cmd, sizeof(backup_cmd), "cp -f %s %s.bak.blockip", pam_file, pam_file);
|
||||
system(backup_cmd);
|
||||
|
||||
/* 移除旧的钩子 */
|
||||
char remove_cmd[MAX_COMMAND_LEN];
|
||||
snprintf(remove_cmd, sizeof(remove_cmd), "sed -i '\\|%s|d' %s", INSTALL_PATH, pam_file);
|
||||
system(remove_cmd);
|
||||
|
||||
/* 添加新的钩子 */
|
||||
FILE *fp = fopen(pam_file, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", pam_file);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
if (!temp_fp) {
|
||||
fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
/* 在第一行插入check钩子 */
|
||||
fprintf(temp_fp, "auth optional pam_exec.so quiet %s check\n", INSTALL_PATH);
|
||||
|
||||
/* 复制原内容 */
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
fputs(line, temp_fp);
|
||||
}
|
||||
|
||||
/* 在末尾添加clean钩子 */
|
||||
fprintf(temp_fp, "session optional pam_exec.so quiet %s clean\n", INSTALL_PATH);
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
rename(temp_file, pam_file);
|
||||
|
||||
log_write("[安装] PAM钩子已配置");
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int remove_pam_hooks(void) {
|
||||
const char *pam_file = "/etc/pam.d/sshd";
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command), "sed -i '\\|%s|d' %s", INSTALL_PATH, pam_file);
|
||||
system(command);
|
||||
|
||||
log_write("[卸载] PAM钩子已移除");
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int create_systemd_service(void) {
|
||||
|
||||
const char *service_file = "/etc/systemd/system/bip.service"; FILE *fp = fopen(service_file, "w");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
fprintf(fp, "[Unit]\n");
|
||||
fprintf(fp, "Description=BIP (Block-IP) Service\n");
|
||||
fprintf(fp, "After=network.target nftables.service\n\n");
|
||||
fprintf(fp, "[Service]\n");
|
||||
fprintf(fp, "Type=oneshot\n");
|
||||
fprintf(fp, "ExecStart=%s restore\n", INSTALL_PATH);
|
||||
fprintf(fp, "RemainAfterExit=yes\n\n");
|
||||
fprintf(fp, "[Install]\n");
|
||||
fprintf(fp, "WantedBy=multi-user.target\n");
|
||||
|
||||
fclose(fp);
|
||||
|
||||
/* 重载systemd配置 */
|
||||
system("systemctl daemon-reload");
|
||||
|
||||
/* 启用服务 */
|
||||
system("systemctl enable bip.service");
|
||||
|
||||
log_write("[安装] systemd服务已创建");
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
static int remove_systemd_service(void) {
|
||||
/* 停止并禁用服务 */
|
||||
system("systemctl stop bip.service 2>/dev/null");
|
||||
system("systemctl disable bip.service 2>/dev/null");
|
||||
|
||||
/* 删除服务文件 */
|
||||
remove("/etc/systemd/system/bip.service");
|
||||
|
||||
/* 重载systemd配置 */
|
||||
system("systemctl daemon-reload");
|
||||
|
||||
log_write("[卸载] systemd服务已移除");
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int install_service(void) {
|
||||
msg(C_YELLOW, "开始安装 BIP (Block-IP)...");
|
||||
|
||||
/* 复制程序到安装路径 */
|
||||
char exe_path[MAX_PATH_LEN];
|
||||
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
|
||||
if (len != -1) {
|
||||
exe_path[len] = '\0';
|
||||
|
||||
char copy_cmd[MAX_COMMAND_LEN];
|
||||
snprintf(copy_cmd, sizeof(copy_cmd), "cp -f %s %s && chmod +x %s",
|
||||
exe_path, INSTALL_PATH, INSTALL_PATH);
|
||||
system(copy_cmd);
|
||||
}
|
||||
|
||||
/* 创建必要的目录和文件 */
|
||||
char mkdir_cmd[MAX_COMMAND_LEN];
|
||||
snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s && chmod 700 %s", CONFIG_DIR, CONFIG_DIR);
|
||||
system(mkdir_cmd);
|
||||
|
||||
snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s && chmod 770 %s", RECORD_DIR, RECORD_DIR);
|
||||
system(mkdir_cmd);
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "a");
|
||||
if (fp) {
|
||||
chmod(PERSIST_FILE, 0600);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
fp = fopen(WHITELIST_FILE, "a");
|
||||
if (fp) {
|
||||
chmod(WHITELIST_FILE, 0600);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* 创建默认配置文件 */
|
||||
if (access(CONFIG_FILE, F_OK) != 0) {
|
||||
save_ban_time_to_config(DEFAULT_BAN_TIME);
|
||||
msg(C_GREEN, " ✓ 已创建默认配置文件");
|
||||
}
|
||||
|
||||
log_init();
|
||||
|
||||
/* 安装nftables */
|
||||
check_and_install_nftables();
|
||||
|
||||
/* 初始化规则 */
|
||||
init_nftables_rules();
|
||||
|
||||
/* 恢复数据 */
|
||||
restore_from_persist();
|
||||
whitelist_restore();
|
||||
|
||||
/* 配置PAM钩子 */
|
||||
setup_pam_hooks();
|
||||
|
||||
/* 创建systemd服务 */
|
||||
create_systemd_service();
|
||||
|
||||
msg(C_GREEN, "✅ 安装完成!输入 bip list 查看效果。");
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int uninstall_service(void) {
|
||||
msg(C_YELLOW, "⚠️ 开始卸载 BIP (Block-IP)...");
|
||||
|
||||
/* 移除systemd服务 */
|
||||
remove_systemd_service();
|
||||
msg(C_GREEN, " ✓ 已移除 systemd 服务");
|
||||
|
||||
/* 清除nftables规则 */
|
||||
char command[MAX_COMMAND_LEN];
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete rule %s input ip saddr @%s drop 2>/dev/null",
|
||||
NFT_TABLE, NFT_SET);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete rule %s input ip6 saddr @%s drop 2>/dev/null",
|
||||
NFT_TABLE, NFT_SET_V6);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete rule %s input ip saddr @%s accept 2>/dev/null",
|
||||
NFT_TABLE, NFT_WHITELIST);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete rule %s input ip6 saddr @%s accept 2>/dev/null",
|
||||
NFT_TABLE, NFT_WHITELIST_V6);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete set %s %s 2>/dev/null", NFT_TABLE, NFT_SET);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete set %s %s 2>/dev/null", NFT_TABLE, NFT_SET_V6);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete set %s %s 2>/dev/null", NFT_TABLE, NFT_WHITELIST);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command), "nft delete set %s %s 2>/dev/null", NFT_TABLE, NFT_WHITELIST_V6);
|
||||
system(command);
|
||||
|
||||
msg(C_GREEN, " ✓ 已清除防火墙规则");
|
||||
|
||||
/* 移除PAM钩子 */
|
||||
remove_pam_hooks();
|
||||
msg(C_GREEN, " ✓ 已移除 PAM 钩子");
|
||||
|
||||
/* 询问是否删除数据文件 */
|
||||
printf("是否删除配置目录和日志? [y/N] ");
|
||||
char answer[10];
|
||||
if (fgets(answer, sizeof(answer), stdin)) {
|
||||
if (answer[0] == 'y' || answer[0] == 'Y') {
|
||||
char rm_cmd[MAX_COMMAND_LEN];
|
||||
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf %s", CONFIG_DIR);
|
||||
system(rm_cmd);
|
||||
|
||||
remove(LOG_FILE);
|
||||
char log_backup[MAX_PATH_LEN];
|
||||
snprintf(log_backup, sizeof(log_backup), "%s.1", LOG_FILE);
|
||||
remove(log_backup);
|
||||
|
||||
msg(C_GREEN, " ✓ 已删除数据文件");
|
||||
} else {
|
||||
printf("%s ↳ 保留: %s, %s%s\n",
|
||||
C_CYAN, CONFIG_DIR, LOG_FILE, C_RESET);
|
||||
}
|
||||
}
|
||||
|
||||
/* 删除程序文件 */
|
||||
remove(INSTALL_PATH);
|
||||
msg(C_GREEN, " ✓ 已删除程序文件");
|
||||
|
||||
msg(C_GREEN, "\n✅ 卸载完成!");
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "ip_utils.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
|
||||
bool is_ipv6(const char *ip) {
|
||||
if (!ip) return false;
|
||||
|
||||
/* 移除CIDR部分 */
|
||||
char ip_copy[MAX_IP_LEN];
|
||||
strncpy(ip_copy, ip, sizeof(ip_copy) - 1);
|
||||
ip_copy[sizeof(ip_copy) - 1] = '\0';
|
||||
|
||||
char *slash = strchr(ip_copy, '/');
|
||||
if (slash) *slash = '\0';
|
||||
|
||||
return strchr(ip_copy, ':') != NULL;
|
||||
}
|
||||
|
||||
bool is_cidr(const char *ip) {
|
||||
return strchr(ip, '/') != NULL;
|
||||
}
|
||||
|
||||
int parse_ip_info(const char *input, ip_info_t *info) {
|
||||
if (!input || !info) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
memset(info, 0, sizeof(ip_info_t));
|
||||
strncpy(info->ip, input, sizeof(info->ip) - 1);
|
||||
|
||||
/* 检查是否是CIDR */
|
||||
char *slash = strchr(info->ip, '/');
|
||||
if (slash) {
|
||||
*slash = '\0';
|
||||
info->cidr_mask = atoi(slash + 1);
|
||||
|
||||
if (is_ipv6(info->ip)) {
|
||||
info->type = IP_TYPE_V6_CIDR;
|
||||
} else {
|
||||
info->type = IP_TYPE_V4_CIDR;
|
||||
}
|
||||
*slash = '/'; /* 恢复 */
|
||||
} else {
|
||||
if (is_ipv6(info->ip)) {
|
||||
info->type = IP_TYPE_V6;
|
||||
} else {
|
||||
info->type = IP_TYPE_V4;
|
||||
}
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
char* get_remote_ip(void) {
|
||||
static char ip[MAX_IP_LEN];
|
||||
char *env_ip = NULL;
|
||||
|
||||
env_ip = getenv("PAM_RHOST");
|
||||
if (!env_ip) {
|
||||
env_ip = getenv("RHOST");
|
||||
}
|
||||
|
||||
if (env_ip) {
|
||||
strncpy(ip, env_ip, sizeof(ip) - 1);
|
||||
ip[sizeof(ip) - 1] = '\0';
|
||||
return ip;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool validate_ip_format(const char *ip) {
|
||||
if (!ip) return false;
|
||||
|
||||
char ip_copy[MAX_IP_LEN];
|
||||
strncpy(ip_copy, ip, sizeof(ip_copy) - 1);
|
||||
ip_copy[sizeof(ip_copy) - 1] = '\0';
|
||||
|
||||
/* 检查CIDR */
|
||||
char *slash = strchr(ip_copy, '/');
|
||||
if (slash) {
|
||||
*slash = '\0';
|
||||
int mask = atoi(slash + 1);
|
||||
|
||||
if (is_ipv6(ip_copy)) {
|
||||
if (mask < 0 || mask > 128) return false;
|
||||
} else {
|
||||
if (mask < 0 || mask > 32) return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 验证IP地址 */
|
||||
struct sockaddr_in sa;
|
||||
struct sockaddr_in6 sa6;
|
||||
|
||||
if (inet_pton(AF_INET, ip_copy, &(sa.sin_addr)) == 1) {
|
||||
return true;
|
||||
}
|
||||
if (inet_pton(AF_INET6, ip_copy, &(sa6.sin6_addr)) == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void format_nft_element(const char *ip, char *output, size_t size, const char *timeout) {
|
||||
if (!ip || !output) return;
|
||||
|
||||
char element[MAX_LINE_LEN];
|
||||
|
||||
if (is_cidr(ip)) {
|
||||
strncpy(element, ip, sizeof(element) - 1);
|
||||
} else if (is_ipv6(ip)) {
|
||||
snprintf(element, sizeof(element), "%s/128", ip);
|
||||
} else {
|
||||
snprintf(element, sizeof(element), "%s/32", ip);
|
||||
}
|
||||
|
||||
if (timeout && strlen(timeout) > 0) {
|
||||
snprintf(output, size, "%s timeout %s", element, timeout);
|
||||
} else {
|
||||
snprintf(output, size, "%s", element);
|
||||
}
|
||||
}
|
||||
|
||||
bool ip_matches_whitelist_entry(const char *ip, const char *whitelist_entry) {
|
||||
if (!ip || !whitelist_entry) return false;
|
||||
|
||||
/* 完全匹配 */
|
||||
if (strcmp(ip, whitelist_entry) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* CIDR匹配 */
|
||||
if (strchr(whitelist_entry, '/')) {
|
||||
char wl_copy[MAX_IP_LEN];
|
||||
strncpy(wl_copy, whitelist_entry, sizeof(wl_copy) - 1);
|
||||
wl_copy[sizeof(wl_copy) - 1] = '\0';
|
||||
|
||||
char *slash = strchr(wl_copy, '/');
|
||||
if (slash) {
|
||||
*slash = '\0';
|
||||
int mask = atoi(slash + 1);
|
||||
|
||||
/* 简单的IPv4段匹配 */
|
||||
if (!is_ipv6(ip)) {
|
||||
char ip_prefix[MAX_IP_LEN];
|
||||
strncpy(ip_prefix, ip, sizeof(ip_prefix) - 1);
|
||||
|
||||
if (mask == 8) {
|
||||
/* 匹配 A.*.*.* */
|
||||
char *dot = strchr(ip_prefix, '.');
|
||||
if (dot) *dot = '\0';
|
||||
return strncmp(ip, wl_copy, strlen(ip_prefix)) == 0;
|
||||
} else if (mask == 16) {
|
||||
/* 匹配 A.B.*.* */
|
||||
char *dot1 = strchr(ip_prefix, '.');
|
||||
if (dot1) {
|
||||
char *dot2 = strchr(dot1 + 1, '.');
|
||||
if (dot2) *dot2 = '\0';
|
||||
}
|
||||
return strncmp(ip, wl_copy, strlen(ip_prefix)) == 0;
|
||||
} else if (mask == 24) {
|
||||
/* 匹配 A.B.C.* */
|
||||
char *dot1 = strchr(ip_prefix, '.');
|
||||
if (dot1) {
|
||||
char *dot2 = strchr(dot1 + 1, '.');
|
||||
if (dot2) {
|
||||
char *dot3 = strchr(dot2 + 1, '.');
|
||||
if (dot3) *dot3 = '\0';
|
||||
}
|
||||
}
|
||||
return strncmp(ip, wl_copy, strlen(ip_prefix)) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "log.h"
|
||||
#include <stdarg.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
|
||||
int log_init(void) {
|
||||
FILE *fp = fopen(LOG_FILE, "a");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
chmod(LOG_FILE, 0666);
|
||||
fclose(fp);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
void log_rotate(void) {
|
||||
struct stat st;
|
||||
if (stat(LOG_FILE, &st) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (st.st_size >= MAX_LOG_SIZE) {
|
||||
char backup_file[MAX_PATH_LEN];
|
||||
snprintf(backup_file, sizeof(backup_file), "%s.1", LOG_FILE);
|
||||
|
||||
remove(backup_file);
|
||||
rename(LOG_FILE, backup_file);
|
||||
|
||||
FILE *fp = fopen(LOG_FILE, "w");
|
||||
if (fp) {
|
||||
chmod(LOG_FILE, 0666);
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void log_write(const char *format, ...) {
|
||||
log_rotate();
|
||||
|
||||
FILE *fp = fopen(LOG_FILE, "a");
|
||||
if (!fp) {
|
||||
return;
|
||||
}
|
||||
|
||||
char timestamp[64];
|
||||
get_timestamp(timestamp, sizeof(timestamp));
|
||||
|
||||
fprintf(fp, "[%s] ", timestamp);
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(fp, format, args);
|
||||
va_end(args);
|
||||
|
||||
fprintf(fp, "\n");
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
void log_show_recent(int lines) {
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command), "tail -n %d %s 2>/dev/null", lines, LOG_FILE);
|
||||
system(command);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "common.h"
|
||||
#include "log.h"
|
||||
#include "ban.h"
|
||||
#include "whitelist.h"
|
||||
#include "stats.h"
|
||||
#include "pam.h"
|
||||
#include "install.h"
|
||||
#include "nftables.h"
|
||||
#include "ip_utils.h"
|
||||
|
||||
/* 显示帮助信息 */
|
||||
void show_help(void) {
|
||||
printf("BIP (Block-IP) %s - IPv6 + CIDR + Whitelist\n", BIP_VERSION);
|
||||
printf("--------------------------------------\n");
|
||||
printf("使用方法:\n");
|
||||
printf(" bip list 查看实时统计/活跃列表/日志\n");
|
||||
printf(" bip show 显示本地持久化封禁列表\n");
|
||||
printf(" bip add <IP> 手动封禁 IP (支持IPv4/IPv6/CIDR)\n");
|
||||
printf(" 示例: 1.1.1.1 或 1.1.1.0/24 或 2001:db8::/32\n");
|
||||
printf(" bip del <IP> 手动解封 IP (支持IPv4/IPv6/CIDR)\n");
|
||||
printf(" bip vip add <IP> 添加IP到白名单 (支持IPv4/IPv6/CIDR)\n");
|
||||
printf(" bip vip del <IP> 从白名单移除IP\n");
|
||||
printf(" bip vip list 显示白名单列表\n");
|
||||
printf(" bip config 显示当前配置\n");
|
||||
printf(" bip config time <time> 设置封禁时间 (如: 24h, 12h, 1h, 30m, \"\" 为永久)\n");
|
||||
printf(" bip config retries <N> 设置最大重试次数 (1-10)\n");
|
||||
printf(" bip restore 从持久化文件恢复黑白名单\n");
|
||||
printf(" bip install 安装/重装服务\n");
|
||||
printf(" bip uninstall 卸载服务\n");
|
||||
printf("--------------------------------------------------------\n");
|
||||
}
|
||||
|
||||
/* VIP白名单子命令处理 */
|
||||
static int handle_vip_command(int argc, char *argv[]) {
|
||||
if (argc < 3) {
|
||||
msg(C_RED, "用法: bip vip {add|del|list} <IP>");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *subcmd = argv[2];
|
||||
|
||||
if (strcmp(subcmd, "list") == 0) {
|
||||
whitelist_show();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
if (argc < 4) {
|
||||
msg(C_RED, "错误: 需要提供IP地址");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *ip = argv[3];
|
||||
|
||||
if (!validate_ip_format(ip)) {
|
||||
char error_msg[MAX_LINE_LEN];
|
||||
snprintf(error_msg, sizeof(error_msg), "❌ 无效的IP格式: %s", ip);
|
||||
msg(C_RED, error_msg);
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (strcmp(subcmd, "add") == 0) {
|
||||
if (nft_add_to_whitelist(ip) == SUCCESS) {
|
||||
whitelist_add_to_file(ip);
|
||||
log_write("[白名单添加] IP=%s", ip);
|
||||
|
||||
char success_msg[MAX_LINE_LEN];
|
||||
snprintf(success_msg, sizeof(success_msg), "✅ 已添加到白名单: %s", ip);
|
||||
msg(C_GREEN, success_msg);
|
||||
return SUCCESS;
|
||||
}
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
if (strcmp(subcmd, "del") == 0) {
|
||||
nft_remove_from_whitelist(ip);
|
||||
whitelist_remove_from_file(ip);
|
||||
log_write("[白名单移除] IP=%s", ip);
|
||||
|
||||
char success_msg[MAX_LINE_LEN];
|
||||
snprintf(success_msg, sizeof(success_msg), "✅ 已从白名单移除: %s", ip);
|
||||
msg(C_GREEN, success_msg);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
msg(C_RED, "用法: bip vip {add|del|list} <IP>");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 主函数 */
|
||||
int main(int argc, char *argv[]) {
|
||||
/* 无参数显示帮助 */
|
||||
if (argc < 2) {
|
||||
show_help();
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *command = argv[1];
|
||||
|
||||
if (strcmp(command, "version") == 0 || strcmp(command, "v") == 0 || strcmp(command, "-v") == 0) {
|
||||
printf("BIP (Block-IP) 版本: %s\n", BIP_VERSION);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* PAM钩子:check命令 */
|
||||
if (strcmp(command, "check") == 0) {
|
||||
return pam_check_failed_login();
|
||||
}
|
||||
|
||||
/* PAM钩子:clean命令 */
|
||||
if (strcmp(command, "clean") == 0) {
|
||||
return pam_clean_on_success();
|
||||
}
|
||||
|
||||
/* list命令:显示统计信息 */
|
||||
if (strcmp(command, "list") == 0) {
|
||||
show_statistics();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* show命令:显示持久化列表 */
|
||||
if (strcmp(command, "show") == 0) {
|
||||
show_persist_list();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* vip命令:白名单管理 */
|
||||
if (strcmp(command, "vip") == 0) {
|
||||
return handle_vip_command(argc, argv);
|
||||
}
|
||||
|
||||
/* config命令:配置管理 */
|
||||
if (strcmp(command, "config") == 0) {
|
||||
if (argc == 2) {
|
||||
/* 显示当前配置 */
|
||||
const char *ban_time = get_ban_time_from_config();
|
||||
int max_retries = get_max_retries_from_config();
|
||||
printf("%s当前配置%s\n", C_CYAN, C_RESET);
|
||||
printf("封禁时间: %s%s%s", C_GREEN, ban_time, C_RESET);
|
||||
if (strlen(ban_time) == 0) {
|
||||
printf(" (永久封禁)\n");
|
||||
} else {
|
||||
printf("\n");
|
||||
}
|
||||
printf("最大重试次数: %s%d%s\n", C_GREEN, max_retries, C_RESET);
|
||||
printf("配置文件: %s\n", CONFIG_FILE);
|
||||
return SUCCESS;
|
||||
} else if (argc == 4 && strcmp(argv[2], "time") == 0) {
|
||||
/* 设置封禁时间 */
|
||||
const char *new_time = argv[3];
|
||||
|
||||
if (save_ban_time_to_config(new_time) == SUCCESS) {
|
||||
char msg_buf[MAX_LINE_LEN];
|
||||
if (strlen(new_time) == 0) {
|
||||
snprintf(msg_buf, sizeof(msg_buf), "✅ 封禁时间已设置为: 永久封禁");
|
||||
} else {
|
||||
snprintf(msg_buf, sizeof(msg_buf), "✅ 封禁时间已设置为: %s", new_time);
|
||||
}
|
||||
msg(C_GREEN, msg_buf);
|
||||
msg(C_YELLOW, "提示: 新的封禁时间将在下次封禁时生效");
|
||||
return SUCCESS;
|
||||
}
|
||||
return ERROR_FILE;
|
||||
} else if (argc == 4 && strcmp(argv[2], "retries") == 0) {
|
||||
/* 设置最大重试次数 */
|
||||
int retries = atoi(argv[3]);
|
||||
if (save_max_retries_to_config(retries) == SUCCESS) {
|
||||
char msg_buf[MAX_LINE_LEN];
|
||||
snprintf(msg_buf, sizeof(msg_buf), "✅ 最大重试次数已设置为: %d", retries);
|
||||
msg(C_GREEN, msg_buf);
|
||||
msg(C_YELLOW, "提示: 新的重试次数将在下次验证时生效");
|
||||
return SUCCESS;
|
||||
}
|
||||
msg(C_RED, "❌ 设置失败: 请使用1-10之间的整数");
|
||||
return ERROR_INVALID_ARG;
|
||||
} else {
|
||||
msg(C_RED, "用法: bip config");
|
||||
msg(C_RED, " bip config time <time>");
|
||||
msg(C_RED, " bip config retries <count>");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
/* add命令:手动封禁IP */
|
||||
if (strcmp(command, "add") == 0) {
|
||||
if (argc < 3) {
|
||||
msg(C_RED, "错误: 需要提供IP地址");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *ip = argv[2];
|
||||
|
||||
if (!validate_ip_format(ip)) {
|
||||
char error_msg[MAX_LINE_LEN];
|
||||
snprintf(error_msg, sizeof(error_msg), "❌ 无效的IP格式: %s", ip);
|
||||
msg(C_RED, error_msg);
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (ban_ip(ip, true) == SUCCESS) {
|
||||
char success_msg[MAX_LINE_LEN];
|
||||
snprintf(success_msg, sizeof(success_msg), "✅ 已封禁: %s", ip);
|
||||
msg(C_GREEN, success_msg);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
/* del命令:手动解封IP */
|
||||
if (strcmp(command, "del") == 0) {
|
||||
if (argc < 3) {
|
||||
msg(C_RED, "错误: 需要提供IP地址");
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *ip = argv[2];
|
||||
|
||||
if (unban_ip(ip) == SUCCESS) {
|
||||
char success_msg[MAX_LINE_LEN];
|
||||
snprintf(success_msg, sizeof(success_msg), "✅ 已解封: %s", ip);
|
||||
msg(C_GREEN, success_msg);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
/* restore命令:恢复黑白名单 */
|
||||
if (strcmp(command, "restore") == 0) {
|
||||
if (check_root() != SUCCESS) {
|
||||
return ERROR_PERMISSION;
|
||||
}
|
||||
|
||||
check_and_install_nftables();
|
||||
init_nftables_rules();
|
||||
restore_from_persist();
|
||||
whitelist_restore();
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* install命令:安装服务 */
|
||||
if (strcmp(command, "install") == 0) {
|
||||
if (check_root() != SUCCESS) {
|
||||
return ERROR_PERMISSION;
|
||||
}
|
||||
|
||||
return install_service();
|
||||
}
|
||||
|
||||
/* uninstall命令:卸载服务 */
|
||||
if (strcmp(command, "uninstall") == 0) {
|
||||
if (check_root() != SUCCESS) {
|
||||
return ERROR_PERMISSION;
|
||||
}
|
||||
|
||||
return uninstall_service();
|
||||
}
|
||||
|
||||
/* 未知命令 */
|
||||
show_help();
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
#include "nftables.h"
|
||||
#include "log.h"
|
||||
#include <sys/wait.h>
|
||||
|
||||
int check_and_install_nftables(void) {
|
||||
/* 检查nft命令是否存在 */
|
||||
if (access("/usr/sbin/nft", X_OK) == 0 || access("/sbin/nft", X_OK) == 0) {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* 尝试安装nftables */
|
||||
FILE *fp = fopen("/etc/os-release", "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
char os_id[64] = {0};
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (strncmp(line, "ID=", 3) == 0) {
|
||||
sscanf(line, "ID=%s", os_id);
|
||||
/* 移除引号 */
|
||||
char *start = strchr(os_id, '"');
|
||||
if (start) {
|
||||
start++;
|
||||
char *end = strchr(start, '"');
|
||||
if (end) *end = '\0';
|
||||
memmove(os_id, start, strlen(start) + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
/* 根据发行版安装 */
|
||||
if (strcmp(os_id, "debian") == 0 || strcmp(os_id, "ubuntu") == 0 || strcmp(os_id, "kali") == 0) {
|
||||
system("apt-get update && apt-get install -y nftables");
|
||||
} else if (strcmp(os_id, "centos") == 0 || strcmp(os_id, "rhel") == 0 || strcmp(os_id, "alma") == 0) {
|
||||
system("dnf install -y nftables || yum install -y nftables");
|
||||
} else if (strcmp(os_id, "alpine") == 0) {
|
||||
system("apk add nftables");
|
||||
} else {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
/* 加载内核模块 */
|
||||
system("modprobe nf_tables >/dev/null 2>&1");
|
||||
|
||||
/* 启用服务 */
|
||||
system("systemctl enable --now nftables >/dev/null 2>&1");
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int init_nftables_rules(void) {
|
||||
char command[MAX_COMMAND_LEN];
|
||||
|
||||
/* 创建表 */
|
||||
snprintf(command, sizeof(command), "nft add table %s 2>/dev/null", NFT_TABLE);
|
||||
system(command);
|
||||
|
||||
/* 创建黑名单集合 */
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add set %s %s '{ type ipv4_addr; flags interval,timeout; }' 2>/dev/null",
|
||||
NFT_TABLE, NFT_SET);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add set %s %s '{ type ipv6_addr; flags interval,timeout; }' 2>/dev/null",
|
||||
NFT_TABLE, NFT_SET_V6);
|
||||
system(command);
|
||||
|
||||
/* 创建白名单集合 */
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add set %s %s '{ type ipv4_addr; flags interval; }' 2>/dev/null",
|
||||
NFT_TABLE, NFT_WHITELIST);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add set %s %s '{ type ipv6_addr; flags interval; }' 2>/dev/null",
|
||||
NFT_TABLE, NFT_WHITELIST_V6);
|
||||
system(command);
|
||||
|
||||
/* 创建input链 */
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add chain %s input '{ type filter hook input priority 0; }' 2>/dev/null",
|
||||
NFT_TABLE);
|
||||
system(command);
|
||||
|
||||
/* 添加白名单规则 */
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list chain %s input | grep -q '@%s' || nft insert rule %s input ip saddr @%s accept",
|
||||
NFT_TABLE, NFT_WHITELIST, NFT_TABLE, NFT_WHITELIST);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list chain %s input | grep -q '@%s' || nft insert rule %s input ip6 saddr @%s accept",
|
||||
NFT_TABLE, NFT_WHITELIST_V6, NFT_TABLE, NFT_WHITELIST_V6);
|
||||
system(command);
|
||||
|
||||
/* 添加黑名单规则 */
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list chain %s input | grep -q '@%s' || nft insert rule %s input ip saddr @%s drop",
|
||||
NFT_TABLE, NFT_SET, NFT_TABLE, NFT_SET);
|
||||
system(command);
|
||||
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list chain %s input | grep -q '@%s' || nft insert rule %s input ip6 saddr @%s drop",
|
||||
NFT_TABLE, NFT_SET_V6, NFT_TABLE, NFT_SET_V6);
|
||||
system(command);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int nft_add_to_blacklist(const ip_info_t *ip_info) {
|
||||
if (!ip_info) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 从配置文件读取封禁时间 */
|
||||
const char *ban_time = get_ban_time_from_config();
|
||||
|
||||
char element[MAX_LINE_LEN];
|
||||
format_nft_element(ip_info->ip, element, sizeof(element), ban_time);
|
||||
|
||||
const char *set_name = (ip_info->type == IP_TYPE_V6 || ip_info->type == IP_TYPE_V6_CIDR)
|
||||
? NFT_SET_V6 : NFT_SET;
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add element %s %s '{ %s }' 2>&1",
|
||||
NFT_TABLE, set_name, element);
|
||||
|
||||
FILE *fp = popen(command, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char output[MAX_LINE_LEN];
|
||||
bool need_init = false;
|
||||
|
||||
if (fgets(output, sizeof(output), fp)) {
|
||||
if (strstr(output, "No such file")) {
|
||||
need_init = true;
|
||||
}
|
||||
}
|
||||
pclose(fp);
|
||||
|
||||
if (need_init) {
|
||||
init_nftables_rules();
|
||||
system(command);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int nft_remove_from_blacklist(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char element[MAX_LINE_LEN];
|
||||
format_nft_element(ip, element, sizeof(element), NULL);
|
||||
|
||||
const char *set_name = is_ipv6(ip) ? NFT_SET_V6 : NFT_SET;
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft delete element %s %s '{ %s }' >/dev/null 2>&1",
|
||||
NFT_TABLE, set_name, element);
|
||||
|
||||
system(command);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int nft_add_to_whitelist(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char element[MAX_LINE_LEN];
|
||||
format_nft_element(ip, element, sizeof(element), NULL);
|
||||
|
||||
const char *set_name = is_ipv6(ip) ? NFT_WHITELIST_V6 : NFT_WHITELIST;
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft add element %s %s '{ %s }' 2>&1",
|
||||
NFT_TABLE, set_name, element);
|
||||
|
||||
FILE *fp = popen(command, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char output[MAX_LINE_LEN];
|
||||
bool need_init = false;
|
||||
|
||||
if (fgets(output, sizeof(output), fp)) {
|
||||
if (strstr(output, "No such file")) {
|
||||
need_init = true;
|
||||
}
|
||||
}
|
||||
pclose(fp);
|
||||
|
||||
if (need_init) {
|
||||
init_nftables_rules();
|
||||
system(command);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int nft_remove_from_whitelist(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char element[MAX_LINE_LEN];
|
||||
format_nft_element(ip, element, sizeof(element), NULL);
|
||||
|
||||
const char *set_name = is_ipv6(ip) ? NFT_WHITELIST_V6 : NFT_WHITELIST;
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft delete element %s %s '{ %s }' >/dev/null 2>&1",
|
||||
NFT_TABLE, set_name, element);
|
||||
|
||||
system(command);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int nft_get_set_count(const char *set_name) {
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list set %s %s 2>/dev/null | sed 's/,/\\n/g' | sed 's/elements = {//g; s/}//g' | awk '{for(i=1;i<=NF;i++) if($i==\"expires\") print $1}' | wc -l",
|
||||
NFT_TABLE, set_name);
|
||||
|
||||
FILE *fp = popen(command, "r");
|
||||
if (!fp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
fscanf(fp, "%d", &count);
|
||||
pclose(fp);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
int nft_list_set_elements(const char *set_name, char *buffer, size_t size) {
|
||||
if (!set_name || !buffer) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"nft list set %s %s 2>/dev/null",
|
||||
NFT_TABLE, set_name);
|
||||
|
||||
FILE *fp = popen(command, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
char line[MAX_LINE_LEN];
|
||||
|
||||
while (fgets(line, sizeof(line), fp) && offset < size - 1) {
|
||||
size_t len = strlen(line);
|
||||
if (offset + len < size - 1) {
|
||||
memcpy(buffer + offset, line, len);
|
||||
offset += len;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer[offset] = '\0';
|
||||
pclose(fp);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "pam.h"
|
||||
#include "ban.h"
|
||||
#include "ip_utils.h"
|
||||
#include "whitelist.h"
|
||||
#include "log.h"
|
||||
#include <sys/file.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* 初始化信号处理(防止僵尸进程) */
|
||||
static void init_sigchld_handler(void) {
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = SIG_IGN; /* 忽略子进程退出信号 */
|
||||
sa.sa_flags = SA_NOCLDWAIT; /* 不产生僵尸进程 */
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGCHLD, &sa, NULL);
|
||||
}
|
||||
|
||||
/* 异步封禁IP(子进程中执行) */
|
||||
static void async_ban_ip(const char *ip) {
|
||||
/* 设置子进程退出时自动回收,避免僵尸进程 */
|
||||
static int initialized = 0;
|
||||
if (!initialized) {
|
||||
init_sigchld_handler();
|
||||
initialized = 1;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
/* fork失败,同步执行 */
|
||||
ban_ip(ip, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
/* 子进程:执行封禁操作 */
|
||||
ban_ip(ip, true);
|
||||
_exit(0); /* 子进程退出 */
|
||||
}
|
||||
|
||||
/* 父进程:立即返回,不等待子进程 */
|
||||
}
|
||||
|
||||
int pam_check_failed_login(void) {
|
||||
char *ip = get_remote_ip();
|
||||
if (!ip) {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* 检查白名单(快速路径) */
|
||||
if (is_in_whitelist(ip)) {
|
||||
log_write("[白名单放行] IP=%s", ip);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/* 记录失败次数 */
|
||||
int count = get_failure_count(ip);
|
||||
count++;
|
||||
record_failure(ip);
|
||||
|
||||
int max_retries = get_max_retries_from_config();
|
||||
log_write("[验证失败] IP=%s (第 %d/%d 次)", ip, count, max_retries);
|
||||
|
||||
/* 达到阈值,异步封禁(不阻塞SSH) */
|
||||
if (count >= max_retries) {
|
||||
async_ban_ip(ip);
|
||||
clear_failure_record(ip);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int pam_clean_on_success(void) {
|
||||
char *ip = get_remote_ip();
|
||||
if (!ip) {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int count = get_failure_count(ip);
|
||||
if (count > 0) {
|
||||
log_write("[登录成功] IP=%s (计数已重置)", ip);
|
||||
clear_failure_record(ip);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int record_failure(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 确保记录目录存在 */
|
||||
mkdir(RECORD_DIR, 0700);
|
||||
|
||||
char record_file[MAX_PATH_LEN];
|
||||
snprintf(record_file, sizeof(record_file), "%s/%s", RECORD_DIR, ip);
|
||||
|
||||
int count = get_failure_count(ip);
|
||||
count++;
|
||||
|
||||
FILE *fp = fopen(record_file, "w");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
fprintf(fp, "%d\n", count);
|
||||
fclose(fp);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int clear_failure_record(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char record_file[MAX_PATH_LEN];
|
||||
snprintf(record_file, sizeof(record_file), "%s/%s", RECORD_DIR, ip);
|
||||
|
||||
remove(record_file);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int get_failure_count(const char *ip) {
|
||||
if (!ip) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char record_file[MAX_PATH_LEN];
|
||||
snprintf(record_file, sizeof(record_file), "%s/%s", RECORD_DIR, ip);
|
||||
|
||||
FILE *fp = fopen(record_file, "r");
|
||||
if (!fp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
fscanf(fp, "%d", &count);
|
||||
fclose(fp);
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
#include "stats.h"
|
||||
#include "nftables.h"
|
||||
#include "log.h"
|
||||
#include "geo.h"
|
||||
#include <ctype.h>
|
||||
|
||||
void show_active_bans(void) {
|
||||
msg(C_CYAN, "=== 🔥 活跃封禁列表 (最新 5 条) ===");
|
||||
|
||||
/* 获取IPv4和IPv6黑名单 */
|
||||
char buffer_v4[8192] = {0};
|
||||
char buffer_v6[8192] = {0};
|
||||
|
||||
nft_list_set_elements(NFT_SET, buffer_v4, sizeof(buffer_v4));
|
||||
nft_list_set_elements(NFT_SET_V6, buffer_v6, sizeof(buffer_v6));
|
||||
|
||||
/* 解析并提取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; } | "
|
||||
"sed 's/,/\\n/g' | sed 's/elements = {//g; s/}//g' | "
|
||||
"awk '{for(i=1;i<=NF;i++) if($i==\"expires\") {time=$(i+1); gsub(\"ms\",\"\",time); print $1, 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) {
|
||||
printf("%s\n", line);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
pclose(fp);
|
||||
|
||||
if (count == 0) {
|
||||
printf("(目前没有被封禁的 IP)\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void show_subnet_aggregation(void) {
|
||||
msg(C_CYAN, "=== 📊 攻击源聚合统计 (自动识别 IP 段) ===");
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) {
|
||||
printf("(无数据)\n\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 创建临时文件存储IPv4地址 */
|
||||
char temp_v4_file[MAX_PATH_LEN];
|
||||
snprintf(temp_v4_file, sizeof(temp_v4_file), "/tmp/blockip_v4_$$");
|
||||
FILE *temp_v4 = fopen(temp_v4_file, "w");
|
||||
|
||||
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++;
|
||||
} else if (temp_v4) {
|
||||
fprintf(temp_v4, "%s\n", line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
if (temp_v4) fclose(temp_v4);
|
||||
|
||||
/* 聚合分析 */
|
||||
char command[MAX_COMMAND_LEN * 2];
|
||||
snprintf(command, sizeof(command),
|
||||
"if [ -f %s ]; then "
|
||||
" cat %s | cut -d. -f1-3 | sort | uniq -c | awk '$1>=2 {printf \"%%d|%%s|24\\n\", $1, $2}' > /tmp/agg24_$$; "
|
||||
" cat %s | cut -d. -f1-2 | sort | uniq -c | awk '$1>=2 {printf \"%%d|%%s|16\\n\", $1, $2}' > /tmp/agg16_$$; "
|
||||
" cat %s | cut -d. -f1 | sort | uniq -c | awk '$1>=2 {printf \"%%d|%%s|8\\n\", $1, $2}' > /tmp/agg8_$$; "
|
||||
" cat /tmp/agg24_$$ /tmp/agg16_$$ /tmp/agg8_$$ | sort -t'|' -k1,1rn -k3,3n | head -n 10; "
|
||||
" rm -f /tmp/agg24_$$ /tmp/agg16_$$ /tmp/agg8_$$; "
|
||||
"fi",
|
||||
temp_v4_file, temp_v4_file, temp_v4_file, temp_v4_file);
|
||||
|
||||
fp = popen(command, "r");
|
||||
bool has_output = false;
|
||||
|
||||
if (fp) {
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
int count, mask;
|
||||
char subnet[MAX_IP_LEN];
|
||||
|
||||
if (sscanf(line, "%d|%[^|]|%d", &count, subnet, &mask) == 3) {
|
||||
has_output = true;
|
||||
if (mask == 8) {
|
||||
printf(" - %-18s %s(%d 个)%s\n",
|
||||
strcat(subnet, ".0.0.0/8"), C_RED, count, C_RESET);
|
||||
} else if (mask == 16) {
|
||||
printf(" - %-18s %s(%d 个)%s\n",
|
||||
strcat(subnet, ".0.0/16"), C_RED, count, C_RESET);
|
||||
} else if (mask == 24) {
|
||||
printf(" - %-18s %s(%d 个)%s\n",
|
||||
strcat(subnet, ".0/24"), C_RED, count, C_RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
pclose(fp);
|
||||
}
|
||||
|
||||
if (!has_output) {
|
||||
printf(" - (散乱分布 IPv4)\n");
|
||||
}
|
||||
|
||||
if (v6_count > 0) {
|
||||
printf(" - (IPv6 地址) (%d 个)\n", v6_count);
|
||||
}
|
||||
|
||||
remove(temp_v4_file);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void show_country_stats(void) {
|
||||
msg(C_CYAN, "=== 🌍 攻击源国家/地区统计 ===");
|
||||
|
||||
FILE *fp = fopen(PERSIST_FILE, "r");
|
||||
if (!fp) {
|
||||
printf("(暂无数据)\n\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 创建临时文件存储国家代码 */
|
||||
char temp_country_file[MAX_PATH_LEN];
|
||||
snprintf(temp_country_file, sizeof(temp_country_file), "/tmp/blockip_country_$$");
|
||||
FILE *temp_fp = fopen(temp_country_file, "w");
|
||||
|
||||
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) {
|
||||
fprintf(temp_fp, "%s\n", pipe + 1);
|
||||
has_data = true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
if (!has_data) {
|
||||
printf("(暂无国家信息)\n\n");
|
||||
remove(temp_country_file);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 统计国家分布 */
|
||||
char command[MAX_COMMAND_LEN];
|
||||
snprintf(command, sizeof(command),
|
||||
"sort %s | uniq -c | sort -rn | head -n 9",
|
||||
temp_country_file);
|
||||
|
||||
fp = popen(command, "r");
|
||||
if (fp) {
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
int count;
|
||||
char code[MAX_COUNTRY_CODE];
|
||||
|
||||
if (sscanf(line, "%d %s", &count, code) == 2) {
|
||||
printf(" - %s %s(%d 个)%s\n",
|
||||
get_country_name(code), C_RED, count, C_RESET);
|
||||
}
|
||||
}
|
||||
pclose(fp);
|
||||
}
|
||||
|
||||
remove(temp_country_file);
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "whitelist.h"
|
||||
#include "ip_utils.h"
|
||||
#include "nftables.h"
|
||||
#include "log.h"
|
||||
|
||||
bool is_in_whitelist(const char *ip) {
|
||||
if (!ip) return false;
|
||||
|
||||
FILE *fp = fopen(WHITELIST_FILE, "r");
|
||||
if (!fp) return false;
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
bool found = false;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
if (ip_matches_whitelist_entry(ip, line)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return found;
|
||||
}
|
||||
|
||||
int whitelist_add_to_file(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* 检查是否已存在 */
|
||||
FILE *fp = fopen(WHITELIST_FILE, "r");
|
||||
if (fp) {
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
if (strcmp(line, ip) == 0) {
|
||||
fclose(fp);
|
||||
return SUCCESS; /* 已存在 */
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* 添加到文件 */
|
||||
fp = fopen(WHITELIST_FILE, "a");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
fprintf(fp, "%s\n", ip);
|
||||
fclose(fp);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int whitelist_remove_from_file(const char *ip) {
|
||||
if (!ip) {
|
||||
return ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(WHITELIST_FILE, "r");
|
||||
if (!fp) {
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char temp_file[MAX_PATH_LEN];
|
||||
snprintf(temp_file, sizeof(temp_file), "%s.tmp", WHITELIST_FILE);
|
||||
FILE *temp_fp = fopen(temp_file, "w");
|
||||
if (!temp_fp) {
|
||||
fclose(fp);
|
||||
return ERROR_FILE;
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (strcmp(line, ip) != 0) {
|
||||
fprintf(temp_fp, "%s\n", line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
fclose(temp_fp);
|
||||
|
||||
rename(temp_file, WHITELIST_FILE);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
void whitelist_show(void) {
|
||||
msg(C_CYAN, "=== 📋 VIP 白名单列表 ===");
|
||||
|
||||
FILE *fp = fopen(WHITELIST_FILE, "r");
|
||||
if (!fp || fseek(fp, 0, SEEK_END) == 0) {
|
||||
if (fp) fclose(fp);
|
||||
printf("(暂无白名单记录)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
rewind(fp);
|
||||
|
||||
/* 统计 */
|
||||
int total = 0, ipv4_count = 0, ipv6_count = 0;
|
||||
char line[MAX_LINE_LEN];
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
total++;
|
||||
if (strchr(line, ':')) {
|
||||
ipv6_count++;
|
||||
} else {
|
||||
ipv4_count++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("总计: %s%d%s 条 | IPv4: %s%d%s 条 | IPv6: %s%d%s 条\n\n",
|
||||
C_GREEN, total, C_RESET,
|
||||
C_CYAN, ipv4_count, C_RESET,
|
||||
C_YELLOW, ipv6_count, C_RESET);
|
||||
|
||||
printf("%s%-45s%s\n", C_YELLOW, "IP 地址", C_RESET);
|
||||
printf("---------------------------------------------\n");
|
||||
|
||||
rewind(fp);
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
if (strlen(line) > 0) {
|
||||
printf("%-45s\n", line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
printf("\n");
|
||||
printf("%s📌 文件位置: %s%s\n", C_CYAN, WHITELIST_FILE, C_RESET);
|
||||
}
|
||||
|
||||
int whitelist_restore(void) {
|
||||
FILE *fp = fopen(WHITELIST_FILE, "r");
|
||||
if (!fp) {
|
||||
return SUCCESS; /* 文件不存在,无需恢复 */
|
||||
}
|
||||
|
||||
char line[MAX_LINE_LEN];
|
||||
int count = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (strlen(line) == 0) continue;
|
||||
|
||||
if (nft_add_to_whitelist(line) == SUCCESS) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
log_write("[系统恢复] 已从磁盘恢复 %d 个白名单 IP", count);
|
||||
|
||||
char message[MAX_LINE_LEN];
|
||||
snprintf(message, sizeof(message), "✅ 已从磁盘恢复 %d 个白名单 IP", count);
|
||||
msg(C_GREEN, message);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user