← 전체 글
SECS/GEM/약 11분 읽기/— 조회

장비 포트 하나에 호스트가 둘 붙으면, 먼저 붙은 쪽은 끝까지 모른다

패시브 HSMS 리스너 하나에 호스트 둘을 붙인 실제 캡처. 둘 다 Select Status 0을 받고, 먼저 붙은 쪽은 두 번째가 붙은 걸 끝까지 모른다.

SECS/GEMMESSCADA문제 해결체크리스트

호스트 로그에는 이상이 없다. Linktest는 계속 정상으로 돌아오고 세션은 SELECTED다. 그런데 장비 화면에는 NOT CONNECTED라고 떠 있고 이벤트 리포트는 30분째 안 올라온다. 그 사이에 있었던 일은, 옆자리 엔지니어가 노트북으로 같은 포트에 붙었다가 닫은 것뿐이다.

패시브 HSMS 리스너 하나에 Host A와 Host B를 동시에 붙여서 잡은 캡처다. 둘 다 Select.req를 보냈고 둘 다 Select Status 0인 Select.rsp를 받았다. Host B가 Separate.req를 던지고 나가자 장비의 세션 상태는 NOT CONNECTED로 떨어졌는데, Host A의 Linktest.req는 그 뒤로도 계속 Linktest.rsp를 받았다. A는 끝까지 모른다.

패시브 HSMS 리스너 하나에 Host A와 Host B가 붙는다 — 둘 다 Select Status 0을 받고, Host B의 Separate.req 뒤에도 Host A의 Linktest는 응답을 받는다 Host A 장비 Host B Select.req Select.rsp · Select Status 0 Select.req Select.rsp · Select Status 0 Linktest.req / Linktest.rsp Linktest.req / Linktest.rsp Separate.req SELECTED NOT CONNECTED

리스너는 두 번째 연결을 거절하지 않았다

Host A가 먼저 붙는다. 아래 hex는 앞의 4바이트가 길이 헤더고, 그 뒤 10바이트가 SEMI E37이 Header Byte 0–9라고 부르는 자리다.

TCP  connect                                       → 127.0.0.1:5501
TX   00 00 00 0A | 00 0B 00 00 00 01 00 00 0A 01   Select.req
RX   00 00 00 0A | 00 0B 00 00 00 02 00 00 0A 01   Select.rsp
길이       00 00 00 0A   헤더 10바이트
Byte 0–1   00 0B         SessionID = 11
Byte 2     00            제어 메시지라 0
Byte 3     00            Select.rsp에서는 Select Status = 0, 수락
Byte 4     00            PType = SECS-II
Byte 5     01 → 02       SType, Select.req → Select.rsp
Byte 6–9   00 00 0A 01   SystemBytes, 요청 값 그대로 echo

여기까지는 교과서다. 이 시점에 장비의 세션 상태는 SELECTED.

이제 Host B가 같은 포트에 붙는다. 새 TCP 연결이고, A의 소켓은 열려 있다.

TCP  connect                                       → 127.0.0.1:5501   (두 번째 소켓)
TX   00 00 00 0A | 00 0B 00 00 00 01 00 00 0B 01   Select.req
RX   00 00 00 0A | 00 0B 00 00 00 02 00 00 0B 01   Select.rsp

SystemBytes가 00 00 0B 01로 다를 뿐, A가 받은 것과 같은 응답이다. Byte 3은 00. 거절이 아니라 수락이다. 호스트 둘이 같은 장비를 상대로 동시에 SELECTED 상태에 있다.

먼저 붙은 쪽에는 아무 신호도 없다

B가 붙은 직후, A가 keepalive를 던져 본다.

TX   00 00 00 0A | 00 0B 00 00 00 05 00 00 0A 03   Linktest.req   (Host A)
RX   00 00 00 0A | 00 0B 00 00 00 06 00 00 0A 03   Linktest.rsp

SType이 05 → 06, SystemBytes 00 00 0A 03은 그대로 돌아온다. A 입장에서 달라진 건 하나도 없다.

세션 건강을 Linktest로만 판단하는 호스트라면 — 대부분 그렇게 한다 — 여기서 알아낼 수 있는 게 없다. Linktest가 도는 건 소켓이 살아 있다는 뜻이지, 그 세션이 장비가 인정하는 세션이라는 뜻이 아니다. Linktest는 되는데 Select가 안 되는 경우와 같은 함정의 반대편이다.

B가 나가면서 A의 세션 상태를 가져간다

B가 Separate.req 한 발을 쏘고 소켓을 닫는다.

TX   00 00 00 0A | 00 0B 00 00 00 09 00 00 0B 02   Separate.req   (Host B)
     (장비가 B의 소켓을 닫는다)

직후 장비의 세션 상태는 NOT CONNECTED다. 그런데 A의 소켓은 그대로 열려 있다. 300 ms 뒤 A가 다시 Linktest를 보낸다.

TX   00 00 00 0A | 00 0B 00 00 00 05 00 00 0A 04   Linktest.req   (Host A)
RX   00 00 00 0A | 00 0B 00 00 00 06 00 00 0A 04   Linktest.rsp

답이 온다. 장비는 세션이 없다고 보는데 keepalive는 계속 돈다.

이 리스너가 세션 상태를 연결마다가 아니라 장비마다 하나씩 들고 있어서 생기는 일이다. 그게 이 구현만의 문제인지 아닌지는 호스트 입장에서 중요하지 않다. 중요한 건 두 번째 연결이 붙는 순간부터 A가 보는 상태와 장비가 보는 상태가 갈라졌고, A에게는 그 사실을 알아낼 수단이 프로토콜 안에 없다는 것이다. 원인 불명으로 남는 세션 사고의 상당수가 이 모양이다.

표준이 그어 둔 선

E37의 연결 상태 기계 — NOT CONNECTED / CONNECTED, 그리고 CONNECTED 아래의 NOT SELECTED와 SELECTED — 는 TCP 연결 하나를 기준으로 정의된다. GEM 장비가 실제로 쓰는 건 단일 세션 부속 표준인 E37.1(HSMS-SS)이고, 이름 그대로 세션은 하나다.

그러면 두 번째 Select.req는 거절되는 게 맞다. 거절할 자리도 이미 있다. Select.rsp의 Byte 3에 0이 아닌 Select Status를 실으면 된다. 어떤 코드가 "이미 세션이 있다"에 해당하는지는 E37의 표와 장비 매뉴얼을 봐야 하고, 나는 그 값을 도구로 확인하지 못했다. 확인한 건 이 리스너가 00을 돌려줬다는 것, 그리고 00은 거절이 아니라는 것뿐이다.

정리하면 호스트는 두 가지 장비를 만난다. 두 번째 Select를 거절하는 장비와, 조용히 받아 주는 장비. 어느 쪽인지 미리 알 방법은 없고, 뒤쪽이면 증상은 위 캡처 그대로다.

호스트 쪽에서 할 것

두 번째 연결이 되지 않는 게 유일한 확실한 방어다. EAP나 엔지니어링 툴이 MES 호스트와 같은 장비 포트를 공유하는 구성은 설계 단계에서 걸러야 한다. 나중에 "원인 불명 세션 끊김"으로 돌아오고, 그때는 로그에 아무것도 안 남아 있다.

방화벽에서 장비 포트로 갈 수 있는 출발지 IP를 호스트 하나로 못 박는 게 가장 싸게 먹힌다. 프로토콜이 못 막는 걸 네트워크가 막는다. 이건 취향 문제가 아니다 — HSMS에는 인증이 없고, 포트에 닿을 수 있으면 Select.req를 보낼 수 있다.

호스트 스택 쪽으로는 두 가지.

  • Linktest를 세션 증명으로 쓰지 마라. 살아 있다는 증거일 뿐이다. 주기적으로 W-bit을 세운 데이터 메시지를 한 발 보내 응답을 확인하는 편이 훨씬 낫다.
  • 연결 이벤트에 peer 주소를 남겨라. 장비 쪽 연결 로그를 볼 수 있다면 첫 질문은 "그 시각에 다른 IP가 붙었나"다. 이걸 안 찍는 호스트 스택이 너무 많아서, 정작 물어볼 때 대답할 근거가 없다.

장비가 Separate.req로 세션을 정상 종료했는지, 아니면 남이 대신 끊어 간 것인지는 결국 두 쪽 로그를 시각으로 맞춰 봐야 갈린다.

재현

캡처는 전부 SECS/GEM 시뮬레이터의 EQ1 패시브 리스너(포트 5501)에 실제 소켓 두 개를 붙여서 잡았다. 장비 쪽 로그에 TCP 연결이 두 번, RX가 프레임마다 찍힌 것으로 와이어에 올라간 바이트임을 확인했다. 원본은 content/demos/hsms-two-hosts-one-listener.json에 있다.

같은 걸 해 보려면 순서는 이렇다. 소켓 A로 붙어 Select 하고, 소켓 A는 열어 둔 채 소켓 B로 같은 포트에 붙어 Select 한다. 그 다음 소켓 A로 Linktest를 보내 본다. 여러분 호스트 스택이 이 상황에서 무슨 로그를 남기는지 — 아마 아무것도 안 남길 것이다 — 확인하는 게 이 실험의 요점이다.