3P by neo 2달전 | favorite | 댓글 1개
  • Firezone에서는 Rust를 사용하여 Android 폰, MacOS 컴퓨터 또는 Linux 서버에서 확장 가능한 안전한 원격 액세스를 구축함
  • connlib라는 연결 라이브러리를 사용하여 네트워크 연결과 WireGuard 터널을 관리함
  • 여러 번의 반복 끝에 sans-IO라는 설계에 도달하여 빠르고 철저한 테스트, 깊은 커스터마이징, 높은 신뢰성을 제공함

connlib는 Rust로 작성되었으며 sans-IO 설계를 따름

  • Rust의 속도와 메모리 안전성 덕분에 네트워크 서비스 구축에 적합함
  • tokio 런타임, tungstenite WebSockets, boringtun WireGuard 구현, rustls API 트래픽 암호화 등 사용
  • sans-IO 설계는 여러 곳에서 소켓을 통해 바이트를 보내고 받는 대신 순수 상태 기계로 프로토콜을 구현함

Rust의 비동기 모델과 "함수 색칠" 논쟁

  • 비동기 함수는 다른 비동기 함수에서만 호출될 수 있음
  • 비동기 함수 깊숙이 있는 함수는 호출하는 모든 함수도 비동기 함수로 만들어야 함
  • 이로 인해 종속성의 비동기 여부에 대해 무관심한 코드를 작성하고자 하는 사람들에게 문제가 될 수 있음

sans-IO 소개

  • sans-IO의 핵심 아이디어는 OOP 세계의 의존성 역전 원칙과 유사함
  • 정책(무엇을 할지)은 구현 세부 사항(어떻게 할지)에 의존하지 않아야 함
  • Transmit 구조체를 사용하여 데이터를 전송하는 대신 Transmit을 방출함

의존성 역전 적용

  • Transmit 구조체를 사용하여 데이터를 전송하는 대신 Transmit을 방출함
  • 이벤트 루프는 부작용을 구현하고 실제로 UdpSocket::send를 호출함

상태 기계

  • STUN 바인딩 요청의 상태 기계 다이어그램은 SentReceived 두 가지 상태를 가짐
  • StunBinding 구조체와 관련 함수들을 정의하여 상태 기계를 구현함

이벤트 루프

  • 이벤트 루프는 상태 기계를 구동하며, poll_transmithandle_input을 사용하여 데이터를 처리함

시간 추상화

  • poll_timeouthandle_timeout API를 사용하여 시간 기반 요구 사항을 처리함

sans-IO의 전제

  • sans-IO 설계는 종속성의 비동기 여부에 대한 결정을 애플리케이션으로 미룸
  • sans-IO 설계는 조합이 쉽고, 유연한 API를 제공하며, 테스트가 용이하고, Rust의 기능과 잘 맞음

쉬운 조합

  • StunBinding의 API는 대부분의 네트워크 프로토콜에 적용 가능함
  • Firezone의 snownet 라이브러리는 ICE와 WireGuard를 결합하여 네트워크 설정에 관계없이 작동하는 "마법" IP 터널을 제공함

유연한 API

  • 이벤트 루프를 직접 작성하면 코드 튜닝이 가능하고 유지 관리가 쉬움

빠른 테스트

  • sans-IO 코드는 부작용이 없으므로 테스트가 매우 용이함
  • Firezone에서는 참조 상태 기계를 구현하여 connlib의 실제 상태와 비교하는 테스트를 수행함

엣지 케이스와 IO 실패

  • sans-IO 설계는 프로토콜 구현을 실제 IO 부작용과 분리하여 엣지 케이스와 오류 처리를 쉽게 만듦

Rust + sans-IO: 천생연분?

  • Rust는 소유권과 가변성을 명시적으로 모델링하여 sans-IO 설계와 잘 맞음
  • sans-IO 설계는 &mut를 자유롭게 사용하여 상태 변경을 표현하고, async Rust와는 달리 동기 API만 사용함

단점

  • 이벤트 루프를 직접 작성하면 미묘한 버그가 발생할 수 있음
  • 순차적 워크플로우는 더 많은 코드를 요구할 수 있음
  • Rust 커뮤니티에서 sans-IO 설계는 아직 널리 사용되지 않음

마무리

  • sans-IO 코드는 처음에는 생소하지만 익숙해지면 매우 즐거움
  • Rust는 상태 기계를 모델링하는 데 훌륭한 도구를 제공함
  • sans-IO 설계는 오류 처리를 입력 처리의 일부로 강제하여 네트워킹 코드를 작성하는 올바른 방식처럼 느껴짐

GN⁺의 의견

  • sans-IO 설계는 Rust의 소유권 모델과 잘 맞아 네트워크 프로토콜 구현에 매우 적합함
  • 이벤트 루프를 직접 작성하면 코드의 유연성과 유지 관리가 용이해짐
  • 테스트가 용이하여 안정적인 코드를 작성하는 데 큰 도움이 됨
  • 그러나 Rust 커뮤니티에서 널리 사용되지 않아 관련 라이브러리가 부족할 수 있음
  • 새로운 기술을 도입할 때는 학습 곡선과 커뮤니티 지원을 고려해야 함
Hacker News 의견
  • Rust의 async/await 문법 도입 전에는 수동으로 상태 기계를 구현했었음

    • Rust의 async/await 문법 덕분에 생산성이 크게 향상되었음
    • Rust의 async는 자동 상태 기계로 변환되어 I/O 지점에서 값을 저장해줌
  • VT100 라이브러리를 작성하면서 Rust의 캡슐화 패턴 문제를 깨달았음

    • 캡슐화에 집착하는 것이 문제를 일으킴
    • 컴퓨터는 입력, 데이터 변환, 출력을 수행하는 기계임을 상기시킴
  • 채널을 사용하여 데이터를 전송하는 디자인과 비교

    • 코드가 복잡해짐
    • 메시지 타입을 수동으로 구현해야 함
    • 송신기를 명시적으로 제공해야 함
    • 네트워크 전송 실패 시 결과를 얻지 못함
    • 그러나 편리한 점도 있음
  • Haskell 생태계에서 논리와 실행을 분리하는 아이디어가 있음

    • tokio::select! 호출을 어떻게 캡슐화했는지 언급되지 않음
    • sans-IO 스타일로 캡슐화된 함수 구현에 관심이 있었음
  • Rust의 async 함수는 상태 기계로 컴파일됨

    • sans-io와 async를 결합하려는 시도가 있었는지 궁금함
    • 주요 문제는 사용성 및 Pin 처리임
  • 상태를 노출하면 async 함수가 '순수'해질 수 있음

    • OpenSSL을 async Rust에 바인딩하려고 시도했음
  • Firezone이 놀라운 도구임

    • Rust-libp2p와 유사한 패턴을 발견했음
  • 컴파일러가 async 코드를 sans io로 자동 변환할 수 있으면 좋겠음

    • 수동 변환은 오류가 발생하기 쉬움
  • 기사와 댓글을 읽고 hexagonal 또는 ports/adapters 아키텍처 스타일을 재발명한 것 같음

  • 실제 트래픽이 게이트웨이를 통해 지나가는지, 아니면 연결 설정에만 사용되는지 궁금함