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。但作为应用层开发者,我们也能做点事情。我个人习惯在应用层加三道防线:

  1. 连接限制 – 限制单个IP的最大连接数
  2. 速率限制 – 限制单位时间内的连接请求数
  3. 超时处理 – 及时清理无效的半连接

这三道防线配合起来,能挡住大部分低配版的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的连接数

检测到攻击后,可以采取以下措施:

  1. 临时封禁IP – 把攻击IP加入黑名单,持续一段时间
  2. 降低阈值 – 动态调整连接限制和速率限制的参数
  3. 启用SYN Cookie – 如果内核支持,可以临时开启

完整防护流程

下面这张图展示了整个防护流程的逻辑:

DDoS防护流程 客户端 攻击者 连接限制 速率限制 攻击检测 业务处理 黑名单 正常 异常 防护流程说明 1. 客户端和攻击者同时发起连接请求 2. 连接限制模块检查IP连接数,速率限制模块检查请求频率 3. 攻击检测模块综合分析,正常请求进入业务处理,异常请求加入黑名单 4. 黑名单中的IP后续请求直接被拒绝

综合防护代码

最后,我把连接限制和速率限制整合到一起,形成一个完整的防护函数。你可以在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服务可以设严一点
  • 监控指标要实时,发现异常立即响应
  • 定期清理黑名单和过期记录,防止内存泄漏

嗯,今天就聊到这儿。这些代码你拿回去改改就能用,但记得要根据自己的业务场景调参数。毕竟每个系统的承受能力不一样。

公众号:蓝海资料掘金营,微信 deep3321