GoSuda

Chrome처럼 말하도록 Go 프로그램 가르치기

By Lemon Mint views ...

여러분이 해외여행 중에 교과서에 나올 법한 완벽한 문장으로 커피를 주문해 본 적이 있다면, 바리스타가 보내는 그 특유의 표정을 알고 계실 것입니다. 문법은 틀리지 않았습니다. 사용한 단어도 모두 사전에 실려 있습니다. 그럼에도 불구하고, 채 반 초도 지나지 않아 카페에 있는 모든 사람이 여러분이 외지에서 왔다는 것을 알아차립니다.

Go의 표준 HTTP 클라이언트에도 이와 비슷한 문제가 조금 있습니다. 이 클라이언트는 신뢰할 수 있고, 깔끔하며, 설치하자마자 본격적인 작업에 바로 쓸 수 있다는 점에서 훌륭한 기본값입니다. 하지만 프로그램이 서버에 요청을 보낼 때, 그 요청은 누가 봐도 Go 프로그램의 것임이 분명하게 드러납니다. 대부분의 경우 이것이 문제가 되지는 않습니다. 그럼에도 일부 웹사이트, CDN, 그리고 테스트 환경은 문을 두드리는 주체가 누구인지 세심하게 살피며, 자신들이 상대하고 있다고 생각하는 클라이언트의 종류에 따라 매우 다르게 반응합니다.

그래서 저희는 gosuda.org/chromenet을 만들었습니다. Go의 표준 클라이언트가 그럴듯한 브라우저 흉내를 내도록 훈련시키는 대신, chromenet은 Go 프로그램이 Google Chrome이 사용하는 것과 동일한 네트워크 엔진, 즉 Chromium 자체의 네트워크 코드를 통해 웹과 통신할 수 있도록 합니다. 자신의 테스트 환경이 브라우저 트래픽을 어떻게 처리하는지 확인하든, 까다로운 CDN 뒤에 있는 사이트와 통신하든, Chrome의 엔진을 통해 통신하면 추측에 의존해야 하는 한 겹의 불확실성이 사라집니다.

이를 사용하기 위해 애플리케이션을 다시 작성할 필요는 없습니다. 코드에서 chromenet은 http.Transport를 대체하여 바로 사용할 수 있는 드롭인(drop-in) 방식으로 동작합니다. 기존과 똑같이 http.Client를 계속 사용하시면, 그 아래에서 통신은 chromenet이 처리합니다.

최신 릴리스를 사용해 보려면 go get으로 설치하십시오:

1go get gosuda.org/chromenet@v0.2.10157081033800

그다음 해당 트랜스포트를 클라이언트에 전달하십시오:

1client := http.Client{
2	Transport: chromenet.NewTransport(chromenet.Options{}),
3}

이번 릴리스에서는 현대 웹 트래픽의 전체 범위가 Chrome의 엔진을 통해 전달됩니다. 이제 일반 웹 요청, WebSocket, WebTransport를 모두 포괄합니다. 이전 버전은 일반 요청만 제대로 처리했고, WebSocket과 WebTransport는 임시 대체 구현에 불과했습니다. 그 결과 현관문에서 건네는 첫인사는 Chrome처럼 들렸지만, 다른 방식의 대화로 전환하면 위장이 드러날 수 있었습니다. 이번 릴리스는 이러한 임시 대체 구현을 실제 구현으로 교체합니다.

잠깐, Chromium 자체의 네트워크 코드를 사용한다면, 조용하던 Go 빌드가 복잡한 C 컴파일러 문제로 번지는 것 아닐까요? 다행히 그렇지 않습니다. Go 코드는 여전히 순수한 Go로 유지되며, cgo도 C 컴파일러도 필요하지 않습니다.

chromenet은 브라우저 코드를 바이너리에 직접 용접해 넣는 대신, 엔진을 Go 프로그램 옆에서 실행되는 작은 별도 프로그램으로 구동합니다. 자신의 성대를 다시 만들려고 하기보다, 곁에서 함께 걸어 다니며 통역해 주는 유창한 현지 통역사를 고용하는 것으로 생각하시면 됩니다. 엔진이 이 별도 프로그램에서 실행되기 때문에, 그 내부에서 충돌이 발생하더라도 서비스가 중단되지는 않습니다.

chromenet의 작동 방식: Go 프로그램이 작은 동반 프로그램에 요청하면, 이 프로그램은 Chromium 네트워크 엔진을 실행하고 웹과 통신합니다

Go 프로그램은 순수한 Go로 남아 있고, 작은 동반 프로그램이 Chromium 네트워크 엔진을 통해 트래픽을 전달합니다.

작업용 컴퓨터나 서버에 추가로 설치할 것도 없습니다. Linux, macOS, Windows용 엔진 바이너리는 Intel/AMD와 ARM 모두에 대해 Go 모듈 안에 함께 포함되어 있으며, 실행되기 전에 검증됩니다.

모듈 안에 바이너리를 포함하는 방식은 신뢰성에 관심이 있는 분들에게 당연히 의문을 불러일으킬 수 있으므로, 명확한 답변을 드리고자 했습니다. 이 바이너리는 특정 Chromium 릴리스, 현재는 Chromium 157로부터 재현 가능한 방식으로 빌드되므로, 누구든지 이를 다시 빌드하여 정확히 동일한 바이트를 얻을 수 있으며, 각 릴리스는 GitHub에 의해 증명(attested)됩니다. 수작업 없이 브라우저 세계의 변화를 따라가기 위해, 예약된 작업이 Chromium의 안정(stable) 채널을 추적하고 새로운 Chrome이 출시될 때 버전을 자동으로 업데이트합니다.

uTLS는 어떤가요?

이전에 Go 클라이언트가 브라우저처럼 보이도록 만들어 보신 적이 있다면, uTLS를 접해 보셨을 가능성이 큽니다. 이는 훌륭한 작업물입니다. 보안 연결의 가장 첫 메시지를 Chrome, Firefox 또는 원하는 어떤 브라우저와 유사하도록 꾸밀 수 있게 해 주는 Go 라이브러리입니다. 규모가 작고, 순수한 Go로 작성되었으며, 프로그램 곁에서 추가로 실행되는 것도 없습니다.

다만 그 변장이 얼마나 멀리까지 미치는지가 문제입니다. uTLS는 대화의 처음 몇 마디에 해당하는 핸드셰이크까지만 다룹니다. 그 이후의 모든 부분은 여전히 직접 구축해야 합니다. HTTP/2 계층은 손수 연결하고 조정해야 하며, HTTP/3은 별도의 모험이고, WebSocket이나 WebTransport는 훨씬 더 많은 접착 코드를 요구합니다. 또한 Chrome 프리셋은 복사본이기 때문에, Chrome이 동작 방식을 바꿀 때마다 누군가 이를 최신 상태로 유지해야 합니다. 이는 사람의 억양은 흉내 내면서도 나머지 신체 언어는 여전히 정체를 드러내는 것과 비슷합니다.

chromenet은 반대의 방식을 택합니다. 브라우저를 흉내 내지 않고, Chrome 자체의 엔진에 대화를 맡깁니다. 따라서 핸드셰이크, 연결 재사용 방식, 프로토콜 업그레이드, 전송 계층이 모두 한 곳에서 나오며, Chromium이 변화할 때 함께 움직입니다.

이에 따른 트레이드오프는 솔직하게 밝히겠습니다. uTLS는 더 가볍고 상상할 수 있는 어떤 인사말이든 직접 만들 수 있게 해 줍니다. 반면 chromenet은 25 MB 규모의 헬퍼를 가져오며, 맞춤형 동작이 아닌 Chrome의 동작을 제공합니다. 작은 의존성과 핸드셰이크에 대한 완전한 제어를 원한다면, uTLS는 여전히 훌륭한 선택입니다.

모든 설계에는 트레이드오프가 존재하며, 여기서 그 한계를 미리 알려 드리고자 합니다. 실제 브라우저 네트워크 엔진을 포함하는 것은 결코 작지 않습니다. 각 엔진 바이너리는 약 25 MB입니다. macOS에서는 바이너리가 공증(notarized)되어 있지 않습니다. 그리고 Linux에서는 시스템에 glibc 2.31 이상이 필요합니다. 이러한 제약이 여러분의 환경에 맞는다면, 트랜스포트 필드 하나만 교체하여 실제 Chrome 네트워킹을 얻을 수 있습니다.

프로젝트는 github.com/gosuda/chromenet에서 확인하실 수 있습니다. gosuda 오픈 소스 패밀리의 자매 프로젝트인 gosuda.org/hqq와 gosuda.org/sqlite3를 알고 계시다면, 그 정신이 익숙하게 느껴지실 것입니다. 더 넓은 소프트웨어 세계에서 성능이 뛰어나지만 무거운 기계를 가져와, 순수한 Go 안에서 자연스럽게 느껴지도록 만드는 것입니다.

따라서 Go 프로그램이 브라우저처럼 들리도록 해야 해서 uTLS를 찾고 계신다면, chromenet을 한번 사용해 보십시오. 특히 HTTPS 요청 이상이 필요할 때 uTLS의 좋은 대안이 될 수 있습니다. HTTP/2, HTTP/3, WebSocket, WebTransport가 모두 동일한 Chrome 엔진을 통해 전달되며, 사용자 쪽에서 접착 코드가 필요하지 않습니다.