📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 45편
이전 글: 44. SYN, SYN/ACK, ACK · 다음 글: 46. 시퀀스 번호·ACK·윈도우 — TCP가 데이터를 빠짐없이 전달하는 방법
참고(리눅스 시스템 기초): Socket 이해 — close()·shutdown()이 소켓에 미치는 영향
연결이 어떻게 끝났는지는 연결이 시작된 방식만큼 많은 것을 알려줍니다.
방화벽·IPS가 차단할 때 RST를 주입하는 경우가 많기 때문에, 관제에서 "이 세션은 RST로 끝났다"는 사실은 "차단이 실제로 동작했는가" 를 판단하는 근거가 되기도 합니다. 반대로 모든 RST를 이상으로 보면 오탐이 넘칩니다. 브라우저·로드밸런서·애플리케이션이 성능이나 설계상의 이유로 정상적으로 RST를 쓰는 경우도 많기 때문입니다.
이번 글은 FIN과 RST의 동작 차이, RST가 생기는 대표 상황, 그리고 종료 방식을 로그와 패킷에서 확인하는 방법을 다룹니다. 종료 과정 중 각 상태(FIN-WAIT, TIME-WAIT 등)의 의미는 10편에서 이어서 다룹니다.
| 구분 | FIN | RST |
|---|---|---|
| 의미 | "나는 더 보낼 데이터가 없다" | "이 연결을 지금 즉시 중단한다" |
| 성격 | 합의에 의한 종료 (상대도 FIN을 보내야 완전히 끝남) | 일방적 중단 |
| 방향 | 방향별로 따로 닫힘 (반쪽 종료 가능) | 양방향 동시에 끝남 |
| 상대의 응답 | ACK로 확인 | 원칙적으로 응답하지 않음 (예외: 2-3의 Challenge ACK) |
| 남은 데이터 | 전송 중인 데이터는 끝까지 전달 | 송·수신 버퍼의 데이터 폐기 |
| 시퀀스 번호 | 1개 소비 (SYN과 동일) | 소비하지 않음 |
| 애플리케이션이 보는 결과 | 읽기 시 EOF(0바이트) | Connection reset by peer(ECONNRESET), 연결 시도 중이면 Connection refused |
| 상황 | 누가 보내는가 | 예 |
|---|---|---|
| 닫힌 포트로 SYN 도착 | 대상 호스트 커널 | 08편의 Connection refused |
| 존재하지 않는 연결로 세그먼트 도착 | 수신 호스트 커널 | 서버 재부팅 후 클라이언트가 옛 연결로 데이터 전송 |
| 애플리케이션의 강제 종료 | 해당 호스트 커널 | SO_LINGER 0초 설정 후 close(), 수신 버퍼에 읽지 않은 데이터가 남은 상태에서 close() |
| 방화벽 reject 정책 | 방화벽 | iptables/nftables의 reject with tcp reset 계열 규칙 |
| IPS·보안 장비의 차단 | 중간 장비 (양쪽 모두에게 주입하기도 함) | 악성 시그니처 탐지 후 세션 끊기 |
| 상태 테이블에서 만료된 세션 | 방화벽·로드밸런서 (장비·설정에 따라 다름) | 장시간 유휴 연결 후 데이터 전송 |
아무 RST나 받아들이면 제3자가 남의 연결을 쉽게 끊을 수 있습니다. 그래서 수신 측은 RST의 시퀀스 번호가 현재 수신 윈도우 안에 있는지를 검사합니다. 현재 Linux는 RFC 5961의 방식을 따라, 시퀀스 번호가 기대값과 정확히 일치할 때만 즉시 연결을 끊고, 윈도우 안이지만 일치하지 않으면 Challenge ACK를 보내 진짜 상대인지 확인합니다. 윈도우 밖이면 무시합니다.

그림 1. FIN 4-Way 종료와 RST 즉시 종료의 차이
클라이언트가 먼저 닫는(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개로 보이는 경우가 흔합니다. ②와 ③ 사이에 시간이 벌어져 있다면 서버가 받은 뒤에도 데이터를 보내고 있었다는 뜻입니다(반쪽 종료).
클라이언트 서버
(ESTABLISHED) (ESTABLISHED)
│ 데이터 주고받는 중 │
│ ① RST seq=기대값 │
│ ─────────────────────────────────────────────→ │
(CLOSED) ← 즉시 (CLOSED) ← 즉시, 앱에는 ECONNRESET
버퍼 데이터 폐기, 응답 없음, TIME-WAIT 없음
RST 방식은 TIME-WAIT 상태를 남기지 않고 바로 끝납니다. 그래서 대량 연결을 처리하는 일부 클라이언트·로드밸런서가 의도적으로 RST로 연결을 정리하기도 합니다. 이것이 "RST = 공격 또는 장애"라고 단정하면 안 되는 이유입니다.
실습 환경: 본인 소유 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
# [서버 터미널 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
서버에서 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()"
# 종료 진행 중인 소켓 (상태 상세는 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 수 |
TcpEstabResets | ESTABLISHED/CLOSE-WAIT 상태에서 RST로 끊긴 연결 수 |
TcpExtTCPAbortOnData | 예상치 못한 데이터가 와서 RST로 중단한 수 |
TcpExtTCPAbortOnClose | 읽지 않은 데이터가 남은 채 닫혀 RST를 보낸 수 |
TcpExtTCPAbortOnTimeout | 재전송 타임아웃으로 중단된 연결 수 |
📷 [실습 화면 삽입] FIN 종료 캡처 —
Flags [F.]두 줄과 이어지는 ACK
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 관련 메시지 (구현에 따라) |
TcpOutRsts 카운터가 실습 전후로 증가한 것을 확인했다📷 [실습 화면 삽입] RST 종료 캡처 —
Flags [R.]한 줄과ss -tan state time-wait결과 비교
| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| 서버가 응답 직후 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를 보낸 쪽이 어디인지, 같은 패턴이 반복되는지를 함께 봐야 합니다.
흔적이 남는 곳
| 위치 | 종료 방식이 남는 형태 |
|---|---|
| 방화벽 세션 로그 | 세션 종료 사유 필드 (예: FIN에 의한 종료, RST에 의한 종료, 에이징(타임아웃), 정책 차단 등 — 표기는 장비마다 다름) |
| IPS 로그 | 차단 액션 종류 (drop / reset 등). reset 액션이면 양방향 RST 주입 흔적이 패킷에도 남음 |
Zeek conn.log | conn_state: SF(정상 성립·정상 종료), RSTO(출발지가 RST로 중단), RSTR(응답자가 RST로 중단), REJ(연결 시도가 거부됨), S1(성립 후 종료 미확인) 등 |
| 호스트 | nstat 누적 카운터, 애플리케이션 에러 로그 (Connection reset by peer) |
한계와 오탐 주의