localhost kernel: possible SYN flooding on port 8888. Sending cookies.

/var/log/messages 에 이와 비슷한 로그들이 남고 있다.

그리고 이 포트는 내가 짠 웹서버가 listen 중인 포트 =_=;.. 내부 네트웍이라 ddos같은 공격은 아닐텐데 이런 로그가 왜 찍히는지 조금 더 찾아봤다. 결론을 내 보면 다음과 같다.


1) 위 로그는 왜 생기나 : 클라이언트 쪽에서 서버의 tcp listen queue 사이즈에 비해 너무 많은 tcp 연결 요청을 하고 있다.

2) 서버 관점 방안 : 코드에서는 socket listen()을 호출할 때 backlog 인자값을 늘리고, 시스템에서는 커널 파라미터 somaxconn과 syn_backlog 값을 늘려준다. tcp listen queue 사이즈는, 좀전에 말한 세가지 요인 중 가장 작은값에 따라 결정되기 때문이다. 커널 주석에서는 syn_backlog 파라미터 값으로 시스템 메모리가 256MB 이상이라면 1024를 권장한다. somaxconn 파라미터 값에 대해서는, 구글링 해보니 그냥 1024로 늘려준 케이스도 많고, somaxconn 값을 늘리기 전에 어플리케이션 단의 성능 문제가 원인은 아닐지 먼저 찾아보라는 조언 도 있다.

3) 클라이언트 관점 방안 : 매 메시지마다 connection을 맺고 끊는 구조를 피한다. 애초에 persistent connection 으로 만들어서, 3-way handshake 는 처음 한 번만 일어나게 하자.


아래는 코드 단위로 찾아간 세부 과정이라 지루한 내용이 될 수 있다.

코드 찾아보기

linux 2.6.32 버전 커널 소스에서 문제의 시스템 로그를 찍는 부분을 찾아봤다.

/net/ipv4/tcp_ipv4.c syn_flood_warning() 때문에 시스템 로그가 찍힌거고, 이 함수는 아래 코드의 tcp_v4_conn_request() 함수 안에서 want_cookie 변수값이 1이라 불린 거다.

int tcp_v4_conn_request(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 (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
    if (sysctl_tcp_syncookies) {
      want_cookie = 1;
    } else
#endif
    goto drop;
  }
	...
  if (want_cookie) {
#ifdef CONFIG_SYN_COOKIES
    syn_flood_warning(skb);
    req->cookie_ts = tmp_opt.tstamp_ok;
#endif
    isn = cookie_v4_init_sequence(sk, skb, &req->mss);
  }
...
  return 0;
}


want_cookie 가 1이 된 이유는 inet_csk_reqsk_queue_is_full() 에 걸려서 그런거다. 이 코드는

include/net/inet_connection_sock.h

static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
	return reqsk_queue_is_full(&inet_csk(sk)->icsk_accept_queue);
}


reqsk_queue_is_full 이 불린거고

include/net/request_sock.h

static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
  return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}

현재 listen_opt 큐의 메시지 수를 max_qlen_log 만큼 비트쉬프트 하고, 그 결과가 0보다 크면 queue full로 판단, 아니면 버틸만하다고 판단.


max_qlen_log 는 주석에서 다음과 같이 설명하고 있다.

include/net/request_sock.h

/** struct listen_sock - listen state
 *
 * @max_qlen_log - log_2 of maximal queued SYNs/REQUESTs
 */

max_qlen_log는 SYN 이나 REQUEST 를 받을 수 있는 큐 맥스값의 로그 2 값. 왜 (현재 listen 큐에 쌓인 메시지수 >= MAX)로 비교하는게 아니라, 굳이 (listen 큐 메시지수 » log2 of MAX) 같은 비트 쉬프트 연산으로 비교를 하는걸까..? 개인적인 생각은 비트쉬프트 연산 속도가 더 빨라 그럴 것 같은데 정확히는 모르겠음.

max_qlen_log 는 어디서 세팅되는지 찾아보면,


net/core/request_sock.c

int reqsk_queue_alloc(struct request_sock_queue *queue,
          unsigned int nr_table_entries)
{
  size_t lopt_size = sizeof(struct listen_sock);
  struct listen_sock *lopt;

  nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
  nr_table_entries = max_t(u32, nr_table_entries, 8);
  nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);

...  

  for (lopt->max_qlen_log = 3;
       (1 << lopt->max_qlen_log) < nr_table_entries;
       lopt->max_qlen_log++);
...

max_qlen_log 는 실제 MAX값에 로그2를 적용시킨 값이니까 실제 MAX 값이 뭔지를 찾아야 하는데, 위의 함수를 보면 nr_table_entries 가 실제 MAX 값인걸로 보인다. for 루프를 돌면서 max_qlen_log 값을 하나씩 증가시켜 나가는데 루프를 도는 횟수에 nr_table_entries 가 영향을 준다.

코드 윗쪽에서 nr_table_entries 이 어떻게 결정되는지 보면

1) 파라미터로 들어온 nr_table_entriessysctl_max_syn_backlog 중 작은값

2) 1)의 결과값과 8 중 큰값

3) 2)의 결과값에 1을 더한 수를 2의 제곱수 단위로 올림한 값

의 순서대로 결정. 여기서 커널 파라미터 systcl_max_syn_backlog 설정값이 백로그 큐 사이즈 최대값에 영향을 준다는 것을 알 수 있고, 최소 백로그 큐 사이즈가 8은 된다는 것을 알 수 있다.

파라미터로 들어온 nr_table_entries 를 세팅하는 부분을 찾기 위해, 위 함수 reqsk_queue_alloc 이 불리는 부분을 찾아보자.

net/ipv4/inet_connection_sock.c inet_csk_listen_start()

int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
  struct inet_sock *inet = inet_sk(sk);
  struct inet_connection_sock *icsk = inet_csk(sk);
  int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
	...
}

위의 함수는 누가 부르냐면,

net/ipv4/af_inet.c

inet_listen 이 부른다.

/*
 *  Move a socket into listening state.
 */
int inet_listen(struct socket *sock, int backlog)
{
	...

  /* Really, if the socket is already in listen state
   * we can only allow the backlog to be adjusted.
   */
  if (old_state != TCP_LISTEN) {
    err = inet_csk_listen_start(sk, backlog);
    if (err)
      goto out;
  }
	...
}

아래 매핑테이블로 인해 sock->ops->listeninet_listen 함수가 매핑 되는 것 같다. 매핑이 정확히 어떻게 되는지는 아직 이해 못하겠어서ㅠ 다른 블로그 도움을 받았다.

net/ipv4/af_inet.c

const struct proto_ops inet_stream_ops = {
...
  .listen      = inet_listen,
...
};
EXPORT_SYMBOL(inet_stream_ops);

한편 이 sock->ops->listen() 이 불리는 건 listen() system call 에서 불린다. 서버 소켓을 만들 때 쓰는 그 listen 함수!

net/socket.c

/*
 *  Perform a listen. Basically, we allow the protocol to do anything
 *  necessary for a listen, and if that works, we mark the socket as
 *  ready for listening.
 */

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)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;
}

코드에서 sock->ops->listen 을 부를 때 주는 파라미터인 backlog 의 값을 정할 때 somaxconn 과 인자로 들어온 backlog 값 중 작은값을 넣어주게 되어 있다. 여기서 시스템 파라미터의 so_maxconn 설정값이 관여하겠구나..

다시 nr_table_entries를 결정하는 함수인 reqsk_queue_alloc() 함수로 돌아가보면, sysctl_max_syn_backlog 에 대해 주석이 다음과 같이 말하고 있음.

/*
 * Maximum number of SYN_RECV sockets in queue per LISTEN socket.
 * One SYN_RECV socket costs about 80bytes on a 32bit machine.
 * It would be better to replace it with a global counter for all sockets
 * but then some measure against one socket starving all other sockets
 * would be needed.
 *
 * It was 128 by default. Experiments with real servers show, that
 * it is absolutely not enough even at 100conn/sec. 256 cures most
 * of problems. This value is adjusted to 128 for very small machines
 * (<=32Mb of memory) and to 1024 on normal or better ones (>=256Mb).
 * Note : Dont forget somaxconn that may limit backlog too.
 */
int sysctl_max_syn_backlog = 256;

int reqsk_queue_alloc(struct request_sock_queue *queue,
          unsigned int nr_table_entries)
{
...
  nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
...
  return 0;
}

감사하게도 주석에 connection을 맺는 성능과 backlog 값 관계에 대한 지침이 있다. max_syn_backlog 커널 파라미터 값을 128 로 해놓으면 1초에 100개의 연결 요청을 처리하기도 시원치 않고, 256 으로 세팅해 놓으면 초당 100개의 연결 요청 정도는 처리할 수 있고, 128 이라는 값은 메모리 크기가 매우 작은 머신(<=32MB) 기준으로 설정해 놓은 것이라고..


결론

(맨 처음에 써놓긴 했지만..)

1) sync flood 시스템 로그가 찍히는 이유는, 서버 소켓의 listen 용 queue에 들어온 메시지 수가 ‘MAX 설정해놓은 값에 1 더한값’ 을 2제곱수로 올림한 값을 넘어갔기 때문

2) MAX 설정해놓은 값은 다음 값들 중 제일 작은값에 의해 결정된다.

  • 서버 코드에서 호출한 socket api : listen() 의 int backlog 파라미터 값

  • 커널 파라미터 somaxconn (sysctl 로 조정 가능)

  • 커널 파라미터 tcp_max_syn_backlog(sysctl로 조정가능)

3) 커널 주석에서는 MAX 설정해놓을 값과 connection 성능 관계에 대해 다음과 같이 언급.

  • 128 이면 100 connection/second 를 만족하기 어려움.

  • 256 이면 100 connection/second 를 대부분 만족.

  • 256MB 이상의 메모리 사양이라면 tcp_max_syn_backlog 파라미터 값은 1024 권장.


참조 링크

backlog 사이즈 관련 코드 분석을 하면서, 이 링크에서 매우 잘 정리되어 있어 도움을 많이 받았다.

syn_backlog와 somaxconn 파라미터(한글)

All you need to know about SYN floods(영문)