Linux カーネルパラメータに net.core.somaxconn
というものがあり、それと一緒に設定することが多いパラメータに net.ipv4.tcp_max_syn_backlog
というものがある。
そういえば backlog って何なんだろう…となったのでメモ。
backlog とは何か
listen
システムコールの引数には backlog
というものがある。
int listen(int sockfd, int backlog);
listen(2)
によって作られたソケットは backlog というキューを持つことになる。
会社で @hiboma さんに聞いたところ、listen queue と呼ばれることもあるらしい。
これは netstat
で出力される(後述)ところから来ているのかなーとのこと。
backlog には次の2種類がある。
-
SYN を受け取ったときに格納するキュー
-
ACK を受け取ったときに格納するキュー
-
https://wiki.bit-hive.com/linuxkernelmemo/pg/listen%20backlog%20%E3%80%903.6%E3%80%91
では、カーネルのソースコードを追いながら、次のことを確認していく。
- backlog の数以上の接続があった場合にどのようなことが起こるのか
- backlog の数が原因で異常が発生しているとして、どこを確認すればいいのか
ちなみに環境は次の通り。
$ uname -a
Linux ubuntu-bionic 4.15.0-62-generic #69-Ubuntu SMP Wed Sep 4 20:55:53 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
listen(2)
におけるキューの初期化
まずは listen
システムコールから見ていく。
listen(2)
で作成されるキューがいわゆる backlog(listen queue) と呼ばれるもので、ここでは Listen Queue と表す。
対して、後述するが、 accept(2)
によって作られる backlog を Accept Queue と呼ぶことにする。
Linux において、システムコールは SYSCALL_DEFINE2
で定義されるので、 SYSCALL_DEFINE2(listen
などで検索する。
SYSCALL_DEFINE2(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;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
net.ipv4.tcp_max_syn_backlog
が net.core.somaxconn
の値より大きい場合は net.core.somaxconn
の値を優先するのだが、その処理はこのあたりっぽい。
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = sock->ops->listen(sock, backlog);
のところで inet_listen
が呼ばれる。
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err, tcp_fastopen;
...
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
/* Enable TFO w/o requiring TCP_FASTOPEN socket option.
* Note that only TCP sockets (SOCK_STREAM) will reach here.
* Also fastopen backlog may already been set via the option
* because the socket was in TCP_LISTEN state previously but
* was shutdown() rather than close().
*/
tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;
if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
(tcp_fastopen & TFO_SERVER_ENABLE) &&
!inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
fastopen_queue_tune(sk, backlog);
tcp_fastopen_init_key_once(sock_net(sk));
}
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;
err = 0;
...
さらに inet_csk_listen_start
が呼ばれている。
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
int err = -EADDRINUSE;
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_max_ack_backlog = backlog;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
/* There is race window here: we announce ourselves listening,
* but this transition is still not validated by get_port().
* It is OK, because this socket enters to hash table only
* after validation is complete.
*/
sk_state_store(sk, TCP_LISTEN);
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
inet->inet_sport = htons(inet->inet_num);
sk_dst_reset(sk);
err = sk->sk_prot->hash(sk);
if (likely(!err))
return 0;
}
sk->sk_state = TCP_CLOSE;
return err;
}
ここでは reqsk_queue_alloc()
というキューを作成するっぽい関数が呼ばれている。
void reqsk_queue_alloc(struct request_sock_queue *queue)
{
spin_lock_init(&queue->rskq_lock);
spin_lock_init(&queue->fastopenq.lock);
queue->fastopenq.rskq_rst_head = NULL;
queue->fastopenq.rskq_rst_tail = NULL;
queue->fastopenq.qlen = 0;
queue->rskq_accept_head = NULL;
}
コメントにある通り、キューの数は128が最小値らしい。
inet_csk_listen_start()
に戻り、backlog の最大値が設定されている。
sk->sk_max_ack_backlog = backlog;
sk->sk_ack_backlog = 0;
ここまでで Listen Queue のサイズは backlog で指定した値のサイズになることが確認できた。
では SYN パケットを受け取る処理を見ていく。
3-Way Handshake 時、カーネルではどのように遷移しているのかを表した図として、 https://www.tldp.org/HOWTO/KernelAnalysis-HOWTO-8.html が参考になる。
*** 3-Way TCP Handshake ***
|switch(sk->state)
|case TCP_LISTEN: // We received SYN
|conn_request -> tcp_v4_conn_request
|tcp_v4_send_synack // Send SYN + ACK
|tcp_v4_synq_add // set SYN state
|case TCP_SYN_SENT: // we received SYN + ACK
|tcp_rcv_synsent_state_process
tcp_set_state(TCP_ESTABLISHED)
|tcp_send_ack
|tcp_transmit_skb
|queue_xmit -> ip_queue_xmit
|ip_queue_xmit2
|skb->dst->output
|case TCP_SYN_RECV: // We received ACK
|if (ACK)
|tcp_set_state(TCP_ESTABLISHED)
tcp_v4_conn_request
の中では tcp_conn_request
が呼ばれる。
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, 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;
}
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
if (!req)
goto drop;
tcp_rsk(req)->af_specific = af_ops;
tcp_rsk(req)->ts_off = 0;
...
}
ちょっとずつ追っていく。
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
inet_csk_reqsk_queue_is_full()
は次のようになっている。
この関数では Listen Queue がいっぱいになっていないかをチェックしている。
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}
ここで net->ipv4.sysctl_tcp_syncookies == 2
や tcp_syn_flood_action()
関数などがでてきたので、そちらも確認しよう。
これは SYN Flood 対策のためのもので、Listen Queue いっぱいにキューが溜まった場合、新規にキューに突っ込むことができずにプロセスと疎通が取れなくなってしまう。
そこで SYN Cookies と呼ばれるものが作られた。
それを有効/無効にするのが net.ipv4.tcp_syncookies
だ。
tcp_syn_flood_action()
は次のようになっている。
static bool tcp_syn_flood_action(const struct sock *sk,
const struct sk_buff *skb,
const char *proto)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
const char *msg = "Dropping request";
bool want_cookie = false;
struct net *net = sock_net(sk);
#ifdef CONFIG_SYN_COOKIES
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)
pr_info("%s: Possible SYN flooding on port %d. %s. Check SNMP counters.\n",
proto, ntohs(tcp_hdr(skb)->dest), msg);
return want_cookie;
}
net.ipv4.tcp_syncookies
の値が0の場合はそのまま破棄され、1の場合は syn cookie 付きの SYN/ACK を返している。
というわけで次のことがわかった。
net.ipv4.tcp_syncookies=0
のときに Listen Queue が一杯だと破棄net.ipv4.tcp_syncookies=1
のときは SYN Flood 攻撃と判断し SYN Cookies 付きのパケットを送り返す
tcp_conn_request
に戻って次の処理へ。
Accept Queue がいっぱいの場合は先程同様 DROP する。
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
ここを Pass するとSYN/ACK を送信して Accept Queue に加える。
af_ops->send_synack(fastopen_sk, dst, &fl, req,
&foc, TCP_SYNACK_FASTOPEN);
/* Add the child socket directly into the accept queue */
inet_csk_reqsk_queue_add(sk, req, fastopen_sk);
ちなみに drop
は次のような処理になっている。
drop:
tcp_listendrop(sk);
return 0;
static inline void tcp_listendrop(const struct sock *sk)
{
atomic_inc(&((struct sock *)sk)->sk_drops);
__NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENDROPS);
}
netstat でいう TcpExtListenDrops
を増加させるらしい。
ここまでで、次のような流れを確認できた。
- SYN を受け取ると listen queue へ格納する
- SYN/ACK を返すと同時に accept queue へ移動する
現在、クライアントは SYN_SENT, サーバーは SYN_RECV の状態にある。
では、クライアントから ACK を受け取ったときの処理を追いかけていく。
tcp_v4_do_recv()
をみていく。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
tcp_rcv_established(sk, skb, tcp_hdr(skb));
return 0;
}
if (tcp_checksum_complete(skb))
goto csum_err;
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_cookie_check(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb);
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
tcp_rcv_state_process()
へ。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
if (req) {
WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
sk->sk_state != TCP_FIN_WAIT1);
if (!tcp_check_req(sk, skb, req, true))
goto discard;
}
...
tcp_check_req()
へ。
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen)
{
/* OK, ACK is valid, create big socket and
* feed this segment to it. It will repeat all
* the tests. THIS SEGMENT MUST MOVE SOCKET TO
* ESTABLISHED STATE. If it will be dropped after
* socket is created, wait for troubles.
*/
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
req, &own_req);
if (!child)
goto listen_overflow;
sock_rps_save_rxhash(child, skb);
tcp_synack_rtt_meas(child, req);
return inet_csk_complete_hashdance(sk, child, req, own_req);
...
ここでも tcp_v4_syn_recv_sock()->sk_acceptq_is_full()
が呼ばれている。
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req)
{
struct inet_request_sock *ireq;
struct inet_sock *newinet;
struct tcp_sock *newtp;
struct sock *newsk;
#ifdef CONFIG_TCP_MD5SIG
struct tcp_md5sig_key *key;
#endif
struct ip_options_rcu *inet_opt;
if (sk_acceptq_is_full(sk))
goto exit_overflow;
キューがいっぱいの場合は exit_overflow
へ移るようだ。
exit_overflow:
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
これは netstat
で確認できる ListenOverflows
の値を増加させている。
ここの定義を見ると netstat -s
の意味が理解できて便利。
最終的に呼び出し元の tcp_check_req()
では listen_overflow
へ処理が移る。
listen_overflow:
if (!sock_net(sk)->ipv4.sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}
これは net.ipv4.tcp_abort_on_overflow=1
の場合に RST を送るという処理になる。
もし 0
であれば、何もしない。
では次に ACK を受け取って以降の処理、つまり、 accpet(2)
の処理を見ていく。
accept(2)
inet_csk_accept()
関数を見ていく。
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
reqsk_queue_remove()
でキューから削除している。
ここまでのまとめ
- TCP ソケットは backlog というキューを持つ
- SYN を受け取ったときに格納する Listen Queue と ACK を受け取ったときの Accept Queue がある
- キューの実装は https://elixir.bootlin.com/linux/v4.15/source/include/net/request_sock.h#L50
- SYN_RECV, ESTABLISHED な状態がキューに入っている
accept(2)
されるとキューから削除される
backlog の数以上の接続があった場合
では backlog 以上の接続が発生した場合どうなるのだろうか。
netcat
は backlog=1 のため、これで確認してみる。
$ strace nc -lvp 12345
...
bind(3, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1) = 0
write(2, "Listening on [0.0.0.0] (family 0"..., 46Listening on [0.0.0.0] (family 0, port 12345)
) = 46
...
ss -tan
で確認すると Send-Q
が 1 になっている。ここは backlog の数を示すらしい。
$ ss -tan
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 5 127.0.0.1:631 0.0.0.0:*
LISTEN 0 128 0.0.0.0:8088 0.0.0.0:*
LISTEN 0 1 0.0.0.0:12345 0.0.0.0:*
10 並列でつないでみる。
$ for i in `seq 10`; do echo '12345'; done | xargs -t -P 10 -n 1 nc localhost
ss -tan
の結果、Recv-Q は2となっている。
$ ss -tan | grep 12345
LISTEN 2 1 0.0.0.0:12345 0.0.0.0:*
sk_acceptq_is_full()
は次のような条件式のため、backlog + 1 だけキューを持てることになり、Recv-Q は 2 になっている。
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
そのため、キューが2なので10並列で接続しても Recv-Q はこれ以上増えない。
パケットのドロップを確認する
キューがいっぱいの時は LINUX_MIB_LISTENOVERFLOWS
と LINUX_MIB_LISTENDROPS
が増加することが分かっているので、それを確認してみる。
現在のカウントは91となっている。
$ nstat -az TcpExtListenDrops
#kernel
TcpExtListenDrops 91 0.0
$ nstat -az TcpExtListenOverflows
#kernel
TcpExtListenOverflows 91 0.0
ここで先程同様に backlog=1 なプロセスに対して複数の接続を行う。
すると119に増加していることが確認できた。
$ nstat -az TcpExtListenOverflows
#kernel
TcpExtListenOverflows 119 0.0
$ nstat -az TcpExtListenDrops
#kernel
TcpExtListenDrops 119 0.0
ESTABLISHED なクライアントの数だけ再送されて、この数がその分増加される。
tcpdump で確認すると確かに SYN パケットが送信されているが、レスポンスはない。
localhost.46684 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0x4251), seq 3634709843, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0
12:20:12.260146 IP (tos 0x0, ttl 64, id 46559, offset 0, flags [DF], proto TCP (6), length 60)
localhost.46678 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0xfe00), seq 3475804962, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0
12:20:12.260150 IP (tos 0x0, ttl 64, id 40247, offset 0, flags [DF], proto TCP (6), length 60)
localhost.46674 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0x9eb9), seq 97958852, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0
Ref
- https://blog.cloudflare.com/syn-packet-handling-in-the-wild/
- https://kazuhira-r.hatenablog.com/entry/2019/07/10/015733
- http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html
- https://wiki.bit-hive.com/linuxkernelmemo/pg/listen%20backlog%20%E3%80%903.6%E3%80%91
- https://jin-yang.github.io/post/network-synack-queue.html
- https://www.tldp.org/HOWTO/KernelAnalysis-HOWTO-8.html
- https://hiboma.hatenadiary.jp/entry/2016/11/29/121205
- https://github.com/hiboma/hiboma/blob/master/kernel/net/net-backlog.md
- https://blog.csdn.net/zhangskd/article/details/16987637
- https://github.com/ton31337/tools/wiki/Is-net.ipv4.tcp_abort_on_overflow-good-or-not%3F