42. TCP 상태 전이 — LISTEN부터 TIME_WAIT까지

changseop lee·6일 전

📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 42편
이전 글: 41. TCP 3-Way Handshake — 연결이 성립했다는 증거 · 다음 글: 43. TCP Flag
참고(리눅스 시스템 기초): ss와 ip 명령어 — ss 기본 옵션 / 44. 프로세스 네트워크 연결 점검 — 연결과 프로세스 연결 짓기

1. 왜 알아야 하는가

08편과 09편에서 연결의 시작(SYN)과 끝(FIN·RST)을 패킷으로 봤습니다. 그런데 관제 현장에서 항상 패킷 캡처가 있는 것은 아닙니다. 서버에 접속해 지금 이 순간의 연결 상태만 확인할 수 있는 경우가 훨씬 많습니다.

이때 ss -tan의 State 열이 중요한 단서가 됩니다.

  • SYN-RECV가 수천 개 → 연결 요청은 오는데 마지막 ACK가 오지 않음
  • SYN-SENT가 계속 쌓임 → 이 서버가 밖으로 연결을 시도하는데 응답이 없음
  • CLOSE-WAIT가 계속 늘어남 → 상대는 끊었는데 이 서버의 프로그램이 소켓을 닫지 않음
  • TIME-WAIT가 많음 → 대부분 정상이지만, 짧은 연결이 폭증했다는 신호일 수 있음

상태 이름의 의미와 어떤 패킷을 계기로 그 상태가 되는지를 알면, 캡처 없이도 "지금 어느 쪽에서 무엇이 멈춰 있는지"를 추론할 수 있습니다. 이번 글은 RFC 9293에 정의된 TCP 상태와 전이 조건, 그리고 Linux에서 상태별로 소켓을 조회·해석하는 방법을 다룹니다.


2. 핵심 개념

2-1. TCP의 11가지 상태

상태 (RFC 이름)ss 표시누가의미
CLOSED(일반적으로 표시 안 됨)양쪽연결 없음 (개념상 상태)
LISTENLISTEN서버연결 요청 대기
SYN-SENTSYN-SENT연결 시작한 쪽SYN 보내고 SYN/ACK 기다림
SYN-RECEIVEDSYN-RECV서버SYN 받고 SYN/ACK 보냄, 마지막 ACK 기다림
ESTABLISHEDESTAB양쪽연결 성립, 데이터 송수신 가능
FIN-WAIT-1FIN-WAIT-1먼저 닫은 쪽FIN 보내고 ACK 기다림
FIN-WAIT-2FIN-WAIT-2먼저 닫은 쪽내 FIN은 확인됨, 상대 FIN 기다림
CLOSE-WAITCLOSE-WAIT나중에 닫는 쪽상대 FIN 받음, 내 애플리케이션이 close() 하기를 기다림
CLOSINGCLOSING양쪽양쪽이 거의 동시에 FIN을 보낸 드문 경우
LAST-ACKLAST-ACK나중에 닫는 쪽내 FIN 보내고 마지막 ACK 기다림
TIME-WAITTIME-WAIT먼저 닫은 쪽마지막 ACK 보낸 뒤 일정 시간 대기

UDP 소켓은 연결 상태가 없어 ss -uan에서 대부분 UNCONN으로 보입니다(07편 참고).

2-2. 상태를 바꾸는 계기

현재 상태계기 (받거나 보낸 것)다음 상태
CLOSED앱이 listen()LISTEN
CLOSED앱이 connect() → SYN 송신SYN-SENT
LISTENSYN 수신 → SYN/ACK 송신SYN-RECEIVED
SYN-SENTSYN/ACK 수신 → ACK 송신ESTABLISHED
SYN-RECEIVEDACK 수신ESTABLISHED
ESTABLISHED앱이 close() → FIN 송신FIN-WAIT-1
ESTABLISHEDFIN 수신 → ACK 송신CLOSE-WAIT
FIN-WAIT-1내 FIN에 대한 ACK 수신FIN-WAIT-2
FIN-WAIT-1상대 FIN 수신 (내 FIN의 ACK 전)CLOSING
FIN-WAIT-2상대 FIN 수신 → ACK 송신TIME-WAIT
CLOSE-WAIT앱이 close() → FIN 송신LAST-ACK
LAST-ACKACK 수신CLOSED
TIME-WAIT대기 시간 만료CLOSED
(대부분의 상태)RST 수신CLOSED

2-3. TIME-WAIT가 필요한 이유

TIME-WAIT는 오류가 아니라 의도된 대기입니다.

  1. 내가 보낸 마지막 ACK가 유실되면 상대가 FIN을 재전송합니다. 이때 다시 ACK를 보내 줄 수 있어야 합니다.
  2. 같은 주소·포트 조합으로 곧바로 새 연결을 만들면, 이전 연결에서 늦게 도착한 패킷이 새 연결에 섞일 수 있습니다. 네트워크에 남은 옛 패킷이 사라질 때까지(RFC상 2MSL) 기다립니다.

Linux는 TIME-WAIT 시간을 60초로 고정해 두고 있습니다(커널 상수). 흔히 조정 대상으로 언급되는 net.ipv4.tcp_fin_timeout은 TIME-WAIT가 아니라 FIN-WAIT-2에 머무를 수 있는 시간을 정하는 값입니다.


3. 동작 원리

연결 한 개의 생애를 클라이언트가 먼저 닫는 경우로 따라가면 다음과 같습니다.

TCP 상태 전이
그림 1. 연결을 시작·종료하는 쪽과 받는 쪽이 거치는 상태

          클라이언트                                   서버
          CLOSED                                      CLOSED
             │                                           │ listen()
             │                                           ↓
             │ connect()                              LISTEN
             ↓            ── SYN ──────────────→          │
          SYN-SENT                                        ↓
             │            ←──────────── SYN/ACK ──   SYN-RECEIVED
             ↓            ── ACK ──────────────→          │
          ESTABLISHED                                     ↓
             │                                       ESTABLISHED
             │         (데이터 송수신)                    │
             │ close()                                    │
             ↓            ── FIN ──────────────→          │
          FIN-WAIT-1                                      ↓
             │            ←──────────────── ACK ──   CLOSE-WAIT ← 앱이 close() 할 때까지 머묾
             ↓                                            │ close()
          FIN-WAIT-2                                      ↓
             │            ←──────────────── FIN ──   LAST-ACK
             ↓            ── ACK ──────────────→          │
          TIME-WAIT                                       ↓
             │ (Linux 60초)                            CLOSED
             ↓
          CLOSED

이 그림에서 관제 관점의 요점은 "상태가 오래 머무는 곳은 누군가가 무엇을 기다리는 곳" 이라는 점입니다.

오래 머무는 상태기다리는 것멈춘 쪽
SYN-SENT상대의 SYN/ACK네트워크·상대 (drop, 호스트 없음)
SYN-RECEIVED상대의 마지막 ACK상대 (응답하지 않거나 위조된 출발지)
CLOSE-WAIT내 애플리케이션의 close()내 프로그램
FIN-WAIT-2상대의 FIN상대 애플리케이션
LAST-ACK상대의 마지막 ACK상대·네트워크

특히 CLOSE-WAIT는 커널이 아니라 애플리케이션이 풀어야 하는 상태입니다. CLOSE-WAIT가 계속 늘어나면 네트워크가 아니라 프로그램의 소켓 처리(연결 누수)를 의심하는 것이 일반적입니다.


4. 실제 명령어 / 실습

실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu) 2대(192.168.10.10 클라이언트, 192.168.10.20 서버 예시, 서버에 sshd 동작 중). 인터페이스 이름은 ens33을 예시로 사용합니다(환경에 따라 다름). ss는 iproute2 패키지에 포함되어 두 배포판 모두 기본 설치되어 있습니다. nc가 없다면 sudo dnf install -y nmap-ncat(Rocky) 또는 sudo apt install -y netcat-openbsd(Ubuntu)로 설치합니다.

4-1. 전체 상태 분포 보기

# 상태별 소켓 수 요약
ss -s

# TCP 소켓을 State 열 기준으로 집계
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

4-2. 상태 필터로 조회

# 특정 상태만 (-H: 헤더 생략)
ss -Htan state established
ss -Htan state syn-sent
ss -Htan state time-wait | wc -l

# 상태 + 포트 조건 (필터식은 작은따옴표로 감쌈)
ss -tan state established '( dport = :443 or sport = :443 )'

# 타이머 정보 포함 (-o): TIME-WAIT 남은 시간 등
ss -tano state time-wait

# 프로세스 정보 포함 (-p, 다른 사용자 소켓까지 보려면 sudo)
sudo ss -tanp state close-wait

4-3. TIME-WAIT 만들어 보기

# [클라이언트] 서버 22번에 연결만 했다가 끊기를 10회 반복 (클라이언트가 먼저 닫음)
for i in $(seq 1 10); do nc -zv 192.168.10.20 22 2>/dev/null; done

# [클라이언트] 먼저 닫은 쪽에 TIME-WAIT가 남는지 확인
ss -tano state time-wait '( dport = :22 )'

서버의 sshd 로그(Rocky: /var/log/secure, Ubuntu: /var/log/auth.log)에는 식별 문자열 없이 끊긴 연결 기록이 남을 수 있습니다. 연결만 맺고 끊는 동작도 서버에 흔적을 남긴다는 점을 함께 확인해 봅니다.

4-4. CLOSE-WAIT / FIN-WAIT-2 만들어 보기

소켓을 받아 놓고 닫지 않는 간단한 서버를 60초 동안 실행합니다(실습 후 자동 종료).

# [서버] 실습 동안만 9000/tcp 허용 (firewalld 사용 시)
sudo firewall-cmd --add-port=9000/tcp

# [서버] 연결을 accept한 뒤 60초간 close()하지 않는 프로그램
python3 -c "import socket,time; s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); s.bind(('0.0.0.0',9000)); s.listen(); c,a=s.accept(); time.sleep(60)"

# [클라이언트] 접속 후 즉시 종료 (FIN 송신)
nc -zv 192.168.10.20 9000

# [서버] 60초 안에 확인
ss -tan state close-wait '( sport = :9000 )'
# [클라이언트] 60초 안에 확인
ss -tan state fin-wait-2 '( dport = :9000 )'

# [서버] 실습 후 허용 해제
sudo firewall-cmd --remove-port=9000/tcp

4-5. 관련 커널 설정 조회

sysctl net.ipv4.tcp_fin_timeout      # FIN-WAIT-2 유지 시간(초)
sysctl net.ipv4.tcp_max_tw_buckets   # 동시에 유지할 수 있는 TIME-WAIT 최대 수

tcp_tw_reuse 등 TIME-WAIT 관련 값을 바꾸라는 글이 많지만, 영향 범위를 이해하지 않고 운영 서버에서 변경하는 것은 권장되지 않습니다(과거의 tcp_tw_recycle은 NAT 환경 문제로 Linux 4.12에서 제거되었습니다).

📷 [실습 화면 삽입] ss -tan | awk ... | uniq -c 상태 집계 결과와 ss -s 요약


5. 결과 확인

출력 형식 예시(값은 환경마다 다름):

$ ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
     12 TIME-WAIT
      5 ESTAB
      4 LISTEN
      1 CLOSE-WAIT

$ ss -tano state time-wait '( dport = :22 )'
Recv-Q  Send-Q   Local Address:Port      Peer Address:Port   Process
0       0        192.168.10.10:51540     192.168.10.20:22    timer:(timewait,52sec,0)

$ ss -tan state close-wait '( sport = :9000 )'
Recv-Q  Send-Q   Local Address:Port      Peer Address:Port   Process
1       0        192.168.10.20:9000      192.168.10.10:51552

state 필터를 쓰면 출력에서 State 열이 빠집니다. 조회한 상태가 이미 정해져 있기 때문입니다. CLOSE-WAIT의 Recv-Q 1은 상대가 보낸 FIN이 아직 애플리케이션에 읽히지 않았다는 흔적으로 볼 수 있습니다.

  • ss -tan 결과를 상태별로 집계할 수 있다
  • 먼저 닫은 쪽(클라이언트)에 TIME-WAIT가 남고 timer:(timewait,...)가 줄어드는 것을 확인했다
  • 애플리케이션이 close()하지 않으면 서버에 CLOSE-WAIT, 클라이언트에 FIN-WAIT-2가 남는 것을 확인했다
  • tcp_fin_timeout이 TIME-WAIT가 아닌 FIN-WAIT-2 시간이라는 점을 설명할 수 있다
  • 실습용 방화벽 허용을 해제했다

📷 [실습 화면 삽입] 서버의 ss -tan state close-wait와 클라이언트의 ss -tan state fin-wait-2 결과를 나란히


6. 패킷 / 로그 분석

관찰정상일 수 있는 경우의심해야 하는 경우
SYN-RECV 다수순간 접속 폭주, 느린 클라이언트수백 개 이상 지속 + 출발지 IP 다양 → SYN Flood (08편 카운터와 함께 확인)
SYN-SENT 다수 (서버가 클라이언트)외부 API·업데이트 서버 장애, 방화벽 정책 누락서비스와 무관한 외부 IP·비표준 포트로 반복 시도 → 악성 프로세스의 C2 접속 시도, 내부 대상이 여러 개면 서버발 스캔 가능성
ESTAB가 장시간 유지DB 커넥션 풀, SSH 세션, 메시지 큐알 수 없는 프로세스가 외부 IP와 장시간 연결 (리버스 셸 등)
CLOSE-WAIT 지속 증가— (정상 운영에서 계속 쌓이지 않음)애플리케이션 소켓 누수 → 자원 고갈·장애. 보안 사고라기보다 장애 신호지만 가용성 영향
TIME-WAIT 수천 개웹서버·프록시처럼 짧은 연결을 많이 처리하는 서버평소 기준선보다 급증 + 특정 출발지 집중 → 대량 요청 (크롤링·무차별 대입 가능성)

기준선(평소 값)을 알아야 이상을 판단할 수 있습니다. 상태 분포를 주기적으로 남겨 두는 예시입니다.

# 시각과 상태 분포를 한 줄로 기록 (cron 등으로 주기 실행 가능)
echo "$(date '+%F %T') $(ss -Htan | awk '{print $1}' | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" >> ~/tcp_state_baseline.log

# SYN-SENT 상태 소켓을 가진 프로세스 확인 (외부로 연결을 시도하는 주체)
sudo ss -tanp state syn-sent

# ESTAB 연결을 원격 IP별로 집계 (Peer Address 열)
ss -Htan state established | awk '{print $4}' | sed 's/:[0-9]*$//' | sort | uniq -c | sort -rn | head

state 필터를 쓴 ss -Htan 출력은 State 열이 빠지므로 Recv-Q Send-Q Local Peer 순서가 되어, Peer 주소는 네 번째 열입니다. 비정상 연결을 프로세스·실행 파일까지 추적하는 방법은 38. 비정상 네트워크 연결 탐지에서 다뤘습니다.


7. 보안관제 관점

흔적이 남는 곳

  • 호스트의 현재 상태: ss 결과는 로그가 아니라 스냅숏입니다. 침해 대응 시에는 가장 먼저 수집해야 하는 휘발성 정보 중 하나입니다(시간이 지나면 TIME-WAIT·연결이 사라짐).
  • 방화벽 세션 테이블: 방화벽도 자체적으로 TCP 상태를 추적하며, 상태별 타임아웃(예: 반쯤 열린 세션, 종료 중 세션)을 따로 둡니다. 설정·표기는 장비마다 다르며 06. 방화벽 · IDS/IPS 분석 시리즈에서 다룹니다.
  • EDR·에이전트: 일부 에이전트는 프로세스별 네트워크 연결 이벤트를 기록하므로, 스냅숏의 한계를 보완할 수 있습니다.

한계와 오탐 주의

  • 상태 수만으로 공격을 단정하지 않습니다. TIME-WAIT가 많은 것은 대부분 정상적인 고부하 서버의 특징입니다.
  • 클라이언트와 서버의 상태는 같은 순간에도 다릅니다. 한쪽 호스트의 상태만 보고 연결 전체를 판단하면 오해할 수 있으므로 가능하면 양쪽 또는 패킷 캡처와 함께 봅니다.
  • NAT·프록시가 있으면 서버에 보이는 Peer 주소는 NAT·프록시의 주소입니다(05편 참고).
  • 스캔에 의한 SYN-RECV·RST 패턴 분석은 05. 네트워크 스캔 · 공격 징후 분석 시리즈에서 다룹니다.

8. 핵심 정리

  • TCP에는 RFC 9293 기준 11가지 상태가 있으며, 각 상태는 특정 패킷 송수신이나 애플리케이션 호출을 계기로 바뀝니다.
  • 오래 머무는 상태는 "무엇을 기다리는지"를 알려줍니다: SYN-SENT(상대 응답), SYN-RECV(마지막 ACK), CLOSE-WAIT(내 앱의 close).
  • TIME-WAIT는 먼저 닫은 쪽에 남는 의도된 대기이며, Linux에서는 60초로 고정되어 있습니다.
  • ss -tan state <상태>와 상태별 집계로 캡처 없이도 연결 문제의 위치를 추론할 수 있습니다.
  • 상태 정보는 휘발성이므로 침해 대응 시 먼저 수집하고, 평소 기준선과 비교해 이상 여부를 판단합니다.

다음 글: 11. 시퀀스 번호·ACK·윈도우 — TCP가 데이터를 빠짐없이 전달하는 방법

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글