c
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
**享受更安全的服务器环境! 🛡️**
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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