SSH가 키 입력 한 번에 패킷 100개를 보낸다고? (feat. OpenSSH의 과잉 친절)
최근 Hacker News에서 꽤 흥미로운 디버깅 스토리가 올라왔습니다. 제목부터 자극적입니다. “Why does SSH send 100 packets per keystroke?”
보통 SSH 세션에서 키 하나를 누르면 패킷이 몇 개나 오갈까요? 상식적으로는 요청(키 입력) 하나, 응답(Echo) 하나, 그리고 TCP ACK 몇 개 정도면 충분할 겁니다. 그런데 키 하나에 패킷 100개라니요. 이건 단순히 비효율적인 수준이 아니라, 네트워크 스택 어딘가가 고장 났다는 신호입니다.
오늘은 이 현상의 원인을 파헤치면서, Security by Default 의 그림자, 그리고 LLM을 활용한 현대적인 디버깅 방식에 대해 이야기해 보겠습니다.
1. 미스터리: 50% CPU 사용량의 비밀
이 글의 작성자는 SSH 위에서 돌아가는 TUI(Text User Interface) 게임을 개발 중이었습니다. Go 언어 기반의 bubbletea와 wish 라이브러리를 사용해서 초당 1000만 개의 셀을 업데이트하는 고성능 게임이었죠.
테스트 도중 실수로 게임 데이터 대신 “화면이 너무 작습니다”라는 정적 에러 메시지만 보내도록 코드를 망가뜨렸습니다. 당연히 렌더링 부하가 없으니 CPU 사용량이 0에 수렴해야 정상인데, 놀랍게도 50%만 감소 했습니다.
데이터를 안 보내는데 왜 CPU가 돌까요? tcpdump를 찍어봤더니 충격적인 결과가 나옵니다.
- 패킷 수: 키 입력 한 번에 약 270개 패킷 발생
- 데이터: 36바이트짜리 미스터리 패킷이 전체의 66% 차지
- 패턴: 약 20ms 간격으로 서버와 클라이언트가 핑퐁을 주고받음
단순한 SSH 연결인데, 마치 DoS 공격이라도 하듯이 패킷을 쏟아내고 있었던 겁니다.
2. 범인은 OpenSSH의 ‘과잉 친절’
작성자가 ssh -vvv (verbose mode)로 디버깅한 결과, 범인은 2023년 OpenSSH에 도입된 기능이었습니다.
debug3: obfuscate_keystroke_timing: starting: interval ~20ms
바로 Keystroke Timing Obfuscation 기능입니다. 사용자가 키보드를 타이핑하는 속도(Inter-keystroke timing)를 분석하면, 입력하는 문자가 무엇인지 추측할 수 있다는 보안 연구(Side-channel attack) 때문에 추가된 기능입니다. 이를 막기 위해 SSH는 실제 키 입력 사이에 가짜 패킷(Chaff packets)을 섞어서 공격자가 타이밍을 분석하지 못하게 만듭니다.
보안 관점에서는 훌륭합니다. 하지만 Latency 와 Throughput 이 생명인 실시간 게임 서버 입장에서는 재앙입니다. 20ms마다 의미 없는 데이터를 주고받느라 대역폭과 CPU를 낭비하고 있었으니까요.
3. 해결책: 라이브러리 Fork라는 과감한 선택
클라이언트 설정에서 ObscureKeystrokeTiming=no를 주면 해결되지만, 게임 유저들에게 “설정 파일 열어서 옵션 끄고 접속하세요”라고 할 수는 없습니다. 서버 사이드에서 해결해야 했습니다.
작성자는 Go의 SSH 라이브러리(x/crypto/ssh)를 뜯어봤습니다. 이 ‘Chaff’ 패킷은 SSH2_MSG_PING이라는 메시지를 사용하고, 이는 ping@openssh.com이라는 익스텐션을 통해 활성화됩니다.
해결책은 의외로 간단했습니다.
- 방법: Go SSH 라이브러리를 Fork 뜬 후, 서버가 핸드쉐이크 과정에서
ping@openssh.com익스텐션을 광고하지 않도록 수정. - 결과: CPU 사용량 30% -> 11% 급감, 대역폭 사용량 절반 감소.
Principal Engineer로서 이 결정을 평가하자면, “Risk는 있지만 정당한(Justifiable) 선택” 입니다. 암호화 라이브러리를 포크해서 유지보수하는 건 보통 금기시되지만, 업스트림에서 옵션을 제공하지 않는 상황에서 비즈니스 요구사항(성능)을 맞추기 위한 가장 현실적인 엔지니어링이었습니다.
4. LLM 디버깅: 환각과 통찰 사이
이 글에서 또 하나 흥미로운 점은 디버깅 과정에서 Claude Code 와 ChatGPT 를 활용했다는 점입니다.
- ChatGPT:
tcpdump결과를 보고 “이건 정상적인 SSH 흐름 제어(Flow control)입니다”라고 뻔뻔하게 거짓말(Hallucination)을 했습니다. 전형적인 “아는 척하는 신입 사원” 모드였죠. - Claude Code: 반면 Claude는
obfuscate_keystroke_timing로그를 보고 정확한 원인을 짚어냈고, Go 라이브러리 패치까지 제안했습니다.
작성자는 “LLM이 문제를 다 해결해주진 않았지만, 훌륭한 러버 덕(Rubber Duck) 역할을 했다”고 평합니다. 저도 동의합니다. LLM은 주니어 엔지니어처럼 다뤄야 합니다. 방향을 제시할 순 있지만, 검증은 시니어(당신)의 몫입니다.
5. Hacker News의 반응과 나의 생각
Hacker News 댓글 창은 역시나 뜨거웠습니다. 주요 논쟁점은 두 가지였습니다.
첫째, “Secure by Default가 항상 옳은가?” 보안론자들은 “안전한 게 기본이어야 한다”고 주장하지만, 임베디드나 게임처럼 리소스가 제한된 환경에서는 이런 오버헤드가 치명적입니다. 사용자가 선택할 수 있는 옵션(Opt-out)을 제공하지 않은 OpenSSH 팀의 결정은 다소 아쉽습니다. 보안은 중요하지만, Usability 를 해치는 보안은 결국 우회(Bypass)를 낳기 마련이니까요.
둘째, “Wireshark 놔두고 왜 LLM씀?” 일부 ‘고인물’들은 “Wireshark로 패킷 깠으면 1분 컷인데 요즘 애들은…”이라는 반응을 보였습니다. 하지만 저는 작성자의 방식이 더 현대적이라고 봅니다. 모든 프로토콜 스펙을 머릿속에 넣고 다닐 순 없습니다. LLM에게 초벌 분석을 맡기고, 그 단서를 바탕으로 깊게 파고드는 것이 훨씬 효율적입니다.
결론
이 사건은 우리가 무심코 사용하는 인프라 도구들(SSH 등)이 내부적으로 얼마나 복잡한 트레이드오프를 가지고 있는지 보여줍니다. “SSH는 그냥 안전한 쉘”이라고만 생각했다면, 그 뒤단에서 20ms마다 가짜 패킷을 쏘고 있는 엔지니어링 디테일을 놓쳤을 겁니다.
Key Takeaways:
- 기본값을 의심하라: 널리 쓰이는 도구라도 내 유스케이스(게임)와 맞지 않는 기본 설정(보안 난독화)이 있을 수 있습니다.
- 도구를 가리지 마라: Wireshark든 LLM이든 문제를 해결하는 데 도움이 된다면 적극적으로 사용하세요. 단, 검증은 필수입니다.
- 오픈소스의 힘: 문제가 생기면 코드를 까보고, 필요하다면 Fork 할 수 있는 용기가 필요합니다.
여러분의 서버는 지금 불필요한 패킷을 쏘고 있지 않나요? 오늘 tcpdump 한번 돌려보시죠.