91、DDoS防护:SYN Flood防护、连接限制、速率限制
DDoS攻击,说白了就是有人想用流量把你服务器撑死。我做了这么多年网络编程,最头疼的就是这个。尤其是SYN Flood,它不跟你讲道理,直接在半连接状态上做文章。今天咱们就聊聊怎么在应用层做点防护。
SYN Flood攻击原理
先简单回顾一下。正常的TCP三次握手是这样的:客户端发SYN,服务器回SYN+ACK,客户端再回ACK。连接建立后,服务器把这个连接放到全连接队列里。
但攻击者不按套路出牌。他只发SYN,不回最后的ACK。服务器那边呢?它傻傻地等着,半连接队列就慢慢被塞满了。满了之后,正常的用户连接就进不来了。
嗯,这里要注意:半连接队列的大小是有限的。我记得Linux内核参数里有个tcp_max_syn_backlog,默认也就1024左右。攻击者稍微发点包,队列就满了。
核心问题:服务器资源被无效的半连接耗尽,导致正常用户无法建立连接。
应用层防护策略
说实话,真正的SYN Flood防护应该在内核层面做,比如SYN Cookie。但作为应用层开发者,我们也能做点事情。我个人习惯在应用层加三道防线:
- 连接限制 – 限制单个IP的最大连接数
- 速率限制 – 限制单位时间内的连接请求数
- 超时处理 – 及时清理无效的半连接
这三道防线配合起来,能挡住大部分低配版的SYN Flood攻击。
连接限制实现
连接限制的思路很简单:记录每个IP当前的连接数,超过阈值就拒绝。我在项目中遇到过一个问题:如果只限制连接数,攻击者可以慢慢建立连接,然后一直保持。所以还要加上时间维度。
// 连接限制示例:基于IP的连接数管理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define MAX_CONN_PER_IP 10
#define HASH_TABLE_SIZE 1024
typedef struct conn_entry {
uint32_t ip;
int conn_count;
time_t last_update;
struct conn_entry *next;
} conn_entry_t;
static conn_entry_t *hash_table[HASH_TABLE_SIZE];
static pthread_mutex_t table_lock = PTHREAD_MUTEX_INITIALIZER;
// 简单的哈希函数
static uint32_t hash_ip(uint32_t ip) {
return ip % HASH_TABLE_SIZE;
}
// 检查并更新连接数
int check_and_update_conn(uint32_t ip) {
pthread_mutex_lock(&table_lock);
uint32_t idx = hash_ip(ip);
conn_entry_t *entry = hash_table[idx];
// 查找现有条目
while (entry) {
if (entry->ip == ip) {
// 检查是否超时(60秒无更新则重置)
if (time(NULL) - entry->last_update > 60) {
entry->conn_count = 1;
entry->last_update = time(NULL);
pthread_mutex_unlock(&table_lock);
return 1; // 允许连接
}
if (entry->conn_count >= MAX_CONN_PER_IP) {
pthread_mutex_unlock(&table_lock);
return 0; // 拒绝连接
}
entry->conn_count++;
entry->last_update = time(NULL);
pthread_mutex_unlock(&table_lock);
return 1;
}
entry = entry->next;
}
// 创建新条目
entry = malloc(sizeof(conn_entry_t));
if (!entry) {
pthread_mutex_unlock(&table_lock);
return 0;
}
entry->ip = ip;
entry->conn_count = 1;
entry->last_update = time(NULL);
entry->next = hash_table[idx];
hash_table[idx] = entry;
pthread_mutex_unlock(&table_lock);
return 1;
}
// 在accept后调用
void handle_new_connection(int client_fd, struct sockaddr_in *client_addr) {
uint32_t client_ip = ntohl(client_addr->sin_addr.s_addr);
if (!check_and_update_conn(client_ip)) {
// 连接数超限,直接关闭
close(client_fd);
printf("拒绝来自 %s 的连接(连接数超限)\n",
inet_ntoa(client_addr->sin_addr));
return;
}
// 正常处理连接
printf("接受来自 %s 的连接\n", inet_ntoa(client_addr->sin_addr));
// ... 业务处理
}
小提示:连接数阈值要根据你的服务器配置来调。我一般先设10,然后观察实际使用情况。如果正常用户经常被误杀,就往上调一调。
速率限制实现
速率限制是防SYN Flood的关键。攻击者会在短时间内发大量SYN包,我们得在应用层识别出这种异常行为。
我常用的方法是令牌桶算法。说白了就是:每个IP有一个桶,桶里有一定数量的令牌。每来一个连接请求,就消耗一个令牌。令牌用完了,就得等下一轮补充。
// 速率限制:令牌桶实现
#include <time.h>
#define TOKEN_BUCKET_SIZE 20 // 桶容量
#define TOKEN_REFILL_RATE 5 // 每秒补充5个令牌
#define TOKEN_REFILL_INTERVAL 1 // 补充间隔(秒)
typedef struct token_bucket {
uint32_t ip;
int tokens;
time_t last_refill;
struct token_bucket *next;
} token_bucket_t;
static token_bucket_t *bucket_list = NULL;
static pthread_mutex_t bucket_lock = PTHREAD_MUTEX_INITIALIZER;
// 获取令牌
int get_token(uint32_t ip) {
pthread_mutex_lock(&bucket_lock);
token_bucket_t *bucket = bucket_list;
while (bucket) {
if (bucket->ip == ip) break;
bucket = bucket->next;
}
if (!bucket) {
// 新IP,创建桶
bucket = malloc(sizeof(token_bucket_t));
if (!bucket) {
pthread_mutex_unlock(&bucket_lock);
return 0;
}
bucket->ip = ip;
bucket->tokens = TOKEN_BUCKET_SIZE;
bucket->last_refill = time(NULL);
bucket->next = bucket_list;
bucket_list = bucket;
}
// 补充令牌
time_t now = time(NULL);
int elapsed = now - bucket->last_refill;
if (elapsed >= TOKEN_REFILL_INTERVAL) {
int refill_count = elapsed * TOKEN_REFILL_RATE;
bucket->tokens = (bucket->tokens + refill_count > TOKEN_BUCKET_SIZE)
? TOKEN_BUCKET_SIZE
: bucket->tokens + refill_count;
bucket->last_refill = now;
}
// 检查是否有令牌
if (bucket->tokens > 0) {
bucket->tokens--;
pthread_mutex_unlock(&bucket_lock);
return 1; // 允许连接
}
pthread_mutex_unlock(&bucket_lock);
return 0; // 限速
}
注意:令牌桶的参数需要根据业务场景调整。如果每秒补充5个令牌,桶容量20,那一个IP每秒最多能建立5个连接,突发情况下可以连续建立20个。这个参数对正常用户来说够用了,但攻击者会被限制住。
SYN Flood检测与响应
光有防护还不够,你得能检测到攻击。我一般会监控几个指标:
| 指标 | 正常值 | 异常值 | 说明 |
|---|---|---|---|
| 半连接数 | < 100 | > 1000 | 大量SYN_RCVD状态 |
| SYN包速率 | < 100/s | > 1000/s | 单位时间SYN包数量 |
| 连接成功率 | > 90% | < 50% | 完成三次握手的比例 |
| 单IP连接数 | < 10 | > 50 | 同一IP的连接数 |
检测到攻击后,可以采取以下措施:
- 临时封禁IP – 把攻击IP加入黑名单,持续一段时间
- 降低阈值 – 动态调整连接限制和速率限制的参数
- 启用SYN Cookie – 如果内核支持,可以临时开启
完整防护流程
下面这张图展示了整个防护流程的逻辑:
综合防护代码
最后,我把连接限制和速率限制整合到一起,形成一个完整的防护函数。你可以在accept()之后调用它。
// 综合防护函数
int ddos_protection_check(int client_fd, struct sockaddr_in *client_addr) {
uint32_t client_ip = ntohl(client_addr->sin_addr.s_addr);
// 第一步:检查黑名单(这里简化了,实际应该持久化)
// 第二步:连接限制检查
if (!check_and_update_conn(client_ip)) {
printf("[防护] 拒绝连接:IP %s 连接数超限\n",
inet_ntoa(client_addr->sin_addr));
close(client_fd);
return 0;
}
// 第三步:速率限制检查
if (!get_token(client_ip)) {
printf("[防护] 拒绝连接:IP %s 请求频率过高\n",
inet_ntoa(client_addr->sin_addr));
close(client_fd);
return 0;
}
return 1; // 通过所有检查
}
// 使用示例
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... 绑定、监听等操作
while (1) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr*)&client_addr,
&addr_len);
if (client_fd < 0) continue;
// 执行防护检查
if (!ddos_protection_check(client_fd, &client_addr)) {
continue; // 被拒绝的连接,继续等待下一个
}
// 正常处理连接
// ... 业务逻辑
}
return 0;
}
经验之谈:我曾经在一个项目中只做了连接限制,没做速率限制。结果攻击者用大量IP慢慢建立连接,每个IP只连一两个,连接限制完全失效。后来加上速率限制才解决问题。所以这两者要配合使用,缺一不可。
总结
SYN Flood防护没有银弹。内核层的SYN Cookie是最后一道防线,应用层的连接限制和速率限制是日常防护手段。我个人建议:
- 连接限制阈值设小一点,宁可误杀也不能让服务器挂掉
- 速率限制要配合业务场景,比如API服务可以设严一点
- 监控指标要实时,发现异常立即响应
- 定期清理黑名单和过期记录,防止内存泄漏
嗯,今天就聊到这儿。这些代码你拿回去改改就能用,但记得要根据自己的业务场景调参数。毕竟每个系统的承受能力不一样。