标准答案未必是正确答案
基于 packetdrill TCP 三次握手脚本,研究下 TCP 三次握手过程中的 SYN Cookies ,此次构造模拟的是服务器端场景。
基础脚本
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
#
半连接和全连接队列
半连接队列,即 SYN 队列,全连接队列,即 Accept 队列。服务器在收到客户端发送的 SYN 数据包后,会将该连接存储到 SYN 队列,并向客户端发送 SYN/ACK,客户端在收到后发送 ACK,而服务端在收到 ACK 后,会将该连接从半连接队列里移除,然后创建一个完全新的连接,并将其添加到 Accept 队列,之后等待应用进程调用 accept() 函数把连接取出来。
半连接和全连接队列均有最大长度限制,在超过限制时,内核会将 SYN 丢弃或响应 RST。
服务器在收到 SYN 数据包的处理过程,首先判断如果没有开启 tcp_syncookies ,且半连接队列溢出,则丢弃。半连接队列溢出的判断条件是半连接队列大小是否大于等于全连接队列最大值,而全连接队列溢出的判断条件是全连接队列大小是否大于全连接队列最大值。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
sk_max_ack_backlog
的设置,主要是应用通过 listen() 系统调用指定,并与 sysctl_somaxconn 值比较,取小后最终赋值给 sk_max_ack_backlog。
listen(sockfd, backlog)
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
return __sys_listen(fd, backlog);
}
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
...
}
int inet_listen(struct socket *sock, int backlog)
{
...
WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
...
}
对于 TCP 半连接,也就是服务器处于SYN_RECV
状态的 TCP 连接,可以通过 ss 命令查看:
查看处于半连接状态的TCP连接
# ss -anto | grep SYN-RECV
#
统计半连接状态的TCP连接数量
# ss -anto | grep SYN-RECV | wc -l
#
对于 TCP 全连接,同样可以通过 ss 命令查看,但需要注意的是 Listen
状态下表达的 Recv-Q/Send-Q
含义。
Recv-Q:目前全连接队列大小,为已完成三次握手并等待应用 accept() 的连接; Send-Q:目前全连接队列最大值。
# ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 01280.0.0.0:220.0.0.0:*
#
SYN Cookies
关于 tcp_syncookies 的说明如下,默认值为 1 ,即当 socket 的 syn backlog 队列溢出时,才发送 syncookies ,而值 0 则是关闭 TCP syncookies,值 2 则是无条件发送 syncookies 。
tcp_syncookies (integer; default: 1; since Linux 2.2)
Enable TCP syncookies. The kernel must be compiled with
CONFIG_SYN_COOKIES. The syncookies feature attempts to
protect a socket from a SYN flood attack. This should be
used as a last resort, if at all. This is a violation of
the TCP protocol, and conflicts with other areas of TCP
such as TCP extensions. It can cause problems for clients
and relays. It isnot recommended as a tuning mechanism
for heavily loaded servers to help with overloaded or
misconfigured conditions. For recommended alternatives
see tcp_max_syn_backlog, tcp_synack_retries, and
tcp_abort_on_overflow. Set to one of the following
values:
0 Disable TCP syncookies.
1 Send out syncookies when the syn backlog queue of a
socket overflows.
2 (since Linux 3.12) Send out syncookies
unconditionally. This can be useful for network
testing.
通过 sysctl 可查看或更改参数值,之前在中均是关闭了 tcp_syncookies,也就是修改值 为 0 。
# sysctl -a|grep syncookies
net.ipv4.tcp_syncookies = 1
#
# sysctl -q net.ipv4.tcp_syncookies=0
# sysctl -a|grep syncookies
net.ipv4.tcp_syncookies = 0
#
# sysctl -q net.ipv4.tcp_syncookies=2
# sysctl -a|grep syncookies
net.ipv4.tcp_syncookies = 2
#
实验测试一
当 tcp_syncookies 为默认值 1 时,如果半连接队列溢出且没有 ISN 时,则调用 tcp_syn_flood_action() 判断需要开启 syncookies 功能,设置 want_cookie = true 。
int tcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
...
}
/*
* Return true if a syncookie should be sent
*/
staticbool tcp_syn_flood_action(conststruct sock *sk, constchar *proto)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
constchar *msg = "Dropping request";
bool want_cookie = false;
struct net *net = sock_net(sk);
if (net->ipv4.sysctl_tcp_syncookies) {
msg = "Sending cookies";
want_cookie = true;
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
} else
#endif
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);
if (!queue->synflood_warned &&
net->ipv4.sysctl_tcp_syncookies != 2 &&
xchg(&queue->synflood_warned, 1) == 0)
net_info_ratelimited("%s: Possible SYN flooding on port %d. %s. Check SNMP counters.n",
proto, sk->sk_num, msg);
return want_cookie;
}
也就是说当半连接队列满时,服务器在收到 SYN 请求后,不再将请求放入半连接队列,而是生成一个 SYN Cookie 并将其编码到 SYN/ACK 中,客户端收到 SYN/ACK后会响应 ACK ,服务器收到 ACK 之后,通过解码和验证 SYN Cookie 来确认请求的合法性,并恢复必要的状态。如果验证通过,服务器完成连接的建立,将连接放入 Accept 队列,供应用层处理,而无需使用半连接队列。SYN Cookies 机制可有效防止 SYN 队列耗尽导致的拒绝服务攻击。
无时间戳
简单来说是在客户端或者服务器端不支持时间戳的情况下,服务器端根据 SYN Cookie 算法构造 ISN (但没有更多的空间存储类似 SACK 和 Wscale 等 TCP 选项), 假如 SYN/ACK Seq Num 为 x ,那么客户端能正常响应 ACK (ACK Num 即为 x + 1),那么服务器在收到 ACK ,经验证 SYN Cookie 通过后,完成后续连接建立过程。
基于 TCP 三次握手脚本,listen 设置的 backlog 为 1,远小于 somaxconn 4096,因此 packetdrill 脚本所模拟的全连接队列最大值为 1。因需模拟服务器端处于 SYN_RECV 状态,所以不发送第三个 ACK 即可,同时关闭时间戳选项。
`sysctl -q net.ipv4.tcp_timestamps=0`
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0 `sleep 1000`
#
# sysctl -a|grep maxconn
net.core.somaxconn = 4096
#
通过 ss 可查询到 SYN-RECV 连接状态,数量 1 。
# packetdrill tcp_syncookies_001.pkt
#
# ss -anto | grep 8080
LISTEN 0 1 192.168.222.128:8080 0.0.0.0:*
SYN-RECV 0 0 192.168.222.128:8080 192.0.2.1:43321 timer:(on,1.236ms,1)
# ss -anto | grep SYN-RECV
SYN-RECV 0 0 192.168.222.128:8080 192.0.2.1:43321 timer:(on,2.428ms,2)
# ss -anto | grep SYN-RECV | wc -l
1
#
通过 tcpdump 捕获数据包,可以看到因为收不到 ACK,所以 SYN/ACK 不断重传,总计 5 次。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
0
那么可以在 packetdrill 脚本重传第 5 个 SYN/ACK 时,尝试以 telnet IP+端口的方式发起连接,会发现此 SYN 可以正常建立连接,因为此时在半连接队列满的情况下开启了 SYN Cookie 功能。
然后通过 ss 不断查询 packetdrill 脚本中原有连接的状态,在 SYN/ACK 最后一次重传超时后,原有半连接释放,此时再次尝试以 telnet IP+端口的方式发起连接,同样正常建立连接,因为此时半连接队列为空。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
1
通过 tcpdump 捕获数据包,总共 13 个,No.1-7 是原始连接,No.8-10 是第一次 telnet,No.11-13 是第二次 telnet 。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
2
通过 wireshark 分析数据包,可以看到同样是 telnet 方式,对于 No.9 是通过 SYN Cookie 响应 SYN/ACK,但是在没有时间戳的情况下没法保存部分 TCP 选项信息,比如 Wscale
和 SACK
,所以 SYN/ACK 也相应不支持。而 No.12 是正常建连,服务器响应 SYN/ACK,则包含 Wscale
和 SACK
。
有时间戳
简单来说是在客户端和服务器端均支持时间戳的情况下,服务器端根据 SYN Cookie 算法构造 ISN ,同时将客户端 SYN 中的部分 TCP 选项编码在 TS val
字段中(方法是使用后六位编码 Wscale
、SACK
和ECN
)。 假如 SYN/ACK Seq Num 为 x ,那么客户端能正常响应 ACK(ACK Num 即为 x + 1,并且 TS ecr
和之前的 TS val
字段相同),那么服务器在收到 ACK ,经验证 SYN Cookie 通过后,完成后续连接建立过程。
基于 TCP 三次握手脚本,listen 设置的 backlog 为 1,远小于 somaxconn 4096,因此 packetdrill 脚本所模拟的全连接队列最大值为 1。因需模拟服务器端处于 SYN_RECV 状态,所以不发送第三个 ACK 即可,同时开启时间戳选项。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
3
通过 ss 可查询到 SYN-RECV 连接状态,数量 1 。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
4
通过 tcpdump 捕获数据包,可以看到因为收不到 ACK,所以 SYN/ACK 不断重传,总计 5 次。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
0
那么可以在 packetdrill 脚本重传第 5 个 SYN/ACK 时,尝试以 telnet IP+端口的方式发起连接,会发现此 SYN 可以正常建立连接,因为此时在半连接队列满的情况下开启了 SYN Cookie 功能。
然后通过 ss 不断查询 packetdrill 脚本中原有连接的状态,在 SYN/ACK 最后一次重传超时后,原有半连接释放,此时再次尝试以 telnet IP+端口的方式发起连接,同样正常建立连接,因为此时半连接队列为空。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
6
通过 tcpdump 捕获数据包,总共 13 个,No.1-7 是原始连接,No.8-10 是第一次 telnet,No.11-13 是第二次 telnet 。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
7
通过 wireshark 分析数据包,在有时间戳支持的情况下,对于 No.9 通过 SYN Cookie 机制响应 SYN/ACK,与 No.12 正常建连,服务器响应的 SYN/ACK,基本无差别,包括 Wscale
、SACK
等选项均支持。
实验测试二
当 tcp_syncookies 值为 2 时,则直接调用 tcp_syn_flood_action() ,判断需要开启 syncookies 功能,设置 want_cookie = true 。
int tcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
...
}
/*
* Return true if a syncookie should be sent
*/
staticbool tcp_syn_flood_action(conststruct sock *sk, constchar *proto)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
constchar *msg = "Dropping request";
bool want_cookie = false;
struct net *net = sock_net(sk);
if (net->ipv4.sysctl_tcp_syncookies) {
msg = "Sending cookies";
want_cookie = true;
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
} else
#endif
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);
if (!queue->synflood_warned &&
net->ipv4.sysctl_tcp_syncookies != 2 &&
xchg(&queue->synflood_warned, 1) == 0)
net_info_ratelimited("%s: Possible SYN flooding on port %d. %s. Check SNMP counters.n",
proto, sk->sk_num, msg);
return want_cookie;
}
也就是说无条件开启 SYN Cookies 功能,服务器在收到 SYN 请求后,不再将请求放入半连接队列,而是生成一个 SYN Cookie 并将其编码到 SYN/ACK 中,客户端收到 SYN/ACK后会响应 ACK ,服务器收到 ACK 之后,通过解码和验证 SYN Cookie 来确认请求的合法性,并恢复必要的状态。如果验证通过,服务器完成连接的建立,将连接放入 Accept 队列,供应用层处理,而无需使用半连接队列。SYN Cookies 机制可有效防止 SYN 队列耗尽导致的拒绝服务攻击。
修改 tcp_syncookies 值为 2 和 tcp_timestamps 值为 1 ,如下。
inttcp_conn_request(struct request_sock_ops *rsk_ops,
conststruct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
...
if (!want_cookie && !isn) {
/* Kill the following clause, if you dislike this way. */
if (!net->ipv4.sysctl_tcp_syncookies &&
(net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(net->ipv4.sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
...
}
检查半连接队列大小是否大于等于全连接队列最大值
static inline intinet_csk_reqsk_queue_is_full(conststruct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
检查全连接队列大小是否大于全连接队列最大值
static inline boolsk_acceptq_is_full(conststruct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
9
脚本修改如下。
listen(sockfd, backlog)
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
return __sys_listen(fd, backlog);
}
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
...
}
int inet_listen(struct socket *sock, int backlog)
{
...
WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
...
}
0
执行脚本,可通过 nstat 查询发送 SYN Cookie 的数量变化。
listen(sockfd, backlog)
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
return __sys_listen(fd, backlog);
}
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
...
}
int inet_listen(struct socket *sock, int backlog)
{
...
WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
...
}
1
往期推荐
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...