← 문제로

37. 재접속 재바인딩 시 이전 연결 미정리로 인한 유령 연결 (C#)

난이도 상
내 리뷰 · C#
해설 · C#

해설 — 재접속 재바인딩 시 이전 연결 미정리로 인한 유령 연결 (C#)

난이도: 상

요약

Reconnect는 세션에 새 연결을 매달기만 하고(A) 이전 연결을 닫거나 분리하지 않는다. 이전 연결의 수신 루프는 여전히 살아 있어 BoundSession이 같은 세션을 가리키므로(B), 두 물리 연결이 하나의 세션에 동시에 패킷을 흘려보낸다. "세션당 활성 연결 1개" 불변식이 깨져 중복 처리·상태 오염·인증 우회가 발생한다.

문제점

  • 분류: 상태 전이 불변식 위반(이중 바인딩) + 리소스/수명 관리 누락 + ABA류 경합.
  • 증상:
    1. 이중 입력: 옛 연결과 새 연결이 동시에 session.Handle을 호출 → 같은 명령이 두 번 적용(이중 공격/이중 이동), 또는 서로 다른 두 소스의 명령이 인터리브되어 세션 상태가 뒤엉킴.
    2. 유령 연결 누수: 옛 ConnectionClose되지 않아 소켓·수신 스레드·버퍼가 계속 살아 있다(리소스 누수). 유예 만료로 세션이 정리돼도 옛 연결은 남는다.
    3. 응답 오배송: session.Conn은 새 연결을 가리키지만 옛 연결도 여전히 세션을 참조하므로, 세션이 내려보내는 응답과 옛 연결이 올려보내는 요청의 대상이 어긋난다.
    4. 하이재킹 위험: 옛 연결(예: 탈취된 소켓)이 유효 토큰 없이도 계속 세션을 조작할 수 있다.
  • 재현 조건: 옛 연결의 수신 루프가 아직 안 끝난 시점에 재접속이 들어옴(정확히 모바일 재접속의 전형적 타이밍). 또한 두 재접속이 거의 동시에 오면 _byToken 자체도 무보호라 사전 접근 레이스.
  • 근본 원인: 재바인딩을 "포인터 교체"로만 처리하고, (a) 이전 연결의 세션 참조 해제, (b) 이전 연결 닫기, (c) 교체 구간의 원자적 배제, (d) 토큰 재발급/일회성 을 모두 빠뜨림.

수정안

public bool Reconnect(string token, Connection newConn)
{
    Connection old = null;
    lock (_gate)                                   // 교체 구간 원자화
    {
        if (!_byToken.TryGetValue(token, out var session))
            return false;

        old = session.Conn;                        // 이전 연결 회수
        if (old != null) old.BoundSession = null;  // (1) 이전 연결의 세션 참조 끊기

        session.Conn = newConn;                    // (2) 새 연결로 교체
        newConn.BoundSession = session;

        _byToken[token] = RotateToken(session);    // (3) 토큰 일회성: 재발급
    }
    old?.Close();                                  // (4) 락 밖에서 이전 연결 닫기
    return true;
}

그리고 OnPacket끊긴 참조를 확인해 유령 입력을 막는다:

public void OnPacket(byte[] data)
{
    var s = BoundSession;                 // 지역 스냅샷
    if (s == null || s.Conn != this) return;   // 나는 더 이상 이 세션의 활성 연결이 아니다
    s.Handle(data);
}

핵심: ① 이전 연결의 BoundSession을 끊고 닫아 유령 입력·누수 제거, ② 교체를 락으로 원자화, ③ Conn != this 재확인으로 경합 잔여 패킷 차단, ④ 토큰 회전(one-time) 으로 옛 토큰 재사용 하이재킹 방지.

더 나은 설계

  • 세대(generation) 토큰: 각 바인딩에 epoch를 부여해 옛 연결이 올린 패킷은 epoch 불일치로 O(1) 폐기. Conn != this 체크의 일반화.
  • 명시적 상태 머신: SessionActive / Suspended / Rebinding / Closed로 두고, 전이 중(Rebinding)에는 양쪽 연결의 입력을 모두 큐잉/차단해 인터리빙을 원천 봉쇄.
  • 드레이닝: 이전 연결을 즉시 Close하기 전에 in-flight 송신을 짧게 flush하고 RST 대신 graceful하게 종료(별도 half-close 문제와 연계). 트레이드오프: 즉시성 대 마지막 응답 보존.
  • 유예 타이머 취소: 재바인딩 성공 시 세션 파기 유예 타이머를 반드시 취소(안 하면 재접속 직후 만료로 세션이 사라지는 별도 결함).

면접 포인트

  • "재접속을 포인터 교체로만 처리하면?" → 옛 연결이 세션을 계속 참조/조작하는 이중 바인딩. 교체는 항상 이전 소유권 회수(끊기+닫기) 와 쌍이어야 한다.
  • "재접속 토큰을 재사용 가능하게 두면?" → 탈취·재생 공격. 성공 시 즉시 회전시켜 일회성으로 만든다.