Sans-IO: o segredo eficaz do Rust para serviços de rede
(firezone.dev)- Na Firezone, o Rust é usado para criar acesso remoto seguro e escalável em celulares Android, computadores macOS e servidores Linux
- Uma biblioteca de conexão chamada
connlibé usada para gerenciar conexões de rede e túneis WireGuard - Após várias iterações, chegou-se a um design chamado sans-IO, que oferece testes rápidos e minuciosos, personalização profunda e alta confiabilidade
connlib foi escrita em Rust e segue o design sans-IO
- Graças à velocidade e à segurança de memória do Rust, ele é adequado para construir serviços de rede
- Usa o runtime
tokio, WebSockets comtungstenite, a implementação WireGuardboringtune criptografia do tráfego de API comrustls - O design sans-IO implementa protocolos como máquinas de estado puras, em vez de enviar e receber bytes por sockets em vários pontos
O modelo assíncrono do Rust e a discussão sobre "colorir funções"
- Funções assíncronas só podem ser chamadas por outras funções assíncronas
- Se uma função no fundo da cadeia for assíncrona, todas as funções que a chamam também precisam se tornar assíncronas
- Isso pode ser um problema para quem quer escrever código indiferente ao fato de as dependências serem assíncronas ou não
Introdução ao sans-IO
- A ideia central do sans-IO é semelhante ao princípio da inversão de dependência no mundo OOP
- A política (o que fazer) não deve depender dos detalhes de implementação (como fazer)
- Em vez de enviar dados usando a struct
Transmit, emite-se umTransmit
Aplicando a inversão de dependência
- Em vez de enviar dados usando a struct
Transmit, emite-se umTransmit - O loop de eventos implementa os efeitos colaterais e de fato chama
UdpSocket::send
Máquina de estado
- O diagrama da máquina de estado de uma solicitação de binding STUN tem dois estados:
SenteReceived - A máquina de estado é implementada definindo a struct
StunBindinge funções relacionadas
Loop de eventos
- O loop de eventos conduz a máquina de estado e processa os dados usando
poll_transmitehandle_input
Abstração de tempo
- Requisitos baseados em tempo são tratados usando as APIs
poll_timeoutehandle_timeout
Premissas do sans-IO
- O design sans-IO deixa para a aplicação a decisão sobre as dependências serem assíncronas ou não
- O design sans-IO é fácil de compor, oferece APIs flexíveis, facilita testes e combina bem com os recursos do Rust
Composição fácil
- A API de
StunBindingpode ser aplicada à maioria dos protocolos de rede - A biblioteca
snownetda Firezone combina ICE e WireGuard para oferecer um túnel IP "mágico" que funciona independentemente da configuração da rede
API flexível
- Escrever o loop de eventos manualmente permite ajustar o código e facilita a manutenção
Testes rápidos
- Como o código sans-IO não tem efeitos colaterais, ele é muito fácil de testar
- Na Firezone, testes são feitos implementando uma máquina de estado de referência e comparando-a com o estado real da
connlib
Casos de borda e falhas de IO
- O design sans-IO separa a implementação do protocolo dos efeitos colaterais reais de IO, facilitando o tratamento de casos de borda e erros
Rust + sans-IO: combinação perfeita?
- O Rust modela explicitamente propriedade e mutabilidade, o que combina bem com o design sans-IO
- O design sans-IO usa livremente
&mutpara expressar mudanças de estado e, ao contrário do Rustasync, usa apenas APIs síncronas
Desvantagens
- Escrever o loop de eventos manualmente pode introduzir bugs sutis
- Fluxos de trabalho sequenciais podem exigir mais código
- O design sans-IO ainda não é amplamente usado na comunidade Rust
Conclusão
- Código sans-IO pode parecer estranho no início, mas se torna muito agradável quando você se acostuma
- Rust oferece ótimas ferramentas para modelar máquinas de estado
- O design sans-IO força o tratamento de erros como parte do processamento de entrada, o que faz parecer a forma correta de escrever código de rede
Opinião do GN⁺
- O design sans-IO combina bem com o modelo de propriedade do Rust e é muito adequado para implementar protocolos de rede
- Escrever o loop de eventos manualmente aumenta a flexibilidade do código e facilita a manutenção
- A facilidade de teste ajuda bastante a escrever código confiável
- Porém, como ainda não é amplamente usado na comunidade Rust, pode haver falta de bibliotecas relacionadas
- Ao adotar uma nova tecnologia, é preciso considerar a curva de aprendizado e o suporte da comunidade
1 comentários
Comentários do Hacker News
Antes da introdução da sintaxe async/await no Rust, era preciso implementar máquinas de estado manualmente
Ao escrever uma biblioteca VT100, percebeu-se um problema no padrão de encapsulamento do Rust
Comparação com um design que transmite dados usando canais
No ecossistema Haskell, existe a ideia de separar lógica e execução
tokio::select!foi encapsuladaFunções async do Rust são compiladas em máquinas de estado
Ao expor o estado, funções async podem se tornar “puras”
Firezone é uma ferramenta impressionante
Seria bom se o compilador pudesse converter código async em sans-IO automaticamente
Depois de ler o artigo e os comentários, parece que isso reinventou o estilo de arquitetura hexagonal ou ports/adapters
Fica a dúvida se o tráfego real passa pelo gateway ou se ele é usado apenas para estabelecer a conexão