当你的服务器开启了SYN Cookie来防御DDoS攻击时,你会发现一个非常棘手的问题:所有连接到你服务器的客户端IP都变成了负载均衡器或者CDN节点的IP,而不是真实用户的IP。这是因为SYN Cookie机制在TCP三次握手阶段就直接响应了SYN请求,根本没有经过正常的accept()流程,导致内核层面无法通过getsockname()或者recvmsg()获取到对端真实IP。要解决这个问题,最核心的方案就是在负载均衡层启用Proxy Protocol,让它把真实客户端IP以协议头的方式透传给后端服务器,后端再从协议头中解析出真实IP。这篇文章会把这套机制从原理到配置、从Nginx到HAProxy到Linux内核参数,全部给你讲透。
SYN Cookie为什么会导致真实IP丢失
正常情况下,TCP三次握手是这样的:客户端发SYN,服务端回复SYN+ACK并把连接放入半连接队列,客户端再回ACK,服务端调用accept()把连接从队列移到全连接队列,这时候内核才会记录对端IP。但开启SYN Cookie之后,服务端在收到SYN时不维护半连接队列,而是根据SYN包的信息(源IP、源端口、序列号等)计算出一个cookie值塞到SYN+ACK的序列号里发回去。客户端回ACK时,服务端验证这个cookie,验证通过就直接建立连接。整个过程绕过了accept(),内核没有机会在socket结构体里记录对端IP,所以你在应用层看到的全部是负载均衡器的IP。
这在DDoS防护场景下是必须的,因为SYN Flood攻击就是疯狂发送SYN包占满半连接队列,SYN Cookie直接让你不需要维护队列就能扛住攻击。但代价就是真实IP信息丢失,对于需要做访问控制、频率限制、日志审计、地理位置分析的业务来说,这是不可接受的。
Proxy Protocol是什么以及它如何解决这个问题
Proxy Protocol是一个网络协议,最初由HAProxy作者Willy Tarreau提出,后来被广泛支持。它的核心思想很简单:在TCP连接建立之后、真正的应用数据传输之前,负载均衡器先发送一行文本,告诉后端服务器"这个连接的真实客户端IP是多少、真实端口是多少、服务端监听的IP和端口是多少"。这行文本就是Proxy Protocol头。
Proxy Protocol有两个版本:v1是纯文本格式,v2是二进制格式。v1的格式长这样:
PROXY TCP4 203.0.113.1 198.51.100.1 56324 443\r\n
意思是:这是一个TCP4连接,客户端真实IP是203.0.113.1,服务端IP是198.51.100.1,客户端端口56324,服务端端口443。后端服务器收到这行之后,就可以从这里面提取真实IP,而不依赖内核记录的IP信息。
关键在于:Proxy Protocol头是在TCP连接已经建立之后发送的,所以即使后端开启了SYN Cookie,只要连接建立成功,负载均衡器就能把这个头塞进来。这就完美绕开了SYN Cookie导致的IP丢失问题。
Nginx作为负载均衡层的Proxy Protocol配置
如果你用Nginx做反向代理或者负载均衡,开启Proxy Protocol透传非常简单。在upstream块或者proxy_pass的location块里加上proxy_protocol指令就行:
stream {
upstream backend {
server 10.0.0.1:80;
server 10.0.0.2:80;
}
server {
listen 443;
proxy_pass backend;
proxy_protocol on;
}
}
如果是HTTP层面的反向代理,写法略有不同:
http {
server {
listen 80 proxy_protocol;
location / {
proxy_pass http://backend;
proxy_protocol on;
# 从Proxy Protocol头中提取真实IP
set_real_ip_from 0.0.0.0/0;
real_ip_header proxy_protocol;
}
}
}
注意listen指令后面直接加proxy_protocol参数,表示这个端口接受带Proxy Protocol头的连接。然后real_ip_header proxy_protocol告诉Nginx从Proxy Protocol头里解析真实IP,而不是从TCP连接的对端IP里取。这样你的后端应用拿到的$_SERVER['REMOTE_ADDR']或者X-Forwarded-For就是真实客户端IP了。
HAProxy的Proxy Protocol配置方法
HAProxy是Proxy Protocol的原生支持者,配置更加直观。在frontend和backend部分都需要做设置:
frontend http_front
bind *:80
mode http
default_backend http_back
backend http_back
mode http
# 向后端发送Proxy Protocol v1头
option forwardfor
server web1 10.0.0.1:80 send-proxy-v1
server web2 10.0.0.2:80 send-proxy-v1
send-proxy-v1表示使用v1文本格式,也可以用send-proxy-v2用二进制格式。option forwardfor同时会添加X-Forwarded-For头,双重保障。如果你的后端是HAProxy自己,还需要在backend的bind上加accept-proxy:
backend http_back
mode http
bind 10.0.0.1:80 accept-proxy
accept-proxy告诉这个监听端口要解析进来的Proxy Protocol头,并把解析出的真实IP记录到连接信息中。
Linux内核层面的SYN Cookie参数调优
光配置Proxy Protocol还不够,你还得确保内核的SYN Cookie机制正常工作且不会和Proxy Protocol冲突。核心参数有这几个:
# 开启SYN Cookie net.ipv4.tcp_syncookies = 1 # SYN Cookie的超时时间(秒),默认5秒 net.ipv4.tcp_synack_retries = 2 # 半连接队列最大长度 net.ipv4.tcp_max_syn_backlog = 65536 # 开启SYN Cookie后仍然允许记录时间戳(有助于调试) net.ipv4.tcp_timestamps = 1 # 关闭TCP SACK以减少SYN Cookie计算开销(高防场景可考虑) net.ipv4.tcp_sack = 0
tcp_syncookies设为1是开启SYN Cookie,tcp_synack_retries设为2表示重试两次,加起来最多等约50秒(指数退避)。tcp_max_syn_backlog在开启SYN Cookie后其实意义不大了,因为不维护队列,但设大一点也没坏处。tcp_sack在极端DDoS场景下可以关闭,因为SACK选项会增加SYN包的大小,稍微增加计算量。
后端应用如何正确解析Proxy Protocol获取真实IP
后端应用需要从TCP数据流的最开始读取Proxy Protocol头。不同语言和框架的处理方式不同。
如果你用的是Nginx作为后端Web服务器,前面已经说了用real_ip_header指令。如果你是自己写的应用程序,比如用Go语言:
import (
"bufio"
"net"
"strings"
)
func getRealIP(conn net.Conn) (string, error) {
reader := bufio.NewReader(conn)
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
// 解析 PROXY TCP4/TCP6 ...
parts := strings.Fields(line)
if len(parts) < 6 || parts[0] != "PROXY" {
return "", fmt.Errorf("invalid proxy protocol header")
}
// parts[3] 是客户端真实IP
return parts[3], nil
}
如果是Python的Flask或者Django,可以用类似的方式在WSGI中间件里处理。关键是要在读取任何HTTP数据之前,先把Proxy Protocol头消费掉,否则HTTP解析器会把它当成非法请求。
Proxy Protocol v1和v2的选择建议
v1是纯文本,人类可读,调试方便,但有一个问题:如果客户端真实IP本身包含空格或者特殊字符(虽然IPv4不会),解析可能出问题。v2是二进制格式,支持IPv6、支持携带更多元信息(比如TLS SNI、TLS证书信息),解析效率更高,但不可读。
实际建议:如果你的环境只有IPv4,用v1就够了,简单直观。如果有IPv6需求或者需要传递TLS相关信息,用v2。大部分生产环境用v1已经完全够用,而且排查问题时直接tcpdump抓包就能看到内容。
SYN Cookie和Proxy Protocol配合使用的注意事项
第一,SYN Cookie开启后,内核不会记录对端IP,但Proxy Protocol头是在连接建立后由负载均衡器注入的,所以两者不冲突。但要注意时序:负载均衡器必须等TCP三次握手完成后才能发送Proxy Protocol头,所以负载均衡器和后端之间的连接也需要正常建立,不能对后端也开SYN Cookie(除非你用Proxy Protocol v2 over UDP之类的特殊方案)。
第二,如果你的架构是:客户端 -> CDN/高防IP -> 负载均衡 -> 后端服务器,那么Proxy Protocol头会被CDN或者高防IP那一层注入。你需要确认你的高防服务商或者CDN是否支持Proxy Protocol透传。很多高防IP在清洗流量后会重建TCP连接,这时候Proxy Protocol头可能会丢失或者被覆盖,需要和服务商确认他们是否支持在回源时携带Proxy Protocol。
第三,安全性方面。Proxy Protocol头是可以伪造的,任何人都可以发一行"PROXY TCP4 xxx"来伪装IP。所以后端服务器必须只信任来自已知负载均衡器IP的Proxy Protocol头。在Nginx里可以用set_real_ip_from限定信任的IP段,在自己写的程序里也要做同样的白名单校验。
完整架构示例和最佳实践总结
一个典型的高防架构是这样的:用户 -> 高防清洗节点(开启SYN Cookie)-> HAProxy负载均衡(发送Proxy Protocol v1)-> Nginx/应用服务器(解析Proxy Protocol获取真实IP)。在这个链路中,高防节点用SYN Cookie扛住攻击,HAProxy用Proxy Protocol把真实IP透传下去,后端正常处理业务。
最佳实践总结几点:第一,SYN Cookie只在最外层高防节点开启,内层不要开;第二,Proxy Protocol尽量用v1,简单可靠;第三,后端必须做IP白名单校验,防止伪造;第四,定期用tcpdump抓包验证Proxy Protocol头是否正常到达;第五,日志系统要记录解析后的真实IP,而不是负载均衡器IP,这样出了问题才能追溯。
这套方案在实际生产中已经非常成熟,几乎所有大型互联网公司的DDoS防护体系都是这个思路。核心就是用SYN Cookie解决资源耗尽问题,用Proxy Protocol解决IP丢失问题,两者各司其职、互不干扰。把这两个技术理解透、配置对,你的服务器既能扛住大规模DDoS,又能精确知道每一个请求来自哪里。
