45. TCP 연결 종료 — FIN과 RST의 차이

changseop lee·6일 전

📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 45편
이전 글: 44. SYN, SYN/ACK, ACK · 다음 글: 46. 시퀀스 번호·ACK·윈도우 — TCP가 데이터를 빠짐없이 전달하는 방법
참고(리눅스 시스템 기초): Socket 이해 — close()·shutdown()이 소켓에 미치는 영향

1. 왜 알아야 하는가

연결이 어떻게 끝났는지는 연결이 시작된 방식만큼 많은 것을 알려줍니다.

  • 양쪽이 FIN을 주고받고 끝났다 → 애플리케이션이 할 일을 마치고 정상 종료
  • 한쪽이 RST를 보냈다 → 무언가가 연결을 강제로 끊음 (프로세스 비정상 종료, 정책 차단, 닫힌 포트 등)

방화벽·IPS가 차단할 때 RST를 주입하는 경우가 많기 때문에, 관제에서 "이 세션은 RST로 끝났다"는 사실은 "차단이 실제로 동작했는가" 를 판단하는 근거가 되기도 합니다. 반대로 모든 RST를 이상으로 보면 오탐이 넘칩니다. 브라우저·로드밸런서·애플리케이션이 성능이나 설계상의 이유로 정상적으로 RST를 쓰는 경우도 많기 때문입니다.

이번 글은 FIN과 RST의 동작 차이, RST가 생기는 대표 상황, 그리고 종료 방식을 로그와 패킷에서 확인하는 방법을 다룹니다. 종료 과정 중 각 상태(FIN-WAIT, TIME-WAIT 등)의 의미는 10편에서 이어서 다룹니다.


2. 핵심 개념

2-1. FIN과 RST 비교

구분FINRST
의미"나는 더 보낼 데이터가 없다""이 연결을 지금 즉시 중단한다"
성격합의에 의한 종료 (상대도 FIN을 보내야 완전히 끝남)일방적 중단
방향방향별로 따로 닫힘 (반쪽 종료 가능)양방향 동시에 끝남
상대의 응답ACK로 확인원칙적으로 응답하지 않음 (예외: 2-3의 Challenge ACK)
남은 데이터전송 중인 데이터는 끝까지 전달송·수신 버퍼의 데이터 폐기
시퀀스 번호1개 소비 (SYN과 동일)소비하지 않음
애플리케이션이 보는 결과읽기 시 EOF(0바이트)Connection reset by peer(ECONNRESET), 연결 시도 중이면 Connection refused

2-2. RST가 발생하는 대표 상황

상황누가 보내는가예
닫힌 포트로 SYN 도착대상 호스트 커널08편의 Connection refused
존재하지 않는 연결로 세그먼트 도착수신 호스트 커널서버 재부팅 후 클라이언트가 옛 연결로 데이터 전송
애플리케이션의 강제 종료해당 호스트 커널SO_LINGER 0초 설정 후 close(), 수신 버퍼에 읽지 않은 데이터가 남은 상태에서 close()
방화벽 reject 정책방화벽iptables/nftables의 reject with tcp reset 계열 규칙
IPS·보안 장비의 차단중간 장비 (양쪽 모두에게 주입하기도 함)악성 시그니처 탐지 후 세션 끊기
상태 테이블에서 만료된 세션방화벽·로드밸런서 (장비·설정에 따라 다름)장시간 유휴 연결 후 데이터 전송

2-3. RST가 받아들여지는 조건

아무 RST나 받아들이면 제3자가 남의 연결을 쉽게 끊을 수 있습니다. 그래서 수신 측은 RST의 시퀀스 번호가 현재 수신 윈도우 안에 있는지를 검사합니다. 현재 Linux는 RFC 5961의 방식을 따라, 시퀀스 번호가 기대값과 정확히 일치할 때만 즉시 연결을 끊고, 윈도우 안이지만 일치하지 않으면 Challenge ACK를 보내 진짜 상대인지 확인합니다. 윈도우 밖이면 무시합니다.


3. 동작 원리

FIN과 RST 비교
그림 1. FIN 4-Way 종료와 RST 즉시 종료의 차이

3-1. 정상 종료 (FIN)

클라이언트가 먼저 닫는(Active Close) 경우입니다.

클라이언트 (Active Close)                        서버 (Passive Close)
(ESTABLISHED)                                    (ESTABLISHED)
    │ ① FIN/ACK  seq=u                               │
    │ ─────────────────────────────────────────────→ │
(FIN-WAIT-1)                                     (CLOSE-WAIT) ← 앱에 EOF 전달
    │ ② ACK  ack=u+1                                 │
    │ ←───────────────────────────────────────────── │
(FIN-WAIT-2)                                         │  서버 앱이 남은 데이터 전송 후 close()
    │ ③ FIN/ACK  seq=v                               │
    │ ←───────────────────────────────────────────── │
(TIME-WAIT)                                      (LAST-ACK)
    │ ④ ACK  ack=v+1                                 │
    │ ─────────────────────────────────────────────→ │
    │  일정 시간 대기 후                            (CLOSED)
(CLOSED)

이론상 4개 패킷(4-Way)이지만, 서버가 ②와 ③을 한 번에 보내면 FIN/ACK → FIN/ACK → ACK의 3개로 보이는 경우가 흔합니다. ②와 ③ 사이에 시간이 벌어져 있다면 서버가 받은 뒤에도 데이터를 보내고 있었다는 뜻입니다(반쪽 종료).

3-2. 강제 중단 (RST)

클라이언트                                        서버
(ESTABLISHED)                                    (ESTABLISHED)
    │  데이터 주고받는 중                             │
    │ ① RST  seq=기대값                              │
    │ ─────────────────────────────────────────────→ │
(CLOSED)  ← 즉시                                 (CLOSED) ← 즉시, 앱에는 ECONNRESET
          버퍼 데이터 폐기, 응답 없음, TIME-WAIT 없음

RST 방식은 TIME-WAIT 상태를 남기지 않고 바로 끝납니다. 그래서 대량 연결을 처리하는 일부 클라이언트·로드밸런서가 의도적으로 RST로 연결을 정리하기도 합니다. 이것이 "RST = 공격 또는 장애"라고 단정하면 안 되는 이유입니다.


4. 실제 명령어 / 실습

실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu) 2대(192.168.10.10 클라이언트, 192.168.10.20 서버 예시). 인터페이스 이름은 ens33을 예시로 사용합니다(환경에 따라 다름). 서버의 방화벽에서 실습 포트 9000/tcp가 허용되어 있어야 합니다.

# 도구 설치 (python3는 두 배포판 모두 대개 기본 설치)
sudo dnf install -y tcpdump nmap-ncat      # Rocky
sudo apt install -y tcpdump netcat-openbsd # Ubuntu

# 실습 동안만 9000/tcp 허용 (firewalld 사용 시, 재부팅·reload 후 사라짐)
sudo firewall-cmd --add-port=9000/tcp

4-1. FIN 종료 관찰

# [서버 터미널 1] 9000번 대기
nc -l 9000

# [서버 터미널 2] FIN 또는 RST가 켜진 패킷만 캡처 (-S: 시퀀스 번호를 절대값으로 표시)
sudo tcpdump -nn -S -i ens33 'tcp port 9000 and tcp[tcpflags] & (tcp-fin|tcp-rst) != 0'

# [클라이언트] 접속 → 문자열 입력 → Ctrl+C로 종료
nc 192.168.10.20 9000

4-2. RST 종료 관찰 (SO_LINGER 0)

서버에서 nc -l 9000을 다시 실행한 뒤, 클라이언트에서 다음을 실행합니다. 연결 직후 SO_LINGER를 0초로 설정하고 닫으므로 FIN 대신 RST가 전송됩니다.

python3 -c "import socket,struct; s=socket.create_connection(('192.168.10.20',9000)); s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii',1,0)); s.close()"

4-3. 종료 관련 상태·카운터 확인

# 종료 진행 중인 소켓 (상태 상세는 10편)
ss -tan state fin-wait-2
ss -tan state time-wait
ss -tan state close-wait

# RST·강제 종료 관련 누적 카운터
nstat -az TcpOutRsts TcpEstabResets TcpExtTCPAbortOnData TcpExtTCPAbortOnClose TcpExtTCPAbortOnTimeout

# 실습 후 임시 허용 해제
sudo firewall-cmd --remove-port=9000/tcp
카운터의미
TcpOutRsts이 호스트가 보낸 RST 수
TcpEstabResetsESTABLISHED/CLOSE-WAIT 상태에서 RST로 끊긴 연결 수
TcpExtTCPAbortOnData예상치 못한 데이터가 와서 RST로 중단한 수
TcpExtTCPAbortOnClose읽지 않은 데이터가 남은 채 닫혀 RST를 보낸 수
TcpExtTCPAbortOnTimeout재전송 타임아웃으로 중단된 연결 수

📷 [실습 화면 삽입] FIN 종료 캡처 — Flags [F.] 두 줄과 이어지는 ACK


5. 결과 확인

tcpdump 출력 형식 예시(값은 환경마다 다름, 시각 생략):

# FIN 종료 (필터 때문에 FIN이 있는 패킷만 보임, 마지막 ACK는 필터에서 제외됨)
# -S 옵션으로 seq/ack를 절대값으로 표시 (한쪽 FIN의 seq+1이 상대 FIN의 ack)
IP 192.168.10.10.51530 > 192.168.10.20.9000: Flags [F.], seq 2894311207, ack 1733905512, win 502, length 0
IP 192.168.10.20.9000 > 192.168.10.10.51530: Flags [F.], seq 1733905512, ack 2894311208, win 509, length 0

# RST 종료 (SO_LINGER 0)
IP 192.168.10.10.51534 > 192.168.10.20.9000: Flags [R.], seq 3310027745, ack 902115634, win 502, length 0
확인 포인트FIN 종료RST 종료
플래그[F.] 양방향 각 1회[R.] 또는 [R] 1회
상대의 응답ACK (필터 없이 보면 확인 가능)없음
먼저 닫은 쪽 상태TIME-WAIT가 ss -tan state time-wait에 잠시 남음남지 않음
서버 nc의 반응조용히 종료 (EOF)조용히 종료되거나 reset 관련 메시지 (구현에 따라)
  • FIN이 양방향으로 한 번씩 오가는 것을 확인했다
  • 먼저 FIN을 보낸 쪽에 TIME-WAIT 소켓이 남는 것을 확인했다
  • SO_LINGER 0 종료에서 RST 한 번으로 끝나고 응답이 없음을 확인했다
  • TcpOutRsts 카운터가 실습 전후로 증가한 것을 확인했다
  • 실습용 방화벽 허용을 해제했다

📷 [실습 화면 삽입] RST 종료 캡처 — Flags [R.] 한 줄과 ss -tan state time-wait 결과 비교


6. 패킷 / 로그 분석

관찰정상일 수 있는 경우의심해야 하는 경우
서버가 응답 직후 RST로드밸런서·헬스체크의 빠른 연결 정리, 브라우저 탭 닫기특정 요청(URL·페이로드) 직후에만 반복 → WAF·IPS 차단 또는 서비스 크래시
양쪽이 동시에 RST를 받음—중간 장비가 양방향으로 RST 주입 (IPS 차단 동작의 전형)
RST의 TTL이 평소 패킷과 다름경로 변경서버가 아닌 중간 장비가 만든 RST일 가능성 (TTL 비교 방법은 04편 참고)
한 호스트의 TcpOutRsts 급증서비스 재시작 직후 옛 연결 정리여러 출발지에서 닫힌 포트로 SYN 유입 → 스캔 대상이 됨 (05. 네트워크 스캔 · 공격 징후 분석 시리즈에서 다룸)
외부로 나가는 세션이 대부분 RST로 종료프록시·보안 게이트웨이의 정책 차단(정상 동작 확인 필요)차단 대상 C2에 반복 연결 시도하는 감염 단말
FIN 없이 세션이 사라짐(타임아웃)모바일·무선 끊김, 유휴 만료방화벽 drop 정책에 걸린 세션, 비정상 종료된 프로세스
# RST만 보고, 누가 보냈는지(출발지) 확인 + TTL 포함(-v)
sudo tcpdump -nn -v -i ens33 'tcp[tcpflags] & tcp-rst != 0'

# 특정 서버 포트에서 나가는 RST만
sudo tcpdump -nn -i ens33 'src port 443 and tcp[tcpflags] & tcp-rst != 0'

# FIN 종료만 (RST 제외)
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & tcp-fin != 0 and tcp[tcpflags] & tcp-rst == 0'

RST 한 개만으로 원인을 단정하기 어렵습니다. 직전에 어떤 요청이 있었는지, RST를 보낸 쪽이 어디인지, 같은 패턴이 반복되는지를 함께 봐야 합니다.


7. 보안관제 관점

흔적이 남는 곳

위치종료 방식이 남는 형태
방화벽 세션 로그세션 종료 사유 필드 (예: FIN에 의한 종료, RST에 의한 종료, 에이징(타임아웃), 정책 차단 등 — 표기는 장비마다 다름)
IPS 로그차단 액션 종류 (drop / reset 등). reset 액션이면 양방향 RST 주입 흔적이 패킷에도 남음
Zeek conn.logconn_state: SF(정상 성립·정상 종료), RSTO(출발지가 RST로 중단), RSTR(응답자가 RST로 중단), REJ(연결 시도가 거부됨), S1(성립 후 종료 미확인) 등
호스트nstat 누적 카운터, 애플리케이션 에러 로그 (Connection reset by peer)

한계와 오탐 주의

  • RST는 정상 동작에서도 흔합니다. RST 수 자체보다 "누가, 어떤 요청 뒤에, 얼마나 반복적으로" 가 판단 기준입니다.
  • IPS가 reset 액션으로 차단했더라도, RST가 도착하기 전에 일부 데이터가 이미 전달됐을 수 있습니다. 차단 로그만 보고 "피해 없음"으로 결론 내리지 말고 서버·단말 측 로그를 확인합니다.
  • RST 위조는 윈도우 검사 때문에 쉽지 않지만, 경로상에 있는 장비는 시퀀스 번호를 볼 수 있으므로 RST 주입이 가능합니다. 중간 장비 RST 식별의 필드 단위 방법은 03. Wireshark 패킷 분석 시리즈에서 다룹니다.
  • 방화벽의 reject(RST 응답)와 drop(무응답) 정책 선택 기준은 06. 방화벽 · IDS/IPS 분석 시리즈에서 다룹니다.

8. 핵심 정리

  • FIN은 "보낼 데이터가 끝났다"는 합의형 종료이며, 방향별로 따로 닫히고 ACK로 확인합니다.
  • RST는 즉시 중단이며, 응답이 없고 버퍼 데이터가 폐기되며 TIME-WAIT도 남기지 않습니다.
  • RST는 닫힌 포트, 존재하지 않는 연결, 애플리케이션 강제 종료, 방화벽 reject, IPS 차단 등 여러 원인에서 발생합니다.
  • 수신 측은 RST의 시퀀스 번호를 검사하므로(RFC 5961 Challenge ACK), 경로 밖에서의 RST 위조는 어렵습니다.
  • 관제에서는 세션 종료 사유(FIN / RST / 타임아웃)와 RST를 보낸 주체를 구분해 차단 동작·서비스 이상을 판단합니다.

다음 글: 10. TCP 상태 전이 — LISTEN부터 TIME_WAIT까지

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

0개의 댓글