발행일

서버 로그에 찍힌 16진수 메서드 이름의 정체 — wss 삽질의 서버 쪽 이야기

서버 로그에 찍힌 16진수 메서드 이름의 정체

wss 연결 실패를 디버깅하던 날, 백엔드 담당 팀원이 서버 로그를 공유해 줬습니다. 프런트 쪽에서는 연결이 그냥 실패했다고만 나오는데, 서버에는 이상한 게 찍혀 있었어요.

o.apache.coyote.http11.Http11Processor : Error parsing HTTP request header
java.lang.IllegalArgumentException: Invalid character found in method name
[0x16, 0x03, 0x01, ...]. HTTP method names must be tokens

HTTP 메서드 이름이라면 GET, POST 같은 문자열이어야 하는데 16진수 바이트가 메서드 자리에 들어와 있습니다. 처음 든 생각은 "외부에서 악성 트래픽이 들어오나?"였습니다. 결론부터 말하면 공격이 아니라 우리 클라이언트가 보낸 TLS 핸드셰이크였습니다.


1. 0x16 0x03 0x01은 TLS의 첫인사다

wss://(또는 https://)로 연결하면 클라이언트는 데이터를 보내기 전에 TLS 핸드셰이크부터 시작합니다. 그 첫 패킷(ClientHello)의 앞부분이 정확히 이 바이트들입니다.

바이트의미
0x16TLS 레코드 타입: Handshake
0x03 0x01프로토콜 버전 (TLS 1.0 표기 — 호환성 때문에 첫 레코드에 이렇게 적는 관례)

그런데 받는 쪽 포트는 평문 HTTP만 처리하는 Tomcat이었습니다. Tomcat은 들어온 바이트를 HTTP 요청으로 읽으려 하고, 요청의 첫 토큰(메서드 이름) 자리에서 0x16을 만나 "메서드 이름에 유효하지 않은 문자"라며 죽습니다. 즉 이 로그는:

**"암호화된 인사를 평문만 아는 서버에 건넸다"**는 뜻입니다. 클라이언트 잘못도 서버 잘못도 아니고, 양쪽 프로토콜이 안 맞는다는 신호예요.

이걸 알고 나니 반대 방향의 대응표도 만들 수 있었습니다. 프로토콜 불일치는 어느 쪽이 틀렸느냐에 따라 에러가 다른 곳에 찍힙니다.

클라이언트서버증상이 찍히는 곳
wss://HTTP만 지원서버 로그 — 위의 16진수 메서드 에러
ws://HTTPS 페이지에서브라우저 콘솔 — Mixed Content 차단 (서버엔 아무것도 안 옴)
ws://HTTPS만 지원(리다이렉트)클라이언트 — 핸드셰이크 중 연결 종료

우리는 이 표의 1행과 2행을 동시에 겪고 있었습니다. https 페이지라 ws://는 브라우저가 막고, 그래서 wss://로 바꾸면 서버가 TLS를 못 받는 상태. 클라이언트만 고쳐서는 풀 수 없는 교착이었고, 서버에 TLS를 얹어야만 끝나는 문제였습니다.


2. 서버 쪽 선택지 — 직접 TLS vs 프록시

서버가 wss://를 받게 하는 방법은 두 갈래였습니다. 서버 작업은 백엔드 담당 팀원 몫이었지만, 어느 쪽으로 갈지는 함께 정리했습니다.

방법장점포기하는 것판단
Spring Boot에 직접 TLS (server.ssl.*)구성 요소 추가 없음, 설정 몇 줄인증서 갱신 때마다 앱 재배포, 포트별 인증서 관리2인 팀 초기엔 이걸로
Nginx 프록시가 TLS 종료 후 백엔드에 평문 전달인증서 관리 일원화, 앱 무관 갱신구성 요소 하나 추가, 프록시의 WebSocket 업그레이드 설정(Upgrade/Connection 헤더) 필요운영이 길어지면 이쪽이 정석

당시엔 빠른 쪽(직접 TLS)을 택했습니다. Spring Boot 기준 설정은 이 정도입니다:

server.port=443
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=<password>
server.ssl.key-store-type=PKCS12

프록시 방식을 미룬 대가는 예상대로 인증서 갱신 운영으로 돌아왔습니다. 앱에 인증서가 박혀 있으니 갱신이 곧 재배포였어요.


3. 재발 시 판별 절차

같은 계열의 문제를 다시 만나면, 로그가 찍힌 위치부터 봅니다.

  1. 브라우저 콘솔에 Mixed Content가 있다 → 클라이언트가 ws://를 https 페이지에서 쏘는 중. 서버는 볼 필요 없음.
  2. 서버 로그에 16진수 메서드 에러가 있다 → 클라이언트는 TLS(wss://)로 말하는데 서버 포트가 평문. 서버에 TLS를 얹거나 프록시 필요.
  3. 양쪽 다 조용한데 연결만 끊긴다 → 그때 중간 장비(프록시의 업그레이드 헤더, 로드밸런서 타임아웃)를 의심.
# 포트가 TLS를 받는지 즉석 확인 — 인증서 체인이 나오면 TLS, 즉시 끊기면 평문
openssl s_client -connect <host>:<port>

4. 돌아보면

  • 에러가 찍힌 위치 자체가 정보였습니다. 클라이언트 에러는 "브라우저가 막았다", 서버의 파싱 에러는 "서버까지 왔는데 언어가 다르다". 이 구분만 있었어도 원인 반경이 처음부터 절반이었어요.
  • "악성 트래픽인가?"는 편한 가설이라 위험했습니다. 외부 탓 가설은 우리 코드를 안 봐도 되게 해줘서 매력적인데, 16진수 값을 검색해 보는 5분이 가설 하나를 통째로 지웠습니다. 모르는 바이트는 일단 검색.
  • 한계 — 이 글의 서버 쪽 서술은 백엔드 담당 팀원의 작업을 옆에서 정리한 것이라, Spring 설정의 세부 시행착오까지는 담지 못했습니다. 또 직접 TLS를 택한 판단은 "2인 팀의 속도" 기준이었고, 인증서 갱신 자동화(certbot 등)를 붙이기 전까지는 갱신이 수동 운영으로 남았습니다.