본문 바로가기
  • Vetheuil in Summer
Tech/TCP IP

TCP Time-Wait & TCP Port number reused

by 눈꽃산행 2021. 9. 2.

세션을 담당하는 보안장비(방화벽)의 세션관리 및 보안파라메터를 통해 악의적인 세션을 차단하는 방법으로

기존 세션이 유지되는 상태에서 동일한 세션 ( tcp /src ip / src port / dst ip / dst port ) 으로  Syn ( First Packet )

 패킷  Inbound 시 Drop.

- 전송지연 또는 미아세션이 발생하여 출발지의 Local 포트 재 사용시 Reused 포트 사용으로 인해 간헐적으로 차단 패킷이 발생할 가능성

- Server(Client) To Server(Server) 통신시 패킷분석을 통해 TCP Port number reused가 발생빈도가 높다면 추가 분석이 필요. 

 

TIME_WAIT 상태가 왜 필요하고, 왜 그렇게 길게 설정되어 있는지 이유를 살펴보도록 한다. 만일 TIME_WAIT이 짧다면 아래와 같은 두 가지 문제2가 발생한다.

첫 번째는 지연 패킷이 발생할 경우다.

이미 다른 연결로 진행되었다면 지연 패킷이 뒤늦게 도달해 문제가 발생한다. 매우 드문 경우이긴 하나 때마침 SEQ까지 동일하다면 잘못된 데이타를 처리하게 되고 데이타 무결성 문제가 발생한다.

두 번째는 원격 종단의 연결이 닫혔는지 확인해야할 경우다.

마지막 ACK 유실시 상대방은 LAST_ACK 상태에 빠지게 되고 새로운 SYN 패킷 전달시 RST를 리턴한다. 새로운 연결은 오류를 내며 실패한다. 이미 연결을 시도한 상태이기 때문에 상대방에게 접속 오류 메시지가 출력될 것이다.

따라서 반드시 TIME_WAIT이 일정 시간 남아 있어서 패킷의 오동작을 막아야 한다.


RFC 793 에는 TIME_WAIT을 2 MSL로 규정했으며 CentOS 6에서는 60초 동안 유지된다. 아울러 이 값은 조정할 수 없다.

틀린 정보
net.ipv4.tcp_fin_timeout 을 설정하면 TIME_WAIT 타임아웃을 변경할 수 있다.

TIME_WAIT의 타임아웃 정보는 커널 헤더 include/net/tcp.h 에 하드 코딩 되어 있으며 변경이 불가능하다.

// include/net/tcp.h #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */

 

 

[root@oivlxwsdbs-mtrg_nms ~]#
[root@oivlxwsdbs-mtrg_nms ~]#
[root@oivlxwsdbs-mtrg_nms ~]# sysctl net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 0
[root@oivlxwsdbs-mtrg_nms ~]#
[root@oivlxwsdbs-mtrg_nms ~]#
[root@oivlxwsdbs-mtrg_nms ~]# netstat -anpo | grep TIME_WAIT
tcp        0      0 192.168.1.251:10050     192.168.253.100:34662   TIME_WAIT   -                    timewait (24.36/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:34638   TIME_WAIT   -                    timewait (20.25/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:59214   TIME_WAIT   -                    timewait (59.64/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:54392   TIME_WAIT   -                    timewait (35.64/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:34624   TIME_WAIT   -                    timewait (18.99/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:59080   TIME_WAIT   -                    timewait (28.00/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:49488   TIME_WAIT   -                    timewait (49.68/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:59078   TIME_WAIT   -                    timewait (26.09/0/0)
tcp        0      0 192.168.1.251:10050     192.168.253.100:49484   TIME_WAIT   -                    timewait (48.28/0/0)

 

샘플 서버

지난 번과 달리 이번에는 소켓의 로우 레벨까지 확인하기 위해 샘플 서버 프로그램을 C 로 구현했다. 리눅스를 포함한 모든 유닉스 기반 OS 의 API 가 C 로 구현되어 있고 특히 네트워크 프로그램에서 커널의 동작과 C 의 어플리케이션 API 는 정확히 1:1 로 대응 된다. 따라서 구체적으로 네트워크가 어떻게 동작하는지를 C 로 직접 구현해 하나씩 확인해보도록 한다.

먼저 서버 프로그램은 예전에 커넥션 테스트 용도로 개발해 깃헙에 공개한 CONTEST 서버를 기반으로 일부 코드를 추가해서 구현했다.

#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <time.h> #include <pthread.h> #include <net/if.h> #include <sys/ioctl.h> #define BACKLOG 128 // backlog for listen() #define DEFAULT_PORT 5000 // default listening port #define BUF_SIZE 1024 // message chunk size time_t ticks; typedef struct { int client_sock; } thread_param_t; thread_param_t *params; // params structure for pthread pthread_t thread_id; // thread id void error(char *msg) { perror(msg); exit(EXIT_FAILURE); } /** * thread handler after accept() */ void *handle_client(void *params) { char msg[BUF_SIZE]; thread_param_t *p = (thread_param_t *) params; // thread params // clear message buffer memset(msg, 0, sizeof(msg)); ticks = time(NULL); snprintf(msg, sizeof(msg), "%.24s\r\n", ctime(&ticks)); write(p->client_sock, msg, strlen(msg)); printf("Sent message to Client #%d\n", p->client_sock); // wait usleep(10); // clear message buffer memset(msg, 0, sizeof(msg)); // active close, It will remains in `TIME_WAIT` state. if (close(p->client_sock) < 0) error("Error: close()"); free(p); return NULL; } int main(int argc, char *argv[]) { int listenfd = 0, connfd = 0; struct sockaddr_in serv_addr; struct ifreq ifr; // build server's internet addr and initialize memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; // an internet addr serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // accept any interfaces serv_addr.sin_port = htons(DEFAULT_PORT); // the port we will listen on // create the socket, bind, listen if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) error("Error: socket()"); if (bind(listenfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("Error: bind() Not enough privilleges(<1024) or already in use"); if (listen(listenfd, BACKLOG) < 0) error("Error: listen()"); ifr.ifr_addr.sa_family = AF_INET; // an internet addr strncpy(ifr.ifr_name, "eth0", IFNAMSIZ-1); // IP address attached to "eth0" ioctl(listenfd, SIOCGIFADDR, &ifr); // get ifnet address // print <IP>:<Port> printf("Listening on %s:%d\n", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), DEFAULT_PORT); while (1) { connfd = accept(listenfd, (struct sockaddr *) NULL, NULL); // memory allocation for `thread_param_t` struct params = malloc(sizeof(thread_param_t)); params->client_sock = connfd; // 1 client, 1 thread pthread_create(&thread_id, NULL, handle_client, (void *) params); pthread_detach(thread_id); } }

Java에 비해 코드는 훨씬 더 길지만 동작 방식에는 큰 차이가 없다. 마찬가지로 서버가 먼저 close()를 시도하는 점도 동일하다. 즉, 서버측이 Active Close가 되고 TIME_WAIT 상태에 빠지게된다.

서버 방식

서버의 동작은 socket() - bind() - listen() - accept() 과정을 거쳐 클라이언트와 연결되며 위 코드에는 그 과정이 상세히 잘 나와 있다.

accept() 이후 서버는 listen() 중인 소켓과 별도로 accept() 소켓을 추가로 생성해 클라이언트를 할당한다. 서버가 클라이언트를 관리하는 방식은 크게 4가지로 구분할 수 있다.3

  1. 요청 당 프로세스 할당, fork()로 자식 프로세스를 만들어 클라이언트를 담당한다. 전통적인 blocking I/O 방식이다.
  2. 요청 당 쓰레드 할당. 위 예제에서 사용한 방식으로 pthread_create()를 통해 쓰레드를 생성, 클라이언트를 담당한다. 마찬가지로 blocking I/O 방식이다.
  3. 쓰레드풀을 구성해 각각의 쓰레드가 여러 커넥션을 asynchronous I/O 방식으로 담당한다.
  4. 쓰레드풀을 구성해 각각의 쓰레드가 여러 커넥션을 select(), poll(), nonblocking I/O 같은 이벤트 기반 방식으로 담당한다.

이 중 대용량 처리에 3, 4번이 우세하며 특히 4번이 대세이다. 하지만, 여기서 모두 언급하기엔 지나치게 방대하므로 추후 별도로 정리해보기로 한다. 자세한 사항은 C10K Problem을 참고하기 바란다.

여기선 가장 간편한 방식인 2번, 클라이언트를 각각의 쓰레드에 할당하고 close() 될때 쓰레드도 함께 종료되는 방식으로 구현했다.

htop을 이용해 쓰레드 옵션을 켜고(H 키), 트리 모드(t 키)에서 요청이 있을때마다 쓰레드가 하나씩 생성되는 모습을 직접 확인한 화면이다.

현재 2개의 요청을 처리 중이며, 물론 요청이 끝나면 쓰레드도 함께 종료된다. 만약 프로세스나 쓰레드를 할당하지 않았다면 요청이 끝날때까지 다른 요청은 받지 못하는 말 그대로 blocking 상태가 유지될 것이다.

종료 과정

샘플 서버를 구동하고 클라이언트에서 FIN/ACK이 잘 전달되는지 확인해본다. 아울러 서버의 Active Close 후 서버측에 남게되는 TIME_WAIT 상태를 직접 확인한다.

TCP/IP Illustrated 의 TCP 연결 종료 다이어그램은 아래와 같다.

연결 종료의 4-way handshake 과정은 다음과 같다.

  • FIN+ACK, seq = K, ack = L
  • ACK, seq = L, ack = K + 1
  • FIN+ACK, seq = L, ack = K + 1
  • ACK, seq = K, ack = L + 1

https://docs.likejazz.com/time-wait/

[원본문서]

 

TIME_WAIT 상태란 무엇인가 · The Missing Papers

TIME_WAIT 상태란 무엇인가 14 Jun 2015 TIME_WAIT 상태가 늘어나면 서버의 소켓이 고갈되어 커넥션 타임아웃이 발생한다는 얘기를 한다. 이 말이 올바른 얘기인지, TIME_WAIT은 어떠한 경우에 발생하고 어

docs.likejazz.com

결론

TCP/IP Illustrated를 쓴 리차드 스티븐스의 또 다른 책 Unix Network Programming에는 이런 구절2이 있다.

The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.

TIME_WAIT은 우리를 도와주는 친구다. 네트워크에서 오래된 중복 세그먼트를 날려주는 훌륭한 역할을 한다. 자꾸 없애려고 노력하지 말고 이해해야 한다.


TIME_WAIT은 패킷의 오동작을 막아주는 우리의 친구같은 존재다.

수 많은 잘못된 정보들 사이에서 아래와 같은 올바른 정보를 반드시 기억해두길 바란다.

  • TIME_WAIT의 타임아웃은 60초로 하드 코딩되어 있다. 설정할 수 없다.
  • 다수의 TIME_WAIT이 서버 성능을 저하시킨다는 논문5은 1997년에 출판됐다. 지금은 2016년이다. 20년이 지났다.
  • 클라이언트가 서버 투 서버로 한 서버에 요청이 많을 경우 tcp_tw_reuse 옵션을 설정해 TIME_WAIT을 재사용하도록 한다. 서버는 해당 사항이 없다.
  • 오래된 서버인 경우 클라이언트가 서버 투 서버 통신을 많이 한다면 빈 포트 스캔으로 성능 저하가 발생하므로 마찬가지로 tcp_tw_reuse 옵션을 설정한다.
  • tcp_tw_reuse와 SO_REUSEADDR는 서로 다른 소켓에 적용되는 옵션이다.
  • 서버/클라이언트 모두 tcp_timestamps가 기본값인 켜져 있어야 하며, 끄면 안되고 끌 필요도 없다.
  • net.ipv4.tcp_fin_timeout은 90 정도로 설정한다.
  • FIN_WAIT1은 상대방 OS에 문제가 있는 경우다.
  • FIN_WAIT2는 상대방 어플리케이션에 문제가 있는 경우다.
  • FIN_WAIT2는 TIME_WAIT의 역할을 대행한다.
  • 특수한 경우가 아니면 링거 옵션은 사용하지 않는다.
  • 서버가 클라이언트를 accept() 할때 할당하는 것은 소켓이다. 포트가 아니다.
  • 소켓의 최대 갯수는 65,535개가 아니다. 소켓은 <protocol>, <src addr>, <src port>, <dest addr>, <dest port> 5개의 값으로 유니크하게 구성되며, 서버 포트 또는 클라이언트의 IP가 추가될 경우 그 만큼의 새로운 쌍을 생성할 수 있다.

TL;DR

  • 서버는 아무것도 할 필요가 없다.
  • 클라이언트9 net.ipv4.tcp_tw_reuse를 1로 설정한다.
  • 서버 클라이언트가 NAT 없이 1:1 로 직접 연결되어 있다면 압도적인 성능을 보이는 net.ipv4.tcp_tw_recycle을 적극 활용한다.

'Tech > TCP IP' 카테고리의 다른 글

WireShark TLS(DH 키교환) 복호화는 거의 어렵다.....  (1) 2024.01.23
TCP 커널 파라메터  (0) 2023.08.29
TCP DUMP  (0) 2020.03.26
SSL Handshake ( SNI 프로파일 )  (0) 2019.09.26
SSL TLS ( TLS Alert Protocol )  (0) 2019.03.26