This commit is contained in:
sushen339
2025-11-18 12:54:21 +08:00
parent 57ecd5ab17
commit 43fc123cb4
25 changed files with 2978 additions and 2 deletions
+2 -2
View File
@@ -98,8 +98,8 @@ check_and_install_env() {
if ! command -v nft >/dev/null 2>&1; then
. /etc/os-release
case "$ID" in
debian|ubuntu|kali) apt-get update && apt-get install -y nftables ;;
centos|rhel|alma) dnf install -y nftables || yum install -y nftables ;;
debian|ubuntu|kali) apt-get update && apt-get install -y nftables curl ;;
centos|rhel|alma) dnf install -y nftables || yum install -y nftables curl ;;
alpine) apk add nftables ;;
*) return 1 ;;
esac
+32
View File
@@ -0,0 +1,32 @@
# 编译生成的文件
*.o
*.a
*.so
*.out
bip
# 目录
obj/
bin/
# 调试文件
*.dSYM/
*.su
*.idb
*.pdb
# 编辑器和IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# 临时文件
*.tmp
*.log
*.bak
# 构建文件
Makefile.deps
+103
View File
@@ -0,0 +1,103 @@
# Block-IP Makefile
# C语言重构版本
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11
LDFLAGS =
TARGET = bip
INSTALL_PATH = /usr/local/bin
# 源文件目录
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
# 源文件
SRCS = $(SRC_DIR)/main.c \
$(SRC_DIR)/common.c \
$(SRC_DIR)/log.c \
$(SRC_DIR)/ip_utils.c \
$(SRC_DIR)/geo.c \
$(SRC_DIR)/nftables.c \
$(SRC_DIR)/whitelist.c \
$(SRC_DIR)/ban.c \
$(SRC_DIR)/pam.c \
$(SRC_DIR)/stats.c \
$(SRC_DIR)/install.c
# 目标文件
OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
# 头文件依赖
DEPS = $(wildcard $(INC_DIR)/*.h)
# 默认目标
all: $(TARGET)
# 创建目标目录
$(OBJ_DIR):
mkdir -p $(OBJ_DIR)
# 编译目标文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS) | $(OBJ_DIR)
$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@
# 链接生成可执行文件
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $(TARGET)
@echo "编译完成: $(TARGET)"
# 安装
install: $(TARGET)
@if [ $$(id -u) -ne 0 ]; then \
echo "错误: 需要root权限执行安装"; \
exit 1; \
fi
install -m 755 $(TARGET) $(INSTALL_PATH)/$(TARGET)
@echo "已安装到: $(INSTALL_PATH)/$(TARGET)"
@echo "运行 'block-ip install' 来完成系统配置"
# 卸载
uninstall:
@if [ $$(id -u) -ne 0 ]; then \
echo "错误: 需要root权限执行卸载"; \
exit 1; \
fi
$(INSTALL_PATH)/$(TARGET) uninstall || true
rm -f $(INSTALL_PATH)/$(TARGET)
@echo "已卸载"
# 清理编译文件
clean:
rm -rf $(OBJ_DIR) $(TARGET)
@echo "清理完成"
# 清理所有文件(包括配置)
distclean: clean
rm -rf /etc/blockip
rm -f /var/log/block-ip.log /var/log/block-ip.log.1
@echo "深度清理完成"
# 调试版本
debug: CFLAGS += -g -DDEBUG
debug: clean $(TARGET)
# 静态链接版本
static: LDFLAGS += -static
static: clean $(TARGET)
# 显示帮助
help:
@echo "Block-IP 构建系统"
@echo ""
@echo "可用目标:"
@echo " make - 编译程序"
@echo " make install - 安装到系统 (需要root)"
@echo " make uninstall- 从系统卸载 (需要root)"
@echo " make clean - 清理编译文件"
@echo " make distclean- 清理所有文件(包括配置)"
@echo " make debug - 编译调试版本"
@echo " make static - 编译静态链接版本"
@echo " make help - 显示此帮助信息"
.PHONY: all install uninstall clean distclean debug static help
+279
View File
@@ -0,0 +1,279 @@
# BIP (Block-IP) - C语言实现
基于 nftables 的自动封禁恶意IP工具,支持IPv4/IPv6和CIDR格式。
## 特性
## 模块划分
```
blockip/
├── include/ # 头文件目录
│ ├── common.h # 公共定义和工具函数
│ ├── log.h # 日志模块
│ ├── ip_utils.h # IP地址处理工具
│ ├── geo.h # 地理位置查询
│ ├── nftables.h # nftables操作接口
│ ├── whitelist.h # 白名单管理
│ ├── ban.h # 封禁/解封核心逻辑
│ ├── pam.h # PAM集成模块
│ ├── stats.h # 统计和展示
│ └── install.h # 安装/卸载功能
├── src/ # 源文件目录
│ ├── main.c # 主程序入口
│ ├── common.c # 公共函数实现
│ ├── log.c # 日志功能实现
│ ├── ip_utils.c # IP处理实现
│ ├── geo.c # 地理位置实现
│ ├── nftables.c # nftables实现
│ ├── whitelist.c # 白名单实现
│ ├── ban.c # 封禁逻辑实现
│ ├── pam.c # PAM集成实现
│ ├── stats.c # 统计功能实现
│ └── install.c # 安装功能实现
├── Makefile # 构建脚本
└── README.md # 本文档
```
## 系统要求
## 编译安装
### 1. 编译程序
```bash
cd blockip
make
```
### 2. 安装到系统(需要root权限)
```bash
sudo make install
```
### 3. 配置系统服务
```bash
sudo bip install
```
这将自动完成以下配置:
## 使用方法
### 查看状态和统计
```bash
# 查看实时统计、活跃封禁列表、日志
bip list
# 显示本地持久化封禁列表
bip show
```
### 手动封禁/解封IP
```bash
# 封禁单个IPv4地址
bip add 1.2.3.4
# 封禁IPv4网段(CIDR
bip add 1.2.3.0/24
# 封禁IPv6地址
bip add 2001:db8::1
# 封禁IPv6网段
bip add 2001:db8::/32
# 解封IP
bip del 1.2.3.4
```
### 白名单管理
```bash
# 添加IP到白名单
bip vip add 192.168.1.100
# 添加网段到白名单
bip vip add 192.168.0.0/16
# 从白名单移除
bip vip del 192.168.1.100
# 显示白名单列表
bip vip list
```
### 系统管理
```bash
# 从持久化文件恢复黑白名单
bip restore
# 卸载服务
bip uninstall
```
## 工作原理
1. **PAM集成**:通过PAM模块监控SSH登录尝试
2. **失败计数**:记录每个IP的失败登录次数
3. **自动封禁**:达到阈值(默认3次)后自动封禁IP
4. **异步处理**:使用fork子进程异步执行封禁和地理查询,不阻塞SSH登录
5. **nftables规则**:使用nftables的集合(set)功能高效封禁
6. **持久化存储**:封禁记录保存到磁盘,重启后自动恢复
7. **白名单保护**:白名单IP永不封禁
8. **自动解封**24小时后自动解封(可配置)
## 配置参数
### 动态配置(无需重新编译)
使用配置文件 `/etc/blockip/config` 灵活修改封禁时间:
```bash
# 查看当前配置
bip config
# 设置封禁时间为12小时
bip config time 12h
# 设置封禁时间为30分钟
bip config time 30m
# 设置为永久封禁
bip config time ""
# 设置最大重试次数为5次
bip config retries 5
```
支持的配置参数:
- `24h` - 24小时
- `12h` - 12小时
- `1h` - 1小时
- `30m` - 30分钟
- `""` - 永久封禁(空字符串)
- 范围:1-10 次
- 默认:3 次
- 说明:SSH登录失败达到此次数后自动封禁
配置文件位置:`/etc/blockip/config`
### 静态配置(需要重新编译)
`include/common.h` 中可以修改以下默认参数:
```c
#define MAX_RETRIES 3 // 默认最大失败次数
#define DEFAULT_BAN_TIME "24h" // 默认封禁时长
```
修改后需要重新编译:
```bash
make clean
make
sudo make install
```
## 文件说明
- `config` - 配置文件(封禁时间等)
- `blacklist` - 封禁IP列表(持久化)
- `whitelist` - 白名单列表
- `counts/` - 失败次数记录目录
## 卸载
```bash
# 完全卸载(会询问是否删除数据文件)
sudo make uninstall
```
或者:
```bash
sudo bip uninstall
```
## 开发和调试
### 编译调试版本
```bash
make debug
```
### 清理编译文件
```bash
make clean
```
### 深度清理(包括配置文件)
```bash
make distclean
```
## 性能优化
## 注意事项
1. **白名单优先**:请先将信任的IP加入白名单,避免误封
2. **网段封禁**:使用CIDR封禁时请谨慎,避免误伤
3. **日志监控**:定期查看日志,了解攻击情况
4. **备份配置**:重要服务器建议备份白名单配置
## 常见问题
### Q: 不小心把自己封禁了怎么办?
A: 通过控制台登录服务器,执行:
```bash
bip del YOUR_IP
bip vip add YOUR_IP
```
### Q: 如何查看当前封禁了多少IP?
A: 执行 `bip list` 查看统计信息
### Q: 封禁时间可以永久吗?
A: 修改 `include/common.h` 中的 `BAN_TIME` 为空字符串 `""`,然后重新编译
### Q: 支持自定义失败次数吗?
A: 修改 `include/common.h` 中的 `MAX_RETRIES` 值,然后重新编译
## 技术特点
## 贡献
欢迎提交Issue和Pull Request
## 许可证
MIT License
## 作者
原Shell版本:su
C语言重构:GitHub Copilot
## 更新日志
### v16.2-C (2025-11-18)
**享受更安全的服务器环境! 🛡️**
+29
View File
@@ -0,0 +1,29 @@
#ifndef BAN_H
#define BAN_H
#include "common.h"
#include "ip_utils.h"
#include <stdbool.h>
/* 封禁IP */
int ban_ip(const char *ip, bool save_to_disk);
/* 解封IP */
int unban_ip(const char *ip);
/* 添加到持久化列表 */
int persist_add_ip(const char *ip, const char *country_code);
/* 从持久化列表移除 */
int persist_remove_ip(const char *ip);
/* 更新IP的国家信息 */
int update_ip_country(const char *ip, const char *country_code);
/* 恢复持久化列表到nftables */
int restore_from_persist(void);
/* 显示持久化列表 */
void show_persist_list(void);
#endif /* BAN_H */
+79
View File
@@ -0,0 +1,79 @@
#ifndef COMMON_H
#define COMMON_H
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
/* 配置常量 */
#define BIP_VERSION "v25.11.18"
#define CONFIG_DIR "/etc/blockip"
#define CONFIG_FILE CONFIG_DIR "/config"
#define LOG_FILE "/var/log/bip.log"
#define MAX_LOG_SIZE 10485760 // 10MB
#define DEFAULT_MAX_RETRIES 3
#define DEFAULT_BAN_TIME "24h"
#define RECORD_DIR CONFIG_DIR "/counts"
#define PERSIST_FILE CONFIG_DIR "/blacklist"
#define WHITELIST_FILE CONFIG_DIR "/whitelist"
#define INSTALL_PATH "/usr/local/bin/bip"
#define NFT_TABLE "inet filter"
#define NFT_SET "blacklist"
#define NFT_SET_V6 "blacklist_v6"
#define NFT_WHITELIST "whitelist"
#define NFT_WHITELIST_V6 "whitelist_v6"
/* 缓冲区大小 */
#define MAX_LINE_LEN 512
#define MAX_IP_LEN 128
#define MAX_COUNTRY_CODE 8
#define MAX_COMMAND_LEN 1024
#define MAX_PATH_LEN 256
/* 颜色定义 */
#define C_RESET "\033[0m"
#define C_GREEN "\033[32m"
#define C_CYAN "\033[36m"
#define C_YELLOW "\033[33m"
#define C_RED "\033[31m"
/* 错误码 */
#define SUCCESS 0
#define ERROR_PERMISSION -1
#define ERROR_FILE -2
#define ERROR_NETWORK -3
#define ERROR_INVALID_ARG -4
/* 工具宏 */
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
/* 打印消息 */
void msg(const char *color, const char *message);
/* 检查root权限 */
int check_root(void);
/* 获取当前时间戳字符串 */
void get_timestamp(char *buffer, size_t size);
/* 读取配置文件中的封禁时间 */
const char* get_ban_time_from_config(void);
/* 保存封禁时间到配置文件 */
int save_ban_time_to_config(const char *ban_time);
/* 读取配置文件中的最大重试次数 */
int get_max_retries_from_config(void);
/* 保存最大重试次数到配置文件 */
int save_max_retries_to_config(int max_retries);
#endif /* COMMON_H */
+15
View File
@@ -0,0 +1,15 @@
#ifndef GEO_H
#define GEO_H
#include "common.h"
/* 查询IP的国家代码 */
int query_country_code(const char *ip, char *country_code, size_t size);
/* 获取国家名称 */
const char* get_country_name(const char *country_code);
/* 补充持久化文件中缺失的国家信息 */
void supplement_country_info(const char *current_ip);
#endif /* GEO_H */
+21
View File
@@ -0,0 +1,21 @@
#ifndef INSTALL_H
#define INSTALL_H
#include "common.h"
/* 安装服务 */
int install_service(void);
/* 卸载服务 */
int uninstall_service(void);
/* 配置PAM钩子 */
int setup_pam_hooks(void);
/* 移除PAM钩子 */
int remove_pam_hooks(void);
/* 创建systemd服务 */
int create_systemd_service(void);
#endif /* INSTALL_H */
+45
View File
@@ -0,0 +1,45 @@
#ifndef IP_UTILS_H
#define IP_UTILS_H
#include "common.h"
#include <stdbool.h>
/* IP类型 */
typedef enum {
IP_TYPE_UNKNOWN = 0,
IP_TYPE_V4,
IP_TYPE_V6,
IP_TYPE_V4_CIDR,
IP_TYPE_V6_CIDR
} ip_type_t;
/* IP信息结构 */
typedef struct {
char ip[MAX_IP_LEN];
char country_code[MAX_COUNTRY_CODE];
ip_type_t type;
int cidr_mask;
} ip_info_t;
/* 判断是否为IPv6 */
bool is_ipv6(const char *ip);
/* 判断是否为CIDR格式 */
bool is_cidr(const char *ip);
/* 解析IP信息 */
int parse_ip_info(const char *input, ip_info_t *info);
/* 获取当前连接IP */
char* get_remote_ip(void);
/* 验证IP格式 */
bool validate_ip_format(const char *ip);
/* 格式化IP为nftables元素 */
void format_nft_element(const char *ip, char *output, size_t size, const char *timeout);
/* 检查IP是否匹配白名单 */
bool ip_matches_whitelist_entry(const char *ip, const char *whitelist_entry);
#endif /* IP_UTILS_H */
+18
View File
@@ -0,0 +1,18 @@
#ifndef LOG_H
#define LOG_H
#include "common.h"
/* 日志初始化 */
int log_init(void);
/* 写入日志 */
void log_write(const char *format, ...);
/* 日志轮转 */
void log_rotate(void);
/* 显示最新日志 */
void log_show_recent(int lines);
#endif /* LOG_H */
+32
View File
@@ -0,0 +1,32 @@
#ifndef NFTABLES_H
#define NFTABLES_H
#include "common.h"
#include "ip_utils.h"
#include <stdbool.h>
/* 检查并安装nftables环境 */
int check_and_install_nftables(void);
/* 初始化nftables规则 */
int init_nftables_rules(void);
/* 添加IP到nftables黑名单 */
int nft_add_to_blacklist(const ip_info_t *ip_info);
/* 从nftables黑名单移除IP */
int nft_remove_from_blacklist(const char *ip);
/* 添加IP到nftables白名单 */
int nft_add_to_whitelist(const char *ip);
/* 从nftables白名单移除IP */
int nft_remove_from_whitelist(const char *ip);
/* 获取nftables集合中的元素数量 */
int nft_get_set_count(const char *set_name);
/* 列出nftables集合中的元素 */
int nft_list_set_elements(const char *set_name, char *buffer, size_t size);
#endif /* NFTABLES_H */
+21
View File
@@ -0,0 +1,21 @@
#ifndef PAM_H
#define PAM_H
#include "common.h"
/* PAM检查:处理登录失败 */
int pam_check_failed_login(void);
/* PAM清理:处理登录成功 */
int pam_clean_on_success(void);
/* 记录失败次数 */
int record_failure(const char *ip);
/* 清除失败记录 */
int clear_failure_record(const char *ip);
/* 获取失败次数 */
int get_failure_count(const char *ip);
#endif /* PAM_H */
+18
View File
@@ -0,0 +1,18 @@
#ifndef STATS_H
#define STATS_H
#include "common.h"
/* 显示完整统计信息 */
void show_statistics(void);
/* 显示活跃封禁列表 */
void show_active_bans(void);
/* 显示国家统计 */
void show_country_stats(void);
/* 显示IP段聚合统计 */
void show_subnet_aggregation(void);
#endif /* STATS_H */
+22
View File
@@ -0,0 +1,22 @@
#ifndef WHITELIST_H
#define WHITELIST_H
#include "common.h"
#include <stdbool.h>
/* 检查IP是否在白名单中 */
bool is_in_whitelist(const char *ip);
/* 添加IP到白名单文件 */
int whitelist_add_to_file(const char *ip);
/* 从白名单文件移除IP */
int whitelist_remove_from_file(const char *ip);
/* 显示白名单列表 */
void whitelist_show(void);
/* 恢复白名单到nftables */
int whitelist_restore(void);
#endif /* WHITELIST_H */
+330
View File
@@ -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);
}
+201
View File
@@ -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;
}
+150
View File
@@ -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);
}
+246
View 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;
}
+180
View File
@@ -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;
}
+63
View File
@@ -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);
}
+263
View File
@@ -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;
}
+284
View File
@@ -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;
}
+145
View File
@@ -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;
}
+228
View File
@@ -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");
}
+172
View File
@@ -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;
}