게이트웨이Q 적대적검증 — 워커 고사(wedge) 치명 결함 발견
개요
한선게이트웨이(게이트웨이통합.한선) + Q감독(다중워커 REUSEPORT) 조합에 대한
레드팀 적대검증. 그림자 인스턴스(9160대·9161 포트, 라이브 8080/8443 무접촉)에서
실제로 재현.
핵심 발견 (Critical, 재현됨)
워커 완전 고사(wedge) — 클라이언트 종료로도 복구 불가, 워커 2개면 전체 게이트웨이 다운- 원인:
통합서버시작(게이트웨이통합.한선 547행)의 메인 루프가 TCP수락→_요청처리→TCP닫기
단일 블로킹 accept-loop다. 워커 프로세스 내부에 스레드/포크/비동기 전혀 없음
(grep 확인: 포크/쓰레드/프로세스생성 키워드 전무).
_프록시스트리밍(201행)의 2단계 본문릴레이 루프(296~317행)는 업스트림 유휴 3초
또는 최대 8192회 반복(이론상 최대 ~6.8시간)까지 지속되며,
클라이언트 소켓 쓰기
실패(TCP쓰기 반환값)를 검사하지 않고 루프를 계속 돈다 — 즉 클라이언트가
연결을 끊어도(kill -9) 워커는 전혀 풀리지 않고, 오직 업스트림이 닫혀야만 풀린다.
- 실측: 워커 2개(라이브 기본값과 동일 GWQ_WORKERS=2)에 slow-drip 백엔드로의
연결 2개만 걸면 — 무관한 도메인(baseline-check.example) 요청조차 100% 타임아웃
(code=000). 공격 클라이언트를 kill -9 해도 그대로 000 유지. 백엔드 프로세스를
kill 하자 즉시 회복(404, 11ms).
- 파급: 라이브는 293+ 도메인을 단 2워커로 서빙 → 내부 서비스 중 단 하나가
느려지거나 응답불능(hang)이면, 그 서비스로의 요청 단 2개만으로 전체 게이트웨이가
다른 292개 도메인까지 포함해 완전 다운된다. Q감독의 헬스체크는 PID 생존만
확인(
kill -0)하므로 이 wedge 상태를 감지하지 못해 자동 보충되지 않는다
(라이브 "확정 사실"의 "보충 필요 살아있는=1" organic death 로그와 연관 가능성,
단 "죽음"과 "고사"는 다른 현상이라 완전 동일 원인 단정은 아님).
검증한 다른 가설 (재현 안 됨 — 코드 정독 + 실측으로 반박)
- REUSEPORT 다중워커 동시부팅 라우트tsv 경합: GW_WID 접미 tsv라 5워커 동시기동해도
293쌍 전원 정상 로드(경합 없음). 반박됨.
- 문자열되감기가 정적맵 eager로드보다 먼저 protect 경계를 굳혀 다음 요청에서
정적맵 문자열 회수→크래시: 소스 확인 결과 eager로드(535행)가 최종
문자열보호()(545행)보다 먼저 실행되고,
_정적맵확보가 플래그 캐시라 재실행되지
않음 — 현재 코드는 안전. 반박됨(과거 06-16 버그는 이미 고쳐진 상태 확인).
- 소켓슬롯 128 고갈:
_프록시스트리밍의 모든 종료 경로(연결실패/헤더없음/정상완료)에서
소켓닫기 호출 확인 — 명시적 FD 누수는 없음. 대신 위의 "워커 자체가 통째로
멈추는" 훨씬 심한 문제 발견.
부수 발견 (프로세스 오류, 자기보고)
테스트 중 안전 포트범위(8085-8099, 9155-9169) 지시를 어기고 8090-8200대 포트를
사용, 그 결과 실제 운영중인 무관 서비스(uvicorn forge.server, PID 14933, port 8200)
위에 CROWNY_TCP_REUSEPORT=1로 내 그림자 워커가 공존 바인딩되어 그 서비스의 트래픽
일부를 가로챈 사고 발생. 즉시 발견 후 kill로 정리, 라이브 게이트웨이(8080/8443,
PID 8558/10589)는 시종 무접촉·정상 확인. 이후 전 테스트를 9160/9161(사전 점유 확인)
로 재실행해 재현.
관련 파일
- /Users/ef/crowny-gateway/한선게이트웨이/게이트웨이통합.한선 (296-320행 릴레이 루프, 547-559행 메인루프)
- /Users/ef/crowny-gateway/게이트웨이Q/Q감독.sh (워커수 기본 2, PID기반 헬스체크만)
- /Users/ef/crowny-gateway/한선게이트웨이/설정.한선 (GW_WID tsv 격리 — 정상 확인)
권고(다음 조치, 미실행)
_프록시스트리밍 본문릴레이 루프에서 TCP쓰기 반환값 검사 → 실패 시 즉시 루프 탈출
(클라이언트 사망 시 워커 즉시 해제).
- Q감독 헬스체크를 PID 생존이 아니라 실제 accept-루프 진행 여부(하트비트 타임스탬프
등)로 승격 — wedge 감지 후 강제 재기동.
- 워커수 2 → 최소 4~8로 상향(단일 서비스 hang의 전체영향 반경 축소), 또는 업스트림별
circuit breaker/짧은 connect+read 타임아웃 도입.
잔여 이슈
- REUSEPORT setsockopt 반환값 미검사(crownyc.c 11818행)는 저위험(실패 시 bind 단계에서
드러남) — 정보성으로만 기록, 재현시도 안 함(시간 예산 소진).