소켓은 열렸는데 데이터는 죽어 있다
한 변전소 게이트웨이가 RTU와 깨끗한 TCP 세션을 물고 있었다. 포트 2404, ESTABLISHED, 패킷 캡처에 재전송도 없었다. 그런데도 SCADA는 모든 차단기와 모든 아날로그를 정지 상태로 보여줬다. 시운전 엔지니어는 방화벽과 IP 계획을 확인하며 한 시간을 썼다. 둘 다 멀쩡했다. 링크는 TCP가 올라왔다는 의미에서만 올라와 있었고, 그 외의 어떤 의미로도 올라와 있지 않았다.
IEC 60870-5-104는 Modbus나 DNP3가 이렇게까지 강제하지 않는 시작 시퀀스를 가지고 있다. 순수 TCP 연결은 제어국이 명시적으로 데이터 흐름을 켜고, 그다음 스냅샷을 요청하기 전까지는 아무것도 실어 나르지 않는다. 두 단계 중 하나라도 건너뛰면 정확히 이 증상이 나온다. 완벽해 보이는 소켓과, 아무것도 없는 데이터베이스.
STARTDT가 켜짐 스위치다
104 프로토콜은 모든 것을 0x68, 길이 바이트, 그리고 제어 필드 4옥텟으로 시작하는 APDU로 감싼다. 이 4옥텟이 프레임 종류를 결정한다. 세 종류가 있다.
- I-포맷 — 정보 전송. ASDU(실제 데이터)를 실어 나른다. 송신·수신 시퀀스 카운트로 번호가 매겨진다.
- S-포맷 — 감독(supervisory). 페이로드 없는 순수 확인 응답. 보낼 자기 데이터가 없을 때 수신 측이 "시퀀스 N까지 받았다"고 말하는 방법이다.
- U-포맷 — 번호 없는 제어. STARTDT, STOPDT, TESTFR이 여기 속한다.
TCP 핸드셰이크가 끝나도 피제어국(RTU)은 스스로 아무것도 보내지 않는다. 기다린다. 제어국이 먼저 STARTDT act — U-포맷 프레임 — 을 보내야 하고, RTU가 STARTDT con으로 답한다. 이 확인이 온 뒤에야 RTU는 감시 방향 데이터를 밀어 올릴 수 있다. 그 전에 RTU가 I-포맷 프레임을 보내면 프로토콜 위반이므로, 제대로 만든 RTU는 그냥 조용히 있는다.
바로 그 침묵이 함정이다. 아무 에러도 안 난다. 캡처에는 SYN/SYN-ACK만 있고 그다음은 무음이다. 마스터에서 나가는 0x68 ... 07 00 00 00(STARTDT act)이 캡처에 없다면 그게 버그다. 마스터가 링크를 무장(arm)시키지 않은 것이다. 어떤 게이트웨이는 설정 플래그가 켜져야만 STARTDT를 보내고, 어떤 것은 한 번만 보내고 소켓이 끊긴 뒤에는 다시 보내지 않는다. RTU가 소켓을 리셋했고 마스터가 TCP는 재연결했지만 STARTDT를 다시 보내지 않았다면, 다시 살아 있는 소켓에 데이터 없음 상태로 돌아온다.
그다음 스냅샷을 요청해야 한다
STARTDT는 자발적(spontaneous) 변화를 통과시킨다. 차단기가 트립하면 RTU가 전송 원인(COT) 3(자발)으로 그 변화를 보낸다. 하지만 시운전 시점에는 아직 아무것도 변하지 않았으니 자발 데이터가 도착할 리 없고, 데이터베이스는 여전히 현재값이 비어 있다.
현재 상태는 **총괄 심문(general interrogation, GI)**을 보내서 받는다. 유형 식별 100번 ASDU, C_IC_NA_1, 전송 원인 6(활성화, activation), 그리고 국(局) 전체 요청을 뜻하는 심문 한정자(QOI) 20을 실어 보낸다. RTU는 정해진 순서로 응답하는데, 캡처에서 전체 교환을 읽을 수 있으니 외워둘 만하다.
- COT 7 — 활성화 확인. "GI 받았고 지금 시작한다."
- 모든 포인트를 실은 I-포맷 ASDU의 연속, 각각 COT 20(국에 의해 심문됨, 흔히 "inrogen"으로 표기). 이게 스냅샷이다.
- COT 10 — 활성화 종료. "이게 전부다."
GI를 보냈는데 COT 7만 오고 COT 20 값이 안 오면 RTU의 포인트 목록이 비었거나 주소가 잘못된 것이다. COT 7 자체가 안 오면 GI가 도달하지 못한 것이니 ASDU 공통 주소(아래)를 확인하라. 많은 마스터가 STARTDT con 직후 자동으로 GI를 쏘지만, 어떤 것은 안 하고, 어떤 것은 콜드 재연결에서만 다시 쏘고 웜 재연결에서는 안 쏜다. "링크는 돌아왔는데 값이 안 돌아온다"면 보통 재연결 후 GI 누락이 원인이다. 수동으로 GI를 하나 강제하고 COT 7 / COT 20 / COT 10 삼박자를 지켜봐라.
전송 원인이 무엇이 틀렸는지 알려준다
COT 옥텟은 104 문제 해결에서 가장 유용한 단일 필드인데, 사람들이 무시한다. 번호로 알아둘 만한 것 몇 개:
- 1 주기적, 3 자발, 5 요청됨 — 정상 트래픽.
- 6 활성화, 7 활성화 확인, 10 활성화 종료 — 명령/심문 생명주기.
- 44 알 수 없는 유형 식별, 45 알 수 없는 전송 원인, 46 알 수 없는 ASDU 공통 주소, 47 알 수 없는 정보 객체 주소(IOA).
이 마지막 네 개가 금맥이다. 명령을 보냈는데 RTU가 COT 46으로 되튕기면 ASDU 공통 주소가 틀린 것이다 — 마스터와 RTU가 국 번호를 서로 다르게 알고 있다. COT 47은 공통 주소는 맞았지만 정보 객체 주소(IOA)가 안 맞았다는 뜻이다. 포인트 맵이 오프셋만큼 어긋났거나 IOA 바이트 순서를 바꿔 넣었다. P/N 비트(COT 옥텟의 비트 7)도 있다. 세팅되면 부정 확인 — RTU가 이해했지만 거부한 것이다. 선택(select)이나 GI에 대한 부정 COT 7은 요청이 구조적으로는 유효했지만 수락되지 않았다는 RTU의 신호이며, 이는 주소 불일치와는 다른 문제로, 주소가 아니라 권한·모드·바쁜 리소스를 가리킨다.
주소 문제가 실제로 104 시운전 시간의 대부분을 잡아먹는 곳이다. ASDU 공통 주소는 보통 2옥텟, IOA는 보통 3옥텟이며, 둘 다 폭이 설정 가능하다. IOA를 2옥텟으로 기본 설정한 게이트웨이가 3옥텟을 쓰는 RTU와 통신하면 모든 객체를 잘못된 오프셋에서 파싱해서, 그럴듯하지만 틀린 값을 보거나 COT 47 홍수를 보게 된다. 포인트 목록을 두고 다투기 전에 양쪽 끝의 옥텟 폭부터 확인하라.
킵얼라이브 타이머에 당하지 마라
104에는 타임아웃이 넷 있고, 기본값이 중요하다. 잘못 맞추면 멀쩡한 링크를 끊어버리기 때문이다.
- t1 = 15 s — 송신 측이 I-프레임이나 U-프레임의 확인을 기다리다가 링크가 죽었다고 선언하고 TCP 소켓을 닫기까지의 시간.
- t2 = 10 s — 수신 측이 확인할 데이터는 있지만 보낼 자기 데이터가 없을 때, S-프레임 확인을 보내기 전에 기다리는 시간. t2는 t1보다 짧아야 한다. 안 그러면 상대가 아직 보내기로 결정하지 않은 확인을 기다리다 타임아웃 난다.
- t3 = 20 s — 유휴 시간이 이만큼 지나면 링크가 살아 있는지 증명하기 위해 TESTFR act를 보낸다. 상대는 TESTFR con으로 답한다.
- k = 12, w = 8 — 송신/수신 윈도우. k는 멈추고 기다려야 하기 전까지 미확인 상태로 둘 수 있는 I-프레임의 최대 개수, w는 수신 측이 반드시 확인을 보내야 하는 수신 I-프레임 개수다. w를 k의 약 3분의 2로 유지하라. w를 k에 비해 너무 높게 잡으면 송신 측이 k 한계에 도달해, 수신 측이 아직 보낼 의무가 없는 확인을 기다리며 멈춘다 — 느린 링크처럼 보이는 자업자득 교착이다.
전형적인 현장 실수는 느린 무선이나 셀룰러 백홀의 왕복 시간보다 t1을 짧게 잡는 것이다. 링크가 잘 돌아가다가, 부하가 걸리면 확인이 16초에 도착하고, t1이 15초에 발화하고, 마스터가 멀쩡한 소켓을 헐어 재연결한다 — 경로가 바빠질 때마다 데이터를 흘린다. 깨끗한 LAN이 아닌 이상, 기본 t1을 믿기 전에 실제 RTT를 측정하라.
하나만 기억한다면: 104 소켓이 열려 있다는 사실은 거의 아무것도 말해주지 않는다. STARTDT con, COT 7 / 20 / 10 총괄 심문, 그리고 서로 맞는 공통 주소·IOA 폭이 링크가 진짜라고 말해준다. 방화벽을 다시 건드리기 전에 이 넷부터 확인하라.