A tecnologia JIT WireGuard
(fly.io)- A Fly.io mudou a forma de reduzir a carga de estado dos gateways WireGuard mantendo a comunicação direta entre
flyctle Fly Machines: em vez de pré-instalar peers, eles passam a ser adicionados ao kernel no momento da conexão - O fluxo antigo funcionava com a API GraphQL enviando a configuração do peer via RPC sobre NATS, o
wggwdregistrando isso no SQLite e no WireGuard do kernel Linux, e então oflyctlse conectando - A combinação de perda de mensagens no NATS com a criação de peers descartáveis em jobs de CI fez com que centenas de milhares de peers nunca reutilizados se acumulassem no gateway, deixando operações no kernel e o carregamento após reboot mais lentos
- O novo método captura pacotes de
handshake initiationno filtro BPF ou no caminho de recepção via WebSockets, descriptografa parte do handshake Noise para identificar a chave pública e então busca apenas os peers necessários por meio de uma API HTTP interna - Após algumas semanas em produção, a quantidade de peers antigos praticamente desapareceu, e os gateways passaram a lidar com menos estado, configuração de peers mais rápida e reinicializações mais ágeis
Como a Fly.io usa WireGuard
- A Fly.io executa contêineres em VMs baseadas em Firecracker e usa WireGuard em vários pontos, quase como parte da API para clientes
- O
flyctl, ao iniciar, cria sua própria pilha TCP/IP com um endereço IPv6 próprio e se comunica diretamente com Fly Machines na rede da Fly.io - Essa abordagem facilita apresentar recursos como builders remotos de Docker como se estivessem na mesma LAN, mas mantê-la operando de forma confiável é mais difícil
- No fim, a Fly.io mudou o caminho padrão para WireGuard-over-WebSockets
Fluxo antigo de provisionamento dos gateways
- A Fly.io conecta conexões WireGuard que entram por vários servidores de gateway ao redor do mundo às redes privadas apropriadas
- Quando o
flyctlprecisa se comunicar com uma Fly Machine para build de contêiner, console SSH, cópia de arquivos ou proxy de serviços, ele inicia ou se conecta a um processo de agente em segundo plano - Na primeira execução, o agente cria uma nova configuração de peer WireGuard na API GraphQL
- A configuração do peer é composta pela chave pública e pelos endereços a conectar
- A API envia essa configuração ao gateway apropriado por RPC no sistema de mensageria NATS
- O
wggwdno gateway recebe a configuração, salva no SQLite, adiciona ao kernel usando a biblioteca WireGuard Go e então responde à API que a instalação foi concluída - Quando a API retorna a configuração na requisição GraphQL, o
flyctlse conecta usando o peer WireGuard já instalado no gateway
Por que a estrutura antiga ficou lenta
- O NATS é rápido, mas não garante entrega, o que dificultava seu uso como base de uma API confiável
- A Fly.io reduziu o uso interno de NATS e, por exemplo, sua API interna
flydpassou de NATS para HTTP - Mesmo com menos NATS, os gateways WireGuard melhoraram, mas não o suficiente
- A Fly.io reduziu o uso interno de NATS e, por exemplo, sua API interna
- Os peers WireGuard criados permaneciam no gateway mesmo depois que o
flyctlencerrava, e não havia processo para limpar peers antigos- Havia a decisão de não remover os peers porque o usuário poderia fazer novo deploy no dia seguinte ou depurar com
fly ssh console - Mas a maioria dos peers era criada em jobs de CI sem armazenamento persistente, e como não era possível reconectar com o mesmo peer na execução seguinte, um novo peer era criado a cada vez
- Havia a decisão de não remover os peers porque o usuário poderia fazer novo deploy no dia seguinte ou depurar com
- Como resultado, os gateways passaram a manter centenas de milhares de peers que talvez nunca fossem reutilizados
- À medida que o número de peers antigos crescia, as operações do WireGuard no kernel ficavam muito lentas
- O processo de recarregar todos os peers no kernel após reboot dos servidores de gateway era especialmente lento
- Também ocorreram alguns kernel panics
Projeto para instalar peers no kernel só quando necessário
- Manter todo o histórico de peers WireGuard em um único SQLite não é difícil, mas manter todos os peers no kernel Linux se tornou um gargalo
- Em vez de enviar configurações aos gateways, a Fly.io escolheu um modelo em que o gateway busca sob demanda na API apenas os peers necessários
- Se o peer for adicionado ao kernel só quando o cliente tenta se conectar, peers antigos podem ser removidos do kernel a qualquer momento
- Mesmo os peers removidos podem ser buscados e reinstalados na próxima conexão, então o gateway não precisa manter tanto estado de longo prazo
- Porém, o WireGuard no kernel Linux não tem uma API para assinar eventos de “incoming connection attempt”
Como os peers JIT do WireGuard foram implementados
- A interface de configuração do WireGuard no kernel Linux é o Netlink, e a biblioteca de controle WireGuard Go usa o wgctrl-go
- A Fly.io aproveitou o fato de que pedidos de conexão WireGuard são pacotes identificáveis e criou eventos diretamente com filtros BPF e packet socket
- No caminho WireGuard sobre WebSockets, é ainda mais fácil obter pacotes WireGuard brutos
- Esse caminho troca pacotes UDP encapsulados por framing com a interface do gateway por meio de conexões WebSocket sem autenticação
- Como a Fly.io mantém o código desse daemon, foi possível colocar um hook na função de recepção de pacotes
- O WireGuard não tem os conceitos de “cliente” e “servidor”; é um protocolo ponto a ponto em que peers se conectam ao enviar tráfego
- Quem inicia primeiro é o initiator, e o outro lado é o responder
- Na Fly.io, normalmente o
flyctlé o initiator e o gateway é o responder
- O primeiro pacote UDP é chamado de
handshake initiationno artigo do WireGuard, e o tipo do pacote é registrado em 1 byte em texto puro- A Fly.io captura conexões de entrada com o filtro BPF
udp and dst port 51820 and udp[8] = 1
- A Fly.io captura conexões de entrada com o filtro BPF
Identificando o peer no handshake Noise
- O WireGuard é baseado no Noise Protocol Framework, e o Noise esconde identificadores durante o handshake por identity hiding
- Por isso, não dá para ler do pacote um valor como nome de usuário e localizar diretamente a configuração
- Para identificar requisições de entrada, a Fly.io executa parte da criptografia do Noise para descriptografar a identidade
- O código é delicado, mas tem cerca de 200 linhas
- A interface Netlink do kernel pode fornecer a chave privada da interface a processos com privilégio, permitindo obter os segredos necessários
- O código relacionado foi publicado em um gist
- Com isso, eles obtêm um fluxo de eventos com as chaves públicas dos usuários que estão tentando abrir uma conexão WireGuard com o gateway
Otimizações de instalação, cache e retry
- O gateway mantém no SQLite um cache com limitação de taxa e, ao encontrar um novo peer, busca e instala as informações correspondentes por meio de uma requisição à API HTTP interna
- Essa lógica se encaixou bem no pequeno daemon que já gerenciava o WireGuard no gateway
- Peers antigos agora podem ser removidos de forma agressiva com um job de
cron - A consulta à API para um novo peer pode não ser rápida o bastante para responder imediatamente à primeira mensagem de
handshake initiation- Como o WireGuard tenta novamente rapidamente, isso não causa problema de funcionamento
- Um recurso do Netlink do WireGuard no Linux, sugerido por Jason Donenfeld, permite estabelecer a conexão mais rápido
- A partir da mensagem de initiation recebida, é possível obter a 4-tupla de endereços, incluindo a porta de origem temporária do
flyctl - O gateway instala o peer como se ele próprio fosse o initiator e o
flyctlfosse o responder - O kernel Linux inicia a conexão WireGuard em direção ao
flyctl, e o protocolo não depende fortemente de papéis de servidor e cliente - A nova conexão é estabelecida quase na velocidade de instalação
- A partir da mensagem de initiation recebida, é possível obter a 4-tupla de endereços, incluindo a porta de origem temporária do
Resultado em produção
- Essa abordagem está rodando em produção há algumas semanas
- A quantidade de peers WireGuard antigos por gateway, que ia de milhares a centenas de milhares, caiu para quase 0
- O estado que o gateway precisa manter diminuiu
- A configuração de peers ficou mais rápida
- Na reinicialização, diminuiu a necessidade de recarregar peers não utilizados no kernel
1 comentários
Opiniões no Hacker News
Não entendo muito bem a afirmação de que o WireGuard no kernel Linux não tem um recurso para instalar peers quando necessário. Parece possível adicionar peers em tempo de execução: https://serverfault.com/questions/1101002/wireguard-client-a...
Se entendi corretamente, essa etapa já seria tarde demais, e eles parecem querer autenticar antes de adicionar o peer, para que entradas antigas não fiquem na interface
Então a arquitetura parece ser colocar um filtro eBPF na frente da interface, tentar se conectar diretamente para verificar se a contraparte é aprovada com base no roteamento por chave criptográfica e, se passar, adicionar o peer à interface e removê-lo após um timeout
Os peers ficariam todos em algum lugar como SQLite e seriam instalados sob demanda quando o cliente tentasse se conectar
Para um provedor de VPN, a API atual é meio grosseira. Embora, na prática, só uma parte dos peers esteja em uso em um dado momento, quando o número de peers cresce de centenas de milhares para dezenas de milhões, armazenar todos eles em uma única instância do kernel se torna inviável
Se for preciso pré-instalar os peers, no fim eles ficam presos a uma máquina de servidor específica
Como o texto diz, mesmo hoje é possível criar algo parecido com a interface necessária usando uma captura simples de pacotes e, graças ao bom design da API feito por Jason, é muito fácil inverter a direção de iniciação entre servidor e cliente. Mesmo que o kernel descarte a primeira mensagem de iniciação, para o usuário a conexão parecerá acontecer de forma fluida
Jann Horn disse que dava para ir um passo além e guardar o pacote de iniciação capturado para reinjetá-lo no kernel depois de instalar o peer, e isso também é uma ideia bem boa
Não acho que esse texto seja algo que mude vidas; vejo mais como alguns truques elegantes que seria bom as pessoas conhecerem
O próximo passo é usar isso como base para criar floating peers, separando completamente o peer da localidade. Assim, o usuário não precisaria se preocupar em qual região o peer foi configurado, e isso parece ter benefícios reais de produto, além de ser apenas uma diversão de entusiasta
A expressão JIT WireGuard me soa um pouco estranha. Meu primeiro pensamento foi: “por quê? O gargalo de desempenho é a criptografia, e JIT por cliente não vai ajudar nisso”
Eu provavelmente teria ido direto para user space. Dá para extrair desempenho usando algo como tokio-uring ou glommio
Se continuarem forçando tudo dentro do kernel, vão continuar batendo em limites, porque o Linux não foi feito para lidar com milhões de túneis ativos. Mesmo milhões de conexões TCP em um único kernel às vezes já são complicados
Cada limite exige um hack, e cada hack gera configurações de sistema que precisam ser aplicadas e gerenciadas. A cadeia de ferramentas de provisionamento de servidores físicos Linux está muito atrás das ferramentas de desenvolvimento de apps e serviços e de gerenciamento de configuração
Ou será que eu sou burro e entendi alguma coisa errado?
Se você quer criar um peer WireGuard em user space em um app Go, vale olhar este projeto experimental recente: https://github.com/dpeckett/noisysockets
Ele se baseia no excelente trabalho do wireguard-go, mas tentou ser mais simples de usar como biblioteca e mais idiomático em Go
Acho que seria interessante criar uma service mesh com isso. Suportar várias linguagens seria difícil, mas talvez desse para implementar uma API de sockets
Ainda não vi aceleração por hardware para a criptografia do WireGuard, então, em desempenho, talvez seja difícil competir com mTLS
A propósito, estou procurando trabalho freelance no momento; se precisar de um freelancer Golang na área de redes rápidas e seguras, pode entrar em contato
Seria algo como um Magic Wormhole para túneis arbitrários, e espero que também melhore bastante o problema de transferências de arquivos em redes longas e de alta largura de banda que desabam em 20–30 MB/s
0 - https://github.com/slackhq/nebula
Em geral, concordo que, para mensagens ponto a ponto simples, uma requisição HTTP direta pode ser mais confiável do que passar por uma fila de mensagens, mas achei um pouco surpreendente que tantas mensagens fossem perdidas no NATS a ponto de afetar bastante o serviço
Quando uma mensagem é perdida, o NATS não retransmite até dar certo? Fico curioso se alguém sabe por que eles sentiram uma instabilidade perceptível
A arquitetura do NATS é intuitiva e atraente, então queria entender onde as coisas saíram dos trilhos. O JetStream tem muitos parâmetros ajustáveis
Por exemplo, é possível usar streams em memória com uma janela de detecção de duplicatas baseada em tempo, modos push/pull, configurações de retransmissão e políticas de confirmação
Ainda assim, ele pode não combinar bem com conexões pontuais de mensagem única. De qualquer forma, detalhes mais concretos seriam muito úteis
Mas, no fim, não precisávamos dele. A camada de mensagens, em vez de nos dar mais expressividade, só tornava testes e monitoramento mais difíceis
A parte “configuramos o peer como se nós fôssemos o initiator e deixamos o flyctl como responder. O kernel Linux reinicia a conexão WireGuard em direção ao flyctl” na prática adiciona meia latência de ida e volta ao handshake?
Por exemplo, fico curioso se o fluxo é algo como 1) o flyctl envia um Initiation, 2) o peer é adicionado via netlink e um novo Initiation é enviado, 3) o flyctl envia uma Response
Ou seja, acho que a etapa 3 não existe ou não precisa ser esperada, e isso certamente seria verdade se o novo início da etapa 2 fosse bloqueado
1.a) Bob não atende, mas adiciona à agenda o número do identificador de chamadas
Não entendi o que quer dizer “toda vez que você executa o flyctl, nossa adorável e enorme CLI cria uma pilha TCP/IP do nada, tem seu próprio endereço IPv6 e se comunica diretamente com Fly Machines em execução na nossa rede”
A expressão “cria uma pilha TCP/IP do nada” é usada porque normalmente o sistema operacional fornece a pilha TCP/IP como parte do kernel
No wireguard-go, a pilha TCP/IP roda em espaço de usuário, então ela pode ser criada dentro de um processo comum em espaço de usuário, como a interface de linha de comando flyctl
Para quem lida com sistemas há muito tempo, isso pode parecer bem mágico. Pilhas TCP/IP em espaço de usuário dentro do próprio processo e realmente utilizáveis são algo relativamente novo e original
Fico curioso sobre o que impede que o primeiro pacote de handshake seja reinjetado na pilha de rede. Assim parece que não haveria perda de pacote
Também fico curioso sobre o objetivo de verificar
udp[8] = 1no filtro eBPFComo foi dito no comentário ao lado, o filtro BPF só captura pacotes de iniciação, e esse é o comportamento desejado. É a versão WireGuard de sniffar SYNs para ver o início de uma conexão TCP
udp[8] = 1filtra apenas pacotes de handshake. Sem isso, os pacotes de dados também seriam enviados para o daemon em espaço de usuárioNão tenho certeza se é possível reproduzir o primeiro handshake, mas talvez seja, já que o WireGuard ignora clientes desconhecidos
Acho interessante que, por padrão, eles tunelem o WireGuard sobre WebSocket. Não é bom para desempenho, mas deve ser aceitável para tarefas de estilo DevOps em que o flyctl é usado
Eu também me perguntava sobre isso ao pensar no futuro do QUIC/HTTP3. A chance de operadores de rede simplesmente bloquearem a porta UDP 443, em vez de tratá-la corretamente, não é zero
flyctlSe UDP não funciona, não funciona mesmo e é difícil de depurar, então deixamos o padrão no caminho que sabemos com certeza que funciona
É amargo admitir que perdi o debate sobre qual padrão escolher
Minha startup usou a Fly por quase um ano. A funcionalidade principal de transformar código em código implantado em menos de um minuto é realmente linda
Subir e derrubar novos nós para backfill também leva só alguns segundos
Mas a empresa em si parecia um pouco imatura. Uma vez, nosso servidor de API ficou inacessível na Fly por 48 horas, e eu não tinha certeza se era erro de configuração meu ou outra indisponibilidade “silenciosa”
Eles têm um produto “db”, mas é algo na linha de “não é Postgres gerenciado”, e ali também havia interrupções constantes
Parecia estranho adicionar Postgres como substantivo de primeiro nível na CLI e, ao mesmo tempo, limitar o escopo das funcionalidades suportadas
O acesso à API do serviço principal também caía com frequência, então era preciso esperar para publicar correções em novos serviços
Sinto falta da experiência de deploy, mas, sinceramente, hoje estou mais satisfeito com o Cloud Run da GCP. Tem muito menos “surpresas” e a documentação é muito mais completa
Acho curioso que provedores de VPS quase não ofereçam suporte para reduzir a latência de serviços de backend para usuários. Nenhum oferece Anycast, e há pouquíssimas opções de GeoDNS
Dito isso, GeoDNS adiciona uma complexidade à parte
Eu gostaria que o custo de transferência de dados da Fly.io fosse mais barato. Do jeito que está, para um serviço parecido com ngrok em que estou trabalhando, preciso reimplementar de forma meio desajeitada uma boa parte dos recursos da Fly.io
[0]: https://lastlogin.io
[1]: O código específico da Fly necessário para executar o LastLogin de forma distribuída pelo mundo todo é mais ou menos isto: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
Quando funcionava bem era muito fluido, mas isso não acontecia com frequência suficiente
Quero aproveitar a oportunidade para apresentar o Netmaker[0]
Não tenho relação com eles; sou apenas alguém que precisa de acesso privado a VPCs da AWS em várias contas e usa a ferramenta com satisfação. Gostaria que ela fosse adotada mais amplamente
[0] https://www.netmaker.io/
No emprego anterior, configuramos e gerenciamos wg com Ansible em algumas máquinas Windows e Linux; funcionava bem, mas no fim ficou meio bagunçado
O site é vago demais
A parte sobre “um gateway com centenas de milhares de peers, entre eles peers que nunca mais serão usados” foi exatamente o que me veio à cabeça ao ler os primeiros parágrafos
A ideia de que “não existe uma chamada de API para assinar eventos de tentativa de conexão de entrada. Tudo bem. Nós mesmos criamos o evento. Uma solicitação de conexão do WireGuard é um pacote e é fácil de identificar, então dá para capturá-la com eficiência usando um filtro BPF e um socket de pacotes” também é boa
Pelo que dizem, ao receber a mensagem inicial de entrada, você passa a ter o endereço de 4-tupla da conexão desejada, incluindo até a porta de origem temporária usada pelo flyctl, e então instala o peer como se nós fôssemos o initiator e o flyctl fosse o responder; fico curioso se isso funciona mesmo atrás de NAT
{wggwd.fly.io, 12345, clientIP, 23456}Para o NAT UDP no caminho, tanto um novo pacote UDP do “initiator” quanto uma resposta à mensagem inicial de saída parecem exatamente a mesma coisa
O critério de decisão é apenas a 4-tupla, e essa 4-tupla é a mesma