더 많은 개발자가 애플리케이션에 Tailscale을 통합할 수 있도록, Tailscale의 Rust 라이브러리 프리뷰인 tailscale-rs를 소개합니다.
더 많은 개발자가 자신의 애플리케이션에 Tailscale을 가져올 수 있도록, 저희는 Tailscale의 Rust 라이브러리 버전을 만들고 있습니다. Go에서 tsnet을 써보셨다면 거의 그것과 같지만, Go가 아닌 언어를 위한 버전입니다. Python, Elixir, C용 초기 바인딩을 제공하고 있으며, 물론 Rust에서도 이 라이브러리를 네이티브하게 사용할 수 있습니다.
저희는 코드를 실험적 프리뷰로 GitHub에 공개했습니다(아직 프로덕션에서는 사용하지 마세요! 경고를 읽어주세요!). 꼭 한번 써보시고, 피드백도 주시고, 무엇을 보고 싶은지도 알려주세요.
급하신 분들을 위한 핵심 발표는 여기까지입니다. 이 글의 나머지 부분에서는 왜 저희가 이것을 만들고 있는지, 그리고 앞으로 어디로 나아갈지에 대해 조금 더 이야기해보겠습니다.
아주 유용하기 때문입니다!
Tailscale의 목표는 새로운 인터넷을 만드는 것입니다. 프로그램들이 예전의 LAN처럼 쉽고 안전하게 서로 통신할 수 있으면서도, 모든 것을 실제로 한곳의 작은 물리적 LAN 위에 두도록 강제하지 않는 인터넷 말입니다. 저희는 Tailscale을 클라이언트 애플리케이션으로 만들었고, 이는 그 아이디어를 직관적인 방식으로 구현합니다. 시스템에 가상 네트워크 인터페이스를 붙이고, 컴퓨터의 나머지 부분이 그것을 일반적인 LAN처럼 다루게 하는 것입니다.
이 방식은 많은 경우에 잘 작동합니다(그리고 보기에도 기업용 VPN 솔루션처럼 안심이 되는데, 듣자 하니 영업에 도움이 된다고 합니다). 하지만 때로는 이런 네트워킹 구성을 OS 계층에서 처리하는 것이 불편하거나, 어떤 환경에서는 아예 불가능하기도 합니다. 예를 들어 필요한 구성 요소가 빠진 축소형 커널을 사용하는 시스템, OS 네트워크 스택 변경을 허용하지 않는 컨테이너 환경 등이 그렇습니다.
저희도 Tailscale을 만들고 사용하면서 이런 불편을 직접 겪었기 때문에, 자체 프로그램에 직접 번들할 수 있는 “라이브러리로서의 Tailscale”인 tsnet을 만들었습니다. 이렇게 하면 프로그램이 자기 일을 하는 동안, 우연히 tailnet에 접근할 수 있는 독립 실행형 프로그램이 됩니다.
tsnet은 훌륭하고, 저희는 이를 사용해 엄청나게 많은 소프트웨어를 작성했습니다. 프로덕션 시크릿 관리를 위한 setec, OpenID Connect를 통해 Tailscale ID를 노출하는 tsidp, tclip pastebin, golink 링크 단축기, 그리고 공개하지 않은 더 많은 내부 도구들이 있습니다. 커뮤니티도 이것의 유용함을 똑같이 발견했고, tsnet을 사용해 자신들만의 도구와 서비스를 만들었습니다(예: tnsrv 리버스 프록시, chat-tails 메시징 서비스).
믿기 어렵겠지만, Go 이외의 프로그래밍 언어들도 계속 존재하고 여전히 유용하기 때문입니다.
Go 프로그래머라면 tsnet이 있으니 삶이 좋습니다. 하지만 다른 언어를 사용한다면, 음… 이를 위한 무언가를 만들려고 시도하긴 했고, 그 결과물이 libtailscale, 즉 C 라이브러리 형태의 tailscale입니다. 그런데 이 방식은 libtailscale을 처음 호출하는 순간, 프로세스 내부에서 완전한 Go 런타임 전체를 띄운 다음, 앞단에 약간의 C 글루를 붙여 tsnet을 다시 실행하는 구조입니다. 문제는 Go 런타임이 제대로 동작하려면 프로세스 수명 주기의 여러 부분을 장악해야 한다는 점이고, 이미 다른 런타임이 그렇게 하려 하고 있으면 일이 잘 풀리지 않는다는 것입니다. 예를 들어 libtailscale과 Ruby VM을 섞는 것은 빠르게 크래시로 가는 지름길입니다. 두 런타임이 서로의 영역을 침범하다가 결국 스스로를 폭발시켜 버리기 때문입니다.
개인적으로 저는 Go로 코드를 쓰는 것을 좋아하지만, Django 웹 앱(Python)이나 Godot 게임(GDScript 또는 C#) 안에서 tsnet 같은 것이 정말로 필요했던 상황을 겪은 적이 있습니다. 그런 상황에서는 잠깐 libtailscale을 고려했다가, 아마 눈물로 끝날 것이라는 사실을 떠올리곤 했습니다. 그리고 tsnet을 쓰겠다고 Godot 전체를 Go로 포팅하는 건 다소 과한 반응일 뿐 아니라, 해야 할 일도 엄청 많습니다.
분명 일부 사용자들도 같은 생각을 하고 있습니다. 수년 동안 저희는 결국 이런 문제 제기로 귀결되는 요청을 여러 번 받아왔습니다. 제발 Tailscale을 우리 소프트웨어에 번들할 수 있게 해달라, 그러면 사용자들에게 서드파티 앱 설치와 설정 과정을 일일이 안내하지 않고도 좋은 기능을 제공할 수 있다는 것이죠. 저는 “그러고 싶지만, 할 수 없습니다”라고 말해야 하는 상황을 좋아하지 않습니다. 몇몇 분들은 여전히 어떻게든 해내고 있습니다(예를 들어 LM Studio의 최근 LM Link). 하지만 저희가 그들의 개발 경험을 분명 더 좋게 만들 수 있습니다.
작년에 있었던 그런 대화 중 하나가, 기본 답변을 바꾸려면 무엇이 필요한지 더 진지하게 탐색해보는 계기가 되었습니다. 그렇게 하나가 다른 하나로 이어졌고, 그 결과가 tailscale-rs입니다.
저희의 모든 요구사항을 충족한 최선의 선택이기 때문입니다.
저희의 출발점은 “libtailscale이지만, 다른 사람의 프로그램 안에서 손님처럼 잘 동작하는 것”이었습니다. 즉, 프로세스가 어떻게 실행되어야 하는지에 대해 강한 의견을 가진 언어 런타임은 사용할 수 없다는 뜻이고, 이 기준으로 Go는 제외됩니다(꼼꼼히 따지자면 Python, Ruby, C#, Java, Haskell, …도 마찬가지입니다).
과거에는 명백한 답이 C였습니다. C는 컴퓨팅의 공용어입니다. 모든 범용 프로그래밍 언어는 C 코드와 대화하는 방법을 알고 있고, “고수준 어셈블리”라는 평판이 약간 부정확하긴 해도, 코드가 말하는 것 이상으로 숨겨진 일이 많이 벌어지지는 않습니다. 비슷한 맥락에서, 잘 선별된 C++의 일부 하위 집합에 대해서는 조금 약한 주장을 할 수도 있겠습니다.
하지만 오늘날에는 기본적인 메모리 안전성 특성이 없는 언어로 새 코드를 쓰기 시작하려면 압도적으로 좋은 이유가 있어야 한다고 생각합니다. 언어가 배열 경계 밖 접근이나 임의의 무검사 포인터 산술 같은 일을, 먼저 명시적으로 “지금 위험합니다” 버튼을 누르지 않아도 허용한다면, 정말로 다른 선택지가 전혀 없을 때만 써야 합니다.
그래서 저희가 원하는 이상적인 언어는, 필수적이고 강한 성향의 런타임을 동반하지 않으며, C만큼 보편적으로 인터페이스할 수 있고, 메모리 안전성에 대한 좋은 이야기를 갖고 있는 언어입니다. 빠르기까지 하면 더 좋습니다. 가능하다면 초당 수십 기가비트 속도로 패킷을 밀어 넣고 싶기 때문입니다. 비즈니스 관점에서는, 이 언어가 10년 이상 건강하게 유지될 것이라는 합리적인 확신도 필요합니다. 또한 의존할 수 있는 견고한 코드와 도구 생태계, 그리고 채용할 수 있는 전문가들도 원합니다.
저희의 결론은 Rust가 이러한 요구사항에 가장 잘 들어맞는다는 것이었습니다.
아니요. 음, 맞기도 하고 아니기도 합니다.
tailscale-rs는 Tailscale 핵심부의 Rust 재구현입니다. 그런 의미에서는 Rust로 다시 쓰고 있는 것이 맞습니다. 하지만 다른 한편으로 이 Rust 구현이 기존 사용 사례(데스크톱 앱, tsnet 등)에서 Go를 대체하는 것은 아닙니다.
Rust를 구현 언어로 선택한 것과 마찬가지로, 여러 구현을 가져가는 것도 이념적 선택이 아니라 현실적인 절충이 있는 실용적 선택입니다. 저희가 다른 언어에도 라이브러리 형태의 Tailscale을 제공하려면, Go만으로는 안 됩니다. 결국 질문은 Tailscale 구현을 두 개 유지할 것인지, 아니면 하나만 유지할 것인지(그 경우 Rust여야 합니다)입니다.
물론 이렇게 놓고 보면 제 즉각적인 답은 이렇습니다. 제발 구현은 하나만 있으면 좋겠습니다. 두 구현을 유지하고 서로 매끄럽게 상호운용되도록 맞추는 일은 엄청난 작업이고, 새로운 기능도 모두 두 번 구현해야 하니까요.
하지만 그런 구도는 마치 마법 지팡이를 휘둘러 하룻밤 사이에 기능과 성능의 동등성을 달성하고(제발 AI라고 말하진 마세요, 이건 진지한 이야기입니다), 아무것도 깨뜨리지 않고 구현을 교체할 수 있다는 전제를 깔고 있습니다. 그건 현실적이지 않고, 어떤 경우에는 명백히 불가능합니다. 코드에 tsnet을 추가하려면 go get tailscale.com/tsnet을 실행합니다. go 도구는 Go/Rust 혼합 패키지를 빌드하는 방법을 모르기 때문에, 무슨 수를 쓰더라도 이것은 일부 사용자에게는 호환성이 깨지는 변경이 됩니다.
또한 저희의 Go 구현에는 6년간의 기능 개발, 디버깅, 최적화가 축적되어 있고, tailscale-rs가 여기에 따라잡으려면 시간이 걸릴 것입니다. 따라잡는 동안 기존 사용자층에 대한 기능과 개선 제공을 멈출 수도 없습니다. 그리고 이것은 저희가 두 구현을 동시에 적극적으로 개발해야 한다는 뜻입니다.
저희는 Go 구현의 일부 조각을 시간이 지나며 Rust 컴포넌트로 바꿔 넣는, 제자리 점진적 재작성도 고려해봤습니다. 결론은 이것이 모든 선택지 중 최악이라는 것이었습니다. 저희는 여전히 대부분 Go를 사용하는 조직이기 때문에, Go/Rust 혼합 코드베이스에서 작업해야 하면 기능 출시 능력이 떨어집니다. 새 Rust 코드를 기존 Go 컴포넌트와 같은 방식으로 구조화해야 하므로, 훌륭하고 관용적인 Rust를 쓰기 더 어려워집니다. 게다가 기존 사용자들을 위해 전체를 안정적이고 동작 가능하게 유지하면서 이 어려운 리팩터링을 해야 하니, 속도는 더더욱 느려집니다.
그래서 당분간은, 더 많은 곳에 tsnet 같은 것을 가져가면서도 관련된 모든 사람의 고통을 최소화할 수 있는 방법이 두 구현 체제라고 보고 있습니다.
기본적인 것들입니다. 데모를 몇 가지 돌리고 패킷이 흐르도록 하기에 충분한 정도입니다.
최상위로 노출되는 API는 약간 다듬어진 tsnet처럼 보입니다. 프로그램 안에서 Tailscale 디바이스를 띄우고, 여기에 설정을 주고, tailnet의 다른 디바이스들과 TCP와 UDP로 통신할 수 있습니다. 여기에는 Go tailscale 클라이언트와의 상호운용도 포함됩니다.
이 라이브러리를 Rust에서 직접 사용하는 것 외에도, 저희는 Rust와 동일한 API 표면과 기능 집합을 가진 Python, Elixir, C용 FFI 바인딩을 제공합니다. 그리고 Rust 코드용으로는 axum에 Tailscale을 연결하기 위한 유틸리티 크레이트 몇 개와, 올바른 플래그와 설정을 노출하는 CLI를 작성하는 데 도움이 되는 도구도 있습니다.
아직 없는 것은, 대체로 그 외의 모든 것들입니다. P2P 통신과 NAT 트래버설은 TODO 상태라서, 다른 디바이스로 가는 모든 트래픽은 DERP를 거치며 처리량에 제한이 있을 것입니다(데이터 플레인 최적화에도 아직 많은 시간을 쓰지 않았으므로, 다른 병목도 아마 있을 것입니다). DNS 해석도 없어서 IP 주소로 직접 연결해야 합니다. exit node와 app connector 같은 더 고급 네트워킹 기능도 작동하지 않습니다. Tailscale SSH, Taildrop, Taildrive, device posture 같은 더 높은 수준의 기능도 없고, 라이브러리 사용 사례에는 아주 잘 맞지 않기 때문에 영원히 없을 수도 있습니다. 코드에 대한 외부 보안 감사도 아직 이뤄지지 않았으므로, 보안을 얼마나 신뢰할지에 대해서는 보수적으로 접근하셔야 합니다. 아직 구현하지 않은 다른 것들도 분명 제가 빠뜨리고 있을 겁니다.
그러니 네, 아직은 아주 초기 단계이고, 해야 할 일이 많이 남아 있습니다. 하지만 코드가 무언가 할 수 있게 되자마자 저희가 무엇을 해왔는지 보여드리고 싶었고, 완벽해질 때까지 기다리기보다는 여러분의 피드백과 함께 공개적으로 계속 만들어가고 싶었습니다.
가까운 미래에는 다음으로 작업할 분명한 항목들이 있습니다. 직접 연결을 위한 P2P 통신과 NAT 트래버설이 당연히 다음 큰 과제입니다(NAT 트래버설이 어떻게 작동하는지를 어떤 분이 정리해 두셨으니, 꽤 도움이 될지도 모르겠습니다). 또한 네트워킹 기능 집합의 빈틈도 메우고 있습니다. DNS, exit node, TLS 인증서 등이 여기에 포함됩니다.
그 이후 장기적인 목표는, 여러분이 무엇을 하든 상관없이 자신의 코드에 Tailscale을 꽂아 넣을 수 있는 방법을 제공하고, 인터넷만큼 어디에나 있는 존재라는 이상에 다가가는 것입니다. 그래서 저희는 tailscale-rs가 지원하는 기능 집합을 계속 확장하고, 더 많은 언어에 연결하고, 성능을 개선하고, 개발자를 위한 문서와 가이드를 더 많이 작성해 나갈 것입니다…
바로 이 지점에서 여러분의 의견이 필요합니다! 이제 기본기가 작동하게 되었으니, 실험 단계의 소프트웨어에서 1.0으로 가기 위해 해야 할 일들이 정말 말 그대로 풍성하게 쌓여 있어 한동안 바쁘게 지낼 수 있습니다. 단기적으로는 분명한 로드맵이 있지만, 그 이후에는 여러분이 저희가 어디로 가야 한다고 생각하는지 정말 알고 싶습니다. 당장 COBOL용 Tailscale을 만들겠다고 약속할 수는 없지만, 사람들이 정말 그것을 원한다면 적어도 저희는 그것을 알고 계획판에 올려두고 싶습니다.
그러니 tailscale-rs가 써보고 싶은 무언가처럼 들린다면, 꼭 가서 이것저것 시험해보시고 피드백을 주세요! 저희 GitHub에 이슈와 기능 요청을 올릴 수 있습니다(중복 여부를 빠르게 확인하고, 해당된다면 기존 이슈에 추천을 눌러주세요). 그리고 그냥 이야기하거나 질문하고 싶다면 Tailscale의 community Discord에도 저희가 눈팅하고 있습니다.