터미널에 명령을 하나 붙여넣었다.

echo hello

그런데 화면에는 처음 보는 손님이 따라왔다.

^[[200~echo hello^[[201~

200201.

누가 내 명령 앞뒤에 번호표를 붙인 걸까?

처음 보면 꽤 수상하다. 웹 터미널이 명령을 변조한 것 같고, 중간 프록시가 이상한 문자를 끼워 넣은 것 같고, 보안 솔루션이 무언가를 덧붙인 것처럼 보인다.

하지만 범인은 대개 악성 문자열이 아니다.

붙여넣기한 내용을 안전하게 운반하려고 터미널이 붙인 포장 테이프다.

그 이름은 Bracketed Paste다.

터미널은 채팅창이 아니다

우리는 터미널을 큰 텍스트 상자처럼 생각하기 쉽다.

키보드로 글자를 입력하면 서버로 보내고, 서버가 결과 문자열을 돌려주면 화면에 그리는 곳.

하지만 실제 터미널은 훨씬 복잡하다.

키보드 또는 붙여넣기
→ 터미널 에뮬레이터
→ WebSocket 또는 SSH
→ 중계 서버
→ 원격 PTY
→ shell
→ 대화형 프로그램

각 구간은 글자만 전달하지 않는다.

  • 커서를 어디로 옮길지
  • 화면을 언제 지울지
  • 어떤 색으로 표시할지
  • 창 크기가 바뀌었는지
  • 지금 입력이 타이핑인지 붙여넣기인지
  • Ctrl+C와 방향키를 어떻게 해석할지

이런 상태와 신호도 함께 주고받는다.

터미널은 글자를 옮기는 창이 아니라 상태를 주고받는 프로토콜이다.

그래서 사람 눈에는 같은 글자로 보여도 실제 byte는 다를 수 있다. 반대로 실제 byte에는 무언가 더 들어 있지만 화면에서는 보이지 않을 수도 있다.

택배 상자 앞뒤에 붙는 포장 테이프

여러 줄짜리 명령을 붙여넣는다고 해보자.

echo "start"
printf "hello\n"
echo "done"

터미널이 이것을 아주 빠른 키보드 입력으로만 취급하면 각 줄이 들어오는 즉시 실행될 수 있다.

shell 입장에서는 곤란하다.

“사람이 한 글자씩 친 건가?”

“여러 줄을 한꺼번에 붙여넣은 건가?”

이 둘을 구분할 수 없기 때문이다.

Bracketed Paste는 붙여넣기 앞뒤에 경계 표시를 넣어 이 문제를 해결한다.

붙여넣기 시작: ESC[200~
붙여넣기 본문: echo hello
붙여넣기 종료: ESC[201~

택배에 비유하면 이렇다.

ESC[200~  = 여기서부터 상자 안 내용물입니다
본문      = 실제 배송할 물건
ESC[201~  = 여기까지가 상자 안 내용물입니다

shell이나 line editor는 이 표시를 보고 붙여넣기 전체를 하나의 편집 가능한 덩어리로 다룰 수 있다.

여러 줄이 실수로 바로 실행되는 것을 막거나, 자동 들여쓰기를 조절하거나, 사용자가 내용을 확인한 뒤 Enter를 누르게 할 수도 있다.

즉 이 이상한 번호표는 방해물이 아니라 안전장치다.

200~?2004h는 다른 신호다

여기서 비슷하게 생긴 친구가 하나 더 등장한다.

ESC[?2004h
ESC[?2004l

이 둘은 붙여넣기 본문의 시작과 끝이 아니다.

신호 의미
ESC[200~ 붙여넣기 본문 시작
ESC[201~ 붙여넣기 본문 종료
ESC[?2004h Bracketed Paste 모드 켜기
ESC[?2004l Bracketed Paste 모드 끄기

다시 택배 비유를 써보자.

?2004h = 지금부터 택배 포장 규칙을 사용하겠습니다
200~   = 새 상자를 엽니다
201~   = 상자를 닫습니다
?2004l = 이제 택배 포장 규칙을 사용하지 않겠습니다

비슷해 보이지만 역할은 전혀 다르다.

이 구분을 놓치면 “모드를 켜는 신호”와 “실제 붙여넣기 경계”를 같은 것으로 오해하게 된다.

그런데 포장 테이프가 왜 내용물 안으로 들어왔을까

정상이라면 사용자는 200~201~을 볼 일이 거의 없다.

터미널 에뮬레이터와 shell이 같은 규칙을 알고 있기 때문이다.

터미널: 붙여넣기 시작 표시를 보낼게
shell: 좋아, 표시를 떼고 본문만 편집할게

문제는 두 쪽의 상태가 어긋날 때 생긴다.

터미널: 지금 Bracketed Paste 모드가 켜져 있어
shell: 아니, 난 그런 모드가 켜진 줄 모르는데?
터미널: 그럼 상자 시작 표시부터 보낼게
shell: 이 문자도 명령의 일부인가 보다

그러면 제어 신호가 제어 신호로 소비되지 않고 평범한 글자처럼 남는다.

화면에는 이런 식으로 보일 수 있다.

^[[200~echo hello^[[201~

환경에 따라 Escape byte는 ^[로 보이기도 하고, 일부 문자만 남기도 하고, 로그에서 다른 표현으로 변환되기도 한다.

그래서 누군가 “^~200~가 보였다”고 말해도 그것을 곧바로 정확한 원문 byte로 단정하면 안 된다.

화면에 보인 표현과 실제 전송된 byte는 다른 증거다.

범인은 한 명이 아닐 수 있다

상태가 어긋나는 이유는 다양하다.

  • 원격 shell이나 line editor가 Bracketed Paste를 지원하지 않는다.
  • vim, less, top 같은 대화형 프로그램이 입력 모드를 바꿨다.
  • 연결이 끊겼다가 다시 붙으면서 화면 상태와 원격 상태가 달라졌다.
  • 저장된 터미널 화면을 복원했지만 실제 shell session은 새로 만들어졌다.
  • 중간 계층에서 control sequence 일부가 유실되거나 변형됐다.
  • 브라우저 터미널이 기억한 모드와 서버가 기억한 모드가 달라졌다.

여기서 중요한 교훈이 하나 나온다.

같은 화면 증상이라고 해서 같은 원인은 아니다.

200~가 보인다는 결과는 같아도, 문제는 브라우저·WebSocket·PTY·shell·대화형 프로그램 중 서로 다른 층에 있을 수 있다.

화면에 두 번 보였다고 두 번 전송된 것도 아니다

터미널 디버깅이 어려운 또 다른 이유는 echo다.

사용자가 l을 입력하면 다음과 같은 일이 일어날 수 있다.

사용자 입력 l
→ 브라우저 터미널
→ WebSocket 또는 SSH
→ 원격 PTY가 l 수신
→ 원격 PTY가 l을 echo
→ 다시 브라우저로 전달
→ 화면에 l 표시

화면에 보인 글자는 브라우저가 직접 그린 것일 수도 있고, 원격 서버가 되돌려준 echo일 수도 있다.

같은 명령이 두 번 보였을 때도 가능성은 여러 개다.

화면에서 보인 현상 가능한 실제 원인
명령이 두 번 보임 브라우저가 두 번 전송함
명령이 두 번 보임 한 번 전송했지만 local echo와 remote echo가 모두 표시됨
이전 명령이 다시 보임 새 입력 없이 저장된 화면 history를 다시 그림
이상한 문자가 섞임 control sequence가 일반 문자로 해석됨

화면 녹화는 사용자가 무엇을 봤는지 알려준다.

하지만 누가 그 byte를 만들었는지는 알려주지 않는다.

그래서 증거는 구간별로 모아야 한다

웹 터미널 입력 문제를 확인할 때는 하나의 로그에 모든 진실이 들어 있을 것이라고 기대하면 안 된다.

최소한 세 구간을 맞춰보는 것이 좋다.

1. 입력 영수증

브라우저가 실제로 무엇을 보냈는지 확인한다.

웹 터미널이라면 개발자 도구의 WebSocket request frame이 좋은 출발점이다.

확인할 내용은 단순하다.

붙여넣기 한 번에 request가 몇 번 발생했는가?
request 안에 ESC[200~와 ESC[201~가 있었는가?
화면에 없던 문자열이 request에도 있었는가?

2. 중계 영수증

중간 터미널 세션이나 SSH stream에서 무엇을 받았고 무엇을 돌려보냈는지 확인한다.

client request에는 한 번만 있었는가?
server response가 같은 문자열을 echo했는가?
재연결 시 과거 화면을 다시 보냈는가?

3. 실행 영수증

마지막으로 원격 프로그램이 실제로 무엇을 처리했는지 확인한다.

위험한 명령을 사용할 필요는 없다. 소유한 일회용 실험 환경에서 harmless marker를 사용하면 된다.

printf 'arrived\n' > /tmp/terminal-lab-marker

이 명령이 실제로 실행됐는지는 화면 글자보다 marker file의 존재로 확인하는 편이 정확하다.

실험이 끝나면 반드시 자신이 만든 marker만 제거한다.

rm -f /tmp/terminal-lab-marker

화면, 전송, 실행은 서로 다른 영수증이다.

셋을 같은 timestamp와 session으로 묶어야 사건의 경로가 보인다.

제어문자를 프록시에서 지워버리면 되지 않을까

가장 유혹적인 해결책이다.

ESC[200~와 ESC[201~가 문제네?
중간 서버에서 전부 삭제하자.

하지만 이 방법은 택배 포장 테이프가 보기 싫다고 물류센터에서 모든 상자의 테이프를 뜯어버리는 것과 비슷하다.

당장 이상한 문자는 사라질 수 있다.

대신 정상적인 붙여넣기 보호 기능도 사라질 수 있다.

특히 SSH terminal 중계기는 가능한 한 byte stream을 투명하게 전달해야 한다. 중간 계층이 terminal protocol을 마음대로 고치기 시작하면 shell, editor, full-screen application마다 새로운 예외가 생긴다.

수정 전에 먼저 물어야 한다.

누가 Bracketed Paste 모드를 켰는가?
누가 시작·종료 marker를 보냈는가?
어느 구간까지 marker가 control sequence였는가?
어느 구간부터 일반 문자로 바뀌었는가?
direct SSH에서는 같은 현상이 발생하는가?

원인이 mode 동기화라면 mode를 다시 맞춰야 한다.

원인이 reconnect 순서라면 화면 복원과 input listener 연결 순서를 고쳐야 한다.

원인이 특정 shell 호환성이라면 지원 조건을 명확히 하거나 그 경계에서만 제한적으로 처리해야 한다.

증상을 지우는 것과 원인을 고치는 것은 다르다.

nslookup이 멈춘 것처럼 보인 작은 오해

터미널 문제에는 정말로 고장처럼 보이는 정상 동작도 섞인다.

대표적인 예가 인자 없이 실행한 nslookup이다.

nslookup example.com

이 형태는 조회한 뒤 종료한다.

반면 다음처럼 실행하면:

nslookup

대화형 prompt를 열고 다음 입력을 기다릴 수 있다.

>

shell prompt가 돌아오지 않으니 사용자는 터미널이 멈췄다고 느낀다.

하지만 exit에 반응한다면 정상적인 interactive mode일 가능성이 높다.

이 사례가 알려주는 원칙은 간단하다.

command 이름만 보지 말고, 정확한 인자와 다음 prompt를 함께 보자.

정확히 어떤 command line을 실행했는가?
새 prompt가 나타났는가?
exit 또는 EOF에 반응하는가?
direct SSH에서도 같은가?

“화면이 이상하다”는 문장을 실행 상태로 번역하는 과정이 필요하다.

SSH 설정 문제는 또 다른 층이다

웹 터미널의 붙여넣기 문제와 로컬 SSH 설정 문제는 사용자에게 모두 “터미널 접속 문제”로 보인다.

하지만 시작점부터 다르다.

웹 터미널 문제
브라우저 → WebSocket → PTY → shell

SSH 설정 문제
OpenSSH config → ProxyCommand → SSH transport → target

.ssh/config의 최종 해석 결과를 확인할 때는 다음 명령이 유용하다.

ssh -G app01

이 명령은 실제 접속 대신 OpenSSH가 계산한 최종 설정을 보여준다.

주로 볼 값은 다음과 같다.

user
hostname
port
proxycommand

무엇을 확인하는가?

  • alias가 기대한 hostname으로 해석되는가?
  • UserPort가 올바르게 적용됐는가?
  • 기존 ProxyCommand가 이미 들어 있는가?
  • 다른 Host, Match, Include 규칙이 덮어썼는가?

ssh -G부터 잘못됐다면 로컬 OpenSSH config 경계다.

ssh -G는 맞지만 프록시 연결에서 실패한다면 그다음 transport와 인증 경계를 본다.

한꺼번에 “SSH 프록시가 config를 무시한다”고 부르지 말아야 하는 이유다.

터미널 사건을 푸는 다섯 질문

다음에 웹 터미널에서 이상한 문자열을 만나면 이 다섯 가지부터 확인해보자.

1. 화면에는 정확히 무엇이 보였나

스크린샷이나 녹화로 사용자의 경험을 남긴다.

2. client는 실제로 무엇을 보냈나

WebSocket request나 raw input byte를 확인한다.

3. server는 무엇을 되돌려줬나

remote echo와 화면 history 복원을 구분한다.

4. 원격 프로그램은 무엇을 실행했나

일회용 환경의 harmless footprint로 확인한다.

5. direct 연결과 중계 연결은 어떻게 다른가

같은 shell, 같은 command, 같은 설정으로 A/B 비교한다.

이 질문에 답하면 “터미널이 이상해요”는 꽤 다루기 쉬운 문제로 변한다.

마지막으로 기억할 것

^[[200~는 대개 괴상한 침입자가 아니다.

붙여넣기 내용을 안전하게 구분하려고 붙인 포장 표시다.

문제는 표시가 존재한다는 사실이 아니라, 표시를 알아야 할 두 프로그램의 상태가 어긋났다는 데 있다.

그리고 이 작은 사건은 터미널 전체를 이해하는 좋은 출발점이 된다.

  • 화면은 byte stream의 한 가지 표현일 뿐이다.
  • PTY와 shell은 입력을 echo할 수 있다.
  • control sequence는 글자가 아니라 상태를 바꾸는 신호다.
  • 같은 화면 증상도 서로 다른 계층에서 생길 수 있다.
  • 제어문자를 무조건 제거하기보다 의미가 깨진 경계를 찾아야 한다.
  • Web Terminal과 OpenSSH config 문제는 별도 실행 경로로 분석해야 한다.

터미널은 채팅창이 아니다.

잘 포장된 신호와 상태가 여러 층을 통과하는 작은 운송망이다.

다음에 200~가 나타나면 놀라기보다 먼저 물어보자.

이 포장 테이프는 어디에서 붙었고, 어디에서 떼어졌어야 할까?