콘텐츠로 이동

커스텀 프로토콜 디코딩

내장 포맷으로는 인식되지 않는 비공개 또는 사내 소켓 프로토콜을 캡처했는데, 남는 건 바이트 더미뿐이신가요? 커스텀 디코딩은 짧은 스크립트로 도구에게 그것을 읽는 법을 가르쳐 줍니다. 연속된 바이트 스트림을 개별 메시지로 잘라내고, 프로토콜 헤더를 벗겨내고, 필요하면 압축을 풀고, 나머지는 도구의 자동 구조 인식에 맡기세요.

이 페이지는 직접 디코더를 작성하려는 분을 위한 것입니다. 함수를 어떻게 작성하는지, 언제 호출되는지, 반환값이 무엇을 뜻하는지 설명합니다.


여러분은 디코딩 훅을 작성하고, 도구는 그 훅에 연결의 바이트를 넘기면서 하나의 질문을 반복합니다. “여기서부터 완전한 메시지 하나를 잘라낼 수 있나요?” 여러분이 메시지 하나를 잘라내고 사용한 바이트 수를 알려주면, 도구는 나머지를 가지고 다시 묻고, 스트림이 완전히 잘릴 때까지 이 과정을 반복합니다.

네트워킹 프레임워크에서 누적 바이트 디코더를 작성해 본 적이 있다면 사고방식은 동일합니다. 연속된 바이트 스트림이 주어지면, 메시지 경계를 여러분이 직접 정한다는 것입니다. 두 가지 핵심 규약이 있습니다.

  • 방향은 신경 쓸 필요 없습니다: 연결의 송신 스트림수신 스트림은 각각 별도로 여러분의 훅을 거치며, 도구가 잘라낸 각 메시지에 방향을 자동으로 표시합니다. 스크립트는 “어떻게 자를지”만 처리하면 됩니다.
  • 잘라낸 뒤의 렌더링은 처리하지 않습니다: 여러분이 밀어낸 모든 메시지는 다시 자동 인식을 거칩니다. 그 안에 protobuf, JSON, plist가 들어 있으면 한 단계 더 구조화되어 파싱됩니다. 여러분은 프레이밍, 헤더 벗기기, 압축 해제만 책임지면 됩니다.

디코더 전체는 함수 하나입니다.

function decode(buf, out) {
// buf: 현재 파싱 대기 중인 바이트 스트림; out: 출력 수집기; return: 이번에 소비한 바이트 수
}

buf (입력): 파싱 대기 중인 바이트 스트림

  • 타입: ArrayBuffer. 그 내용은 “마지막으로 소비를 끝낸 지점부터 현재 끝까지” 남아 있는 모든 바이트입니다.
  • 속성 buf.byteLength: 지금 남아 있는 바이트 수 (호출할 때마다 늘어날 수 있으며, 메시지를 잘라낼 만큼 충분한지 판단하는 기준입니다).
  • ⚠️ 이것은 Uint8Array가 아니라 원시 바이트 버퍼이므로, 바이트를 직접 인덱싱할 수 없습니다 (buf[0]undefined를 반환). 바이트를 읽으려면 3절의 내장 헬퍼(u8 / u16be / sub …)를 쓰거나, 직접 뷰를 감싸세요: new Uint8Array(buf), new DataView(buf).

out (출력): 출력 수집기

  • 타입: 일반 배열.
  • 메서드 out.push(메시지 바이트): 메시지 하나를 밀어냅니다. 한 번의 호출에서 여러 개를 밀어낼 수 있습니다.
  • “메시지 바이트”는 다음이 될 수 있습니다: 내장 헬퍼(sub(...), gunzip(...) 등)가 반환한 ArrayBuffer, 또는 문자열. 그 외 타입은 무시됩니다.

반환값: 소비한 바이트 수

  • 타입: 정수(number), buf시작 지점부터 사용한 바이트 수.
  • > 0: 앞에서 이만큼 바이트를 먹어 치우고 메시지 하나를 잘라냈다는 뜻 → 도구는 그것을 버리고 나머지를 가지고 다시 호출합니다.
  • 0 / return 없음 / 음수: “앞에 아직 완전한 메시지가 없다” 또는 “더 이상 잘라낼 것이 없다”는 뜻 → 도구는 루프를 멈추고 남은 바이트를 원시 데이터로 표시합니다.
  • 반환값은 (오버런 방지를 위해) 버퍼 길이 안으로 자동 클램핑되지만, 실제로 사용한 진짜 바이트 수를 반환하세요.
  • 도구는 연결의 송신 스트림에 한 번, 수신 스트림에 한 번 패스를 실행합니다. 각 패스에서 decode루프로 호출됩니다. 매번 남은 미소비 바이트를 넘겨주면, 여러분은 메시지 하나를 잘라내고 소비한 수를 반환하며, 0을 반환할 때까지 이 과정이 반복됩니다.
  • 메시지는 소비가 확정되어야만 나타납니다: 여러분이 밀어낸 메시지는 > 0을 함께 반환할 때만 표시됩니다. 밀어냈는데 0을 반환하면, 루프가 멈추고 그 push는 버려집니다. 따라서 메시지를 밀어내는 것은 반드시 그것이 몇 바이트를 차지했는지 반환하는 것과 짝을 이뤄야 합니다.
  • 루프가 끝난 뒤, 소비되지 않은 후행 바이트는 사라지지 않습니다. 하나의 원시 데이터 조각으로 표시됩니다 (예: 미완성 메시지의 뒷부분).
  • 각 스트림(방향)은 별도의, 완전히 새로운 스크립트 환경을 갖습니다. 송신 스트림과 수신 스트림은 서로 섞이지 않습니다.
  • 호출 간에 상태(카운터, 이전 메시지의 타입 등)를 유지하려면 변수를 decode 바깥에 선언하세요. 그것들은 같은 스트림 안에서는 호출 간에 유지되고, 방향이 바뀌거나 재디코딩할 때 초기화됩니다.
  • 디코딩은 이미 캡처된 데이터 위에서 실행되며, 반복 재실행이 가능합니다. 스크립트를 수정하고 저장한 뒤 다시 클릭하면, 같은 연결을 새 규칙으로 재디코딩합니다.

오류는 아무것도 망가뜨리지 않습니다

섹션 제목: “오류는 아무것도 망가뜨리지 않습니다”
  • 스크립트가 예외를 던지거나 한 번의 실행이 타임아웃되면, “이번에는 아무것도 소비하지 않음”으로 처리되어 남은 바이트가 원시 데이터로 표시되고, 캡처는 데이터 손실 없이 계속 실행됩니다.
  • 다시 말해, 망가진 스크립트가 할 수 있는 최악은 디코딩에 실패해 원시 바이트를 남기는 것뿐입니다. 연결을 죽이지 않으니 마음껏 수정하세요.
  • 그리고 오류는 삼켜지지 않습니다: 예외와 타임아웃은 결과 위쪽의 디버그 출력 패널(5절 참고)에 표시되며, 송신 인지 수신 스트림인지 라벨이 붙습니다. 그것을 따라가면 곧장 해결로 이어집니다.

3. 내장 헬퍼 (스크립트에서 바로 사용 가능)

섹션 제목: “3. 내장 헬퍼 (스크립트에서 바로 사용 가능)”

바이트 읽기, 정수 읽기, 텍스트 변환, 압축 해제 같은 흔한 작업은 모두 내장되어 있어, 직접 다시 만들 필요가 없습니다.

분류 시그니처 설명
서브슬라이스 추출 sub(buf, off[, len]) off부터 슬라이스(len을 생략하면 끝까지), ArrayBuffer 반환
정수 읽기 u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) 빅엔디안 / 리틀엔디안으로 부호 없는 정수 읽기
텍스트 변환 hex(buf) / ascii(buf) 16진수 문자열로 변환 / 텍스트로 읽기
압축 감지 gzipMagic(buf) gzip인지 여부 (불리언 반환)
압축 해제 gunzip / inflate / unzstd / lz4dtx (buf) 압축 해제; 불가능하면 오류 없이 입력을 그대로 반환
디버그 log(...args) / console.log(...args) 디버그 패널에 출력 (5절 참고)

표준 바이트 읽기/쓰기 기능(DataView, Uint8Array 등)도 바로 사용할 수 있습니다.


① 길이 접두사 [4바이트 빅엔디안 길이][페이로드], 가장 흔한 비공개 프로토콜 형태:

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // 길이 헤더가 아직 다 도착하지 않음, 대기
const total = 4 + u32be(buf, 0) // 전체 메시지 = 4바이트 헤더 + 페이로드
if (buf.byteLength < total) return 0 // 전체 메시지가 아직 다 도착하지 않음, 대기
out.push(sub(buf, 4, total - 4)) // 헤더를 벗기고 페이로드를 자동 인식에 넘김
return total // 이 메시지를 소비, 다음 것을 자르러 계속
}

② 구분자 / 줄 단위로, 개행 같은 표시로 프레임을 자르기:

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // 아직 개행이 없음, 대기
out.push(sub(buf, 0, i)) // 이 줄을 밀어냄 (개행 제외)
return i + 1 // 개행까지 함께 소비
}

③ 전체를 한 번에 처리, 스트림 전체를 하나의 메시지로 취급, 예를 들어 전체를 압축 해제:

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // gzip이면 풀기
return buf.byteLength // 전부 소비, 루프가 바로 끝남
}

④ 타입에 따라 분기, 헤더에 타입 필드가 있어 타입마다 다르게 처리:

function decode(buf, out) {
if (buf.byteLength < 4) return 0
const total = 4 + u32be(buf, 0)
if (buf.byteLength < total) return 0
const type = u8(buf, 4) // 첫 바이트가 메시지 타입
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // 타입 2는 압축됨
return total
}

에디터에는 이 모든 패턴에 대한 바로 쓸 수 있는 템플릿이 있습니다. 하나 삽입하고 숫자 몇 개만 손보면 바로 동작합니다.

커스텀 디코더 에디터: decode(buf, out) 스크립트를 작성하고, “템플릿 삽입”으로 길이 접두사 / 매직 시그니처 / 구분자 / 고정 길이 / gzip / 타입별 분기용 골격을 바로 얻으며, 함수 빠른 참조도 제공


  • 작성하고 저장하기: 디코더 에디터에서 만들고, 이름을 붙이고, 저장합니다. 템플릿을 삽입해 시작하고, 내장 함수 빠른 참조를 바로 옆에 두세요.
  • 적용하기: 이해가 안 되는 연결에 대해 마우스 오른쪽 버튼을 눌러 “다음으로 디코드” → 여러분의 디코더를 선택하면, 그 연결의 송수신 트래픽 전체가 즉시 여러분의 규칙대로 잘려서 표시됩니다.
  • 디버그와 변수 확인: decode 안에서 log(...) 또는 console.log(...)로 어떤 값이든 출력하세요: 바이트 수, 타입 필드, hex(sub(buf, 0, 8)) 등. 출력은 “다음으로 디코드” 결과 위쪽의 “디버그 출력” 패널에 나타나며, 각 줄은 송신 인지 수신 스트림에서 왔는지 라벨이 붙습니다. 스크립트 오류와 타임아웃도 같은 패널에 방향 라벨과 함께 표시됩니다. 이렇게 출력하고 위치를 짚는 것이 무작정 수정하는 것보다 훨씬 빠릅니다.

    샌드박스에는 완전한 브라우저 / Node의 console이 없습니다. console.log와 그에 상응하는 log만 이 패널에 연결되어 있습니다. TextDecoder, fetch, setTimeout 등은 사용할 수 없습니다. 바이트를 텍스트로 읽으려면 ascii(buf)를 쓰세요.

  • 수정하고 바로 확인하기: 재캡처가 필요 없습니다. 스크립트를 저장하고 “다음으로 디코드”를 다시 클릭하면, 같은 연결이 새 규칙으로 그 자리에서 재디코딩됩니다. 원하는 대로 잘릴 때까지 반복하세요.
  • 구조로 넘어가기: 디코드된 각 조각은 자동 인식으로 되돌려지므로, protobuf, JSON, plist가 한 단계 더 구조화되어 파싱되며, 데이터 검사 및 디코딩의 여러 뷰를 통해 볼 수 있습니다.
  • 디코더는 이름을 붙여 목록으로 보관할 수 있으며, 언제든 추가, 수정, 삭제할 수 있습니다.

  • 내장 포맷으로 인식되지 않는 사내 또는 비공개 소켓 프로토콜(흔한 형태는 길이 접두사 + protobuf / JSON / 바이너리)을 캡처했을 때, 디코더를 작성해 잘라내고 읽을 수 있는 구조로 복원하세요.
  • 커스텀 헤더로 감싸졌거나 한 차례 압축을 거친 바이트 덩어리를 읽을 수 있는 내용으로 되돌리고 싶을 때.

디코드 진입점, 내장 헬퍼 함수, 런타임 환경을 한데 모은 빠른 참조입니다. buf를 받는 모든 함수는 ArrayBuffer 또는 문자열을 받습니다.

decode(buf, out) → number
용도 디코더의 유일한 진입점, 반드시 구현해야 함. buf의 앞에서 메시지를 잘라 outpush하고, 이번에 소비한 바이트 수를 반환합니다.
buf ArrayBuffer, 파싱 대기 중인 남은 바이트 스트림. 길이는 buf.byteLength로 확인. 바이트를 인덱싱할 수 없으니 아래 헬퍼나 new DataView(buf) / new Uint8Array(buf)를 쓰세요.
out Array, 출력 수집기. out.push(bytes)로 메시지 하나를 밀어냅니다(bytesArrayBuffer 또는 문자열; 그 외 타입은 무시). 한 번에 여러 개를 밀어낼 수 있습니다.
반환 number, buf의 앞에서 소비한 바이트 수. > 0이면 계속; 0 / 음수 / 반환 없음 → 멈추고, 남은 바이트는 원시 데이터로 표시.
호출 규약 연결의 송신 스트림과 수신 스트림에 각각 한 번씩 패스 실행; 각 패스는 매번 남은 미소비 바이트를 넘기며 반복 호출하고, 0을 반환할 때까지 계속.

서브슬라이스 추출

시그니처 반환 설명
sub(buf, off) ArrayBuffer off부터 끝까지 슬라이스
sub(buf, off, len) ArrayBuffer off부터 len 바이트 슬라이스; 범위를 벗어난 off / len은 오류 없이 자동 클램핑

정수 읽기 (부호 없음; off가 범위를 벗어나면 0 반환)

시그니처 반환 설명
u8(buf, off) number 1바이트 읽기
u16be(buf, off) / u16le(buf, off) number 2바이트 읽기, 빅엔디안 / 리틀엔디안
u32be(buf, off) / u32le(buf, off) number 4바이트 읽기, 빅엔디안 / 리틀엔디안

텍스트 변환

시그니처 반환 설명
hex(buf) string 소문자 16진수로 변환
ascii(buf) string 텍스트로 읽어냄 (ASCII / 텍스트 내용에 적합)

압축 / 압축 해제 (압축 해제할 수 없으면 입력을 그대로 반환; 단일 압축 해제 한도 약 16 MB)

시그니처 반환 설명
gzipMagic(buf) boolean gzip 매직 넘버로 시작하는지 여부
gunzip(buf) ArrayBuffer gzip 압축 해제
inflate(buf) ArrayBuffer zlib / deflate 압축 해제
unzstd(buf) ArrayBuffer zstd 압축 해제
lz4dtx(buf) ArrayBuffer 블록 LZ4 스트림 압축 해제

디버그

시그니처 반환 설명
log(...args) (없음) 인자들을 한 줄로 합쳐 “디버그 출력” 패널에 출력; ArrayBuffer는 16진수로 표시
console.log(...args) (없음) log와 동일 (console.log에 익숙한 분에게 편리)
  • 표준 JavaScript: ArrayBuffer, 타입 배열(Uint8Array 등), DataView, JSON, Math, RegExp, Date 같은 흔한 내장 객체를 모두 사용할 수 있으며, 더 낮은 수준의 바이트 읽기/쓰기가 필요하면 new DataView(buf) / new Uint8Array(buf)를 바로 쓸 수 있습니다.
  • 디버그: log(...) / console.log(...)는 “디버그 출력” 패널에 출력됩니다. 스크립트 오류와 타임아웃도 거기에 출력되며, 송신 인지 수신 스트림에서 왔는지 라벨이 붙습니다.
  • 제공되지 않는 것은 그 외 브라우저 / Node 전용 기능들입니다: log 이외의 console 메서드, TextDecoder / TextEncoder, fetch, setTimeout, require 등은 모두 사용할 수 없습니다. 바이트를 텍스트로 읽으려면 ascii(buf)를 쓰세요.
  • decode 호출에는 실행 시간 한도가 있습니다: 무한 루프나 타임아웃은 중단되어 “아무것도 소비하지 않음”으로 처리되므로 도구가 멈추지 않습니다.

프록시 캡처로 돌아가기 · 관련: 데이터 검사 및 디코딩 · 로컬 NIC 캡처