SSH, modo de usuário, TCP / IP e WireGuard

Qualquer pessoa que hospeda um aplicativo de um provedor como Fly.io (doravante simplesmente Fly) pode precisar se conectar ao servidor que executa esse aplicativo via SSH.



Mas Fly é como uma ovelha negra entre outras plataformas semelhantes. Nosso hardware funciona em data centers espalhados pelo mundo. Nossos servidores estão conectados à Internet através da rede Anycast, e eles são conectados uns aos outros através da rede WireGuard. Pegamos os contêineres Docker dos usuários e os transformamos em microvirtuais do Firecracker. E quando começamos, fizemos exatamente isso para dar aos nossos clientes a capacidade de executar "aplicativos de ponta". Esses aplicativos são geralmente pedaços de código relativamente pequenos e autocontidos, altamente sensíveis ao desempenho da rede. Como resultado, esses trechos de código precisam ser executados em servidores localizados o mais próximo possível dos usuários. Em tal ambiente, a capacidade de se conectar ao servidor via SSH não é tão importante.







Mas agora nem todos os nossos clientes usam Fly dessa forma. Hoje em dia, no ambiente Fly, você pode executar facilmente todo o código relacionado a uma aplicação. Simplificamos o procedimento para iniciar um conjunto de serviços em um ambiente em cluster. Esses serviços podem, através de canais de comunicação seguros, interagir uns com os outros, podem armazenar dados de forma permanente, podem, através da rede WireGuard, comunicar-se com os seus operadores. Se eu continuar a história sobre nosso sistema com o mesmo espírito, terei que fornecer links para todos os materiais que escrevemos nos últimos meses.



Mas, em qualquer caso, não tínhamos suporte SSH normal.



É claro, é claro, que você pode simplesmente construir um contêiner com um serviço SSH ao qual pode se conectar por SSH. A plataforma Fly suporta o trabalho com portas TCP comuns (e portas UDP também). Se o cliente, usando o arquivo fly.toml



, "informar" nossa rede Anycast sobre sua porta SSH estranha, o sistema organizará o roteamento de suas conexões SSH, após o que tudo funcionará como deveria.



Mas aqueles que criam contêineres geralmente não fazem isso, e não sugerimos que o façam. Como resultado, equipamos o Fly com suporte SSH. O que fizemos foi organizado de uma forma bastante incomum. Neste artigo, que consiste em duas partes, falarei sobre isso.



Parte 1: 6PN e Hallpass



Eu escrevi muito sobre como as redes privadas são organizadas no Fly. Resumindo, o que temos pode ser comparado a uma versão IPv6 simplificada das "nuvens privadas virtuais" GCP ou AWS. Chamamos esse sistema de 6PN. Quando uma instância do aplicativo (máquina microvirtual do Firecracker) é iniciada no Fly, atribuímos um prefixo IPv6 especial a essa instância. Vários identificadores são codificados no prefixo: o identificador do aplicativo, a organização que possui o aplicativo e os recursos de hardware nos quais o aplicativo está sendo executado. Usamos um pouco do código eBPF para rotear estaticamente esses pacotes IPv6 em nossa rede interna WireGuard e para garantir que os clientes não possam se conectar aos sistemas das organizações com as quais não estão envolvidos.



Você também pode usar o WireGuard para conectar as redes IPv6 privadas que criamos com outras redes. Nossa API é capaz de criar configurações WireGuard que podem ser usadas, por exemplo, em hosts EC2 para proxy RDS Postgres . Ou, se necessário, você pode usar clientes WireGuard (no Windows, Linux ou macOS) para conectar o computador de desenvolvimento à sua rede privada.



Você provavelmente já sabe aonde quero chegar. Escrevemos um servidor SSH



muito pequeno e muito simples em Go chamado Hallpass. Pode ser comparado a "Hello, World!" Criado com a biblioteca Go x/crypto/ssh



... (Se eu fizesse isso de novo, provavelmente apenas usaria o pacote Glider Labs para construir servidores SSH. Usando este pacote, nosso servidor seria literalmente um "Hello, World!" A inicialização de todas as instâncias de máquinas microvirtuais Firecracker é realizada e Hallpass é lançado com vinculação a seus endereços 6PN.



Se você é capaz de operar na rede 6PN de sua organização (digamos, por meio de uma conexão WireGuard), isso significa que você pode fazer login na instância microvirtual usando Hallpass.



Há apenas um detalhe interessante sobre como funciona o Hallpass. Trata-se de autenticação. Os elementos de infraestrutura em nossa rede de produção geralmente não têm acesso direto às nossas APIs ou aos seus bancos de dados subjacentes. E as próprias instâncias do Firecracker, é claro, também não têm esse acesso. Isso leva a algumas dificuldades associadas à alteração das configurações de comunicação. Como, por exemplo, você pode responder à pergunta sobre que tipo de chaves você precisa ter para se conectar a certas instâncias de máquinas microvirtuais?



Encontramos uma solução alternativa para esse problema, recorrendo a certificados de cliente SSH. Em vez de ter que lidar com a entrega das chaves toda vez que um usuário deseja efetuar login de um novo host, criamos um certificado raiz para organizar esse usuário. A chave pública para este certificado raiz está hospedada em nosso sistema DNS privado, e Hallpass contata o DNS para obter este certificado sempre que houver uma tentativa de login. Nossa API assina novos certificados para usuários, esses certificados podem ser usados ​​para entrar no sistema.



Você pode ter dúvidas sobre esta solução. Portanto, revelarei mais alguns detalhes sobre ele.



Primeiro, vamos falar sobre certificados. Décadas da Loucura X.509”Pode ter feito com que a palavra“ certificado ”deixasse um gosto desagradável na boca. E eu não te culpo por isso. Mas os certificados devem ser usados ​​ao organizar conexões SSH, uma vez que tais certificados, neste caso, são uma boa solução. No entanto, os certificados SSH não são certificados X.509. Ele usa seu próprio formato OpenSSH e, em geral, nada de especial pode ser dito sobre esses certificados. Eles, como todos os outros certificados, têm uma "data de validade", o que permite que você crie chaves de curta duração (e isso é, quase sempre, exatamente o que você precisa). E, é claro, eles permitem que você atribua uma chave pública a um grupo inteiro de servidores, o que pode autorizar um número arbitrário de chaves privadas. Não há necessidade de atualizar constantemente os servidores correspondentes.



A seguir está nossa API e assinatura de certificado. Nós vamos! Somos muito cuidadosos, mas esses certificados geralmente são tão seguros quanto os tokens de acesso Fly. No momento, os certificados não podem ser mais protegidos do que os tokens, já que o token permite a implantação de novas versões de contêineres de aplicativos. Trabalhar com Web PKI X.509 CA envolve muitas formalidades. Nós fazemos sem eles.



E, finalmente, nosso DNS. Ela, eu concordo, parece um absurdo completo. Mas realmente não é tão ruim. Cada host executando instâncias microvirtuais do Firecracker executa uma versão local de nosso servidor DNS privado (um pequeno programa escrito em Rust). O código eBPF garante que as máquinas do Firecracker só possam interagir com este servidor DNS, referindo-se a ele a partir do endereço 6PN de seu servidor. (Do ponto de vista técnico, um usuário só pode fazer consultas à API DNS privada deste servidor, e todas as consultas de outros usuários serão processadas recursivamente.) Um servidor DNS pode (sei que parece incomum) identificar uma organização de forma confiável analisando as solicitações de endereços IP de origem. Em geral, é assim que trabalhamos.



Tudo isso acontece nas profundezas do nosso sistema, os usuários não podem ver tudo isso. Os usuários viram apenas um comando flyctl ssh issue -a



que solicitou um novo certificado de nossa API e, em seguida, o passou para o agente SSH local, após o qual as conexões SSH, em geral, tornaram-se operacionais. Tudo isso foi organizado de maneira bem organizada. Mas qualquer negócio sempre pode ser feito com mais precisão do que antes.



Parte 2: trabalhando em uma rede WireGuard do modo de usuário usando TCP / IP



Há um problema com o esquema acima de uso de SSH, que nem todo mundo tem o WireGuard instalado. O programa correspondente, entretanto, deve ser instalado por todos. WireGuard é uma ótima tecnologia que ajuda muito no gerenciamento de aplicativos executados na plataforma Fly. Mas, seja como for, alguns de nossos usuários não possuem o WireGuard.



É verdade que esses usuários também precisam trabalhar com seus sistemas via SSH.



À primeira vista, o fato de alguém não ter o WireGuard instalado pode parecer um obstáculo intransponível. Como funciona o WireGuard? Uma nova interface de rede é criada no computador do usuário. Esta é uma interface WireGuard em nível de kernel (no Linux) ou um túnel com um serviço WireGuard em modo de usuário anexado a ele (em todos os outros sistemas operacionais). Sem esta interface de rede, você não pode trabalhar com a rede WireGuard.



Mas se você olhar o WireGuard do ângulo certo, verá que, do ponto de vista técnico, não é esse o caso. Ou seja, os privilégios de nível de sistema operacional são necessários para configurar uma nova interface de rede. Mas para enviar pacotes para 51820/udp



nenhum privilégio é necessário. Qualquer coisa necessária para fazer o protocolo WireGuard funcionar pode ser iniciado como um processo sem privilégios em execução no modo de usuário. É assim que funciona o pacote wireguard-go .



Isso só permitirá que você execute o procedimento de handshake do WireGuard. Mas, ao mesmo tempo, não estamos falando sobre a troca de informações com os nós da rede WireGuard, já que você não pode simplesmente pegar e enviar alguns dados arbitrários para outro sistema conectado a esta rede. Esse sistema escuta os pacotes que normalmente seriam transmitidos por redes TCP / IP. As ferramentas de sistema padrão que oferecem suporte a soquetes UDP não ajudam em nada no estabelecimento de uma conexão TCP usando tais soquetes.



Seria difícil escrever um pequeno trecho de código que habilitasse o TCP em modo de usuário, projetado exclusivamente para oferecer suporte à comunicação pela rede WireGuard, novamente em modo de usuário? Tal código permitiria aos usuários do Fly se conectar aos seus sistemas via SSH sem ter que instalar o software que alimenta o WireGuard.



Fui imprudente ao discutir tudo isso no canal Slack em que Jason Donenfeld estava. Ou seja, depois de pensar em voz alta, fui para a cama. Quando acordei, Jason já havia implementado tudo isso usando o gVisor e incluído na biblioteca WireGuard.



O mais interessante aqui é o gVisor. Já escrevemos sobre isso ... Se alguém não souber, o gVisor é essencialmente um sistema operacional Linux de espaço de usuário, Linux implementado em Golang, usado como um substituto runc



para a execução de contêineres. Este é realmente um projeto completamente insano. E se você o usar, então suponho que você possa contar aos outros com orgulho, porque é simplesmente uma coisa linda. Em sua profundidade, há uma implementação TCP / IP completa, escrita em Go, que opera em dados de entrada e saída representados como buffers comuns []byte



.



Em seguida, alguns tweets foram tuitados e, algumas horas depois, recebi um e-mail muito bom de Ben Barkert... Ben já havia trabalhado em várias tarefas relacionadas ao subsistema de rede gVisor, ele estava interessado no que estávamos trabalhando, ele queria saber se gostaríamos de cooperar com ele. Gostamos da ideia de trabalhar juntos neste projeto. E agora, sem entrar em detalhes, temos uma implementação SSH baseada em certificado que é executada por meio da implementação gVisor TCP / IP em modo de usuário. Tudo isso interage com a rede WireGuard por meio de um pacote de modo personalizado wireguard-go



. E, finalmente, essa coisa está embutida no flyctl



.



Para usar o SSH usando flyctl



- basta inserir um comando como este:



flyctl ssh shell personal dogmatic-potato-342.internal

      
      





E agora, para que você possa perceber o quão incrível é o que está acontecendo, vou lhe contar um pouco sobre este comando. Portanto - dogmatic-potato-342.internal



é um nome DNS interno que só é resolvido por um servidor DNS privado na rede 6PN. Tudo isso é eficiente devido ao fato de que no modo o ssh shell



utilitário flyctl



usa o modo de usuário gVisor da pilha TCP / IP. Mas não há código no gVisor para fazer uma pesquisa DNS. Esta é apenas uma biblioteca Go padrão que enganamos ao inserir nossa interface TCP / IP especial nela.



Flyctl



, a propósito, este é um projeto de código aberto(Deve ser assim, uma vez que os clientes precisam usá-lo em seus próprios computadores nos quais estão envolvidos no desenvolvimento). Portanto, se você estiver interessado, basta ler o código. Ben escreveu um código interessante na pasta pkg . E o resto do código, horrível, eu escrevi. Em Go, fornecer comunicações IP na rede WireGuard é surpreendentemente simples. Se você já fez programação TCP / IP de baixo nível, talvez ache essa simplicidade incrível. Os objetos da pilha TCP do gVisor se conectam diretamente ao código de rede da biblioteca padrão.



Dê uma olhada neste código:



tunDev, gNet, err := netstack.CreateNetTUN(localIPs, []net.IP{dnsIP}, mtu)
if err != nil {
    return nil, err
}

// ...

wgDev := device.NewDevice(tunDev, device.NewLogger(cfg.LogLevel, "(fly-ssh) "))

      
      





CreateNetTUN



É uma parte wireguard-go



. É aqui que os recursos do gVisor são usados. Em primeiro lugar, temos à nossa disposição um dispositivo de túnel sintético que pode ser usado para ler e escrever pacotes comuns que fornecem operação WireGuard. Em segundo lugar, temos a função net.Dialer , um wrapper para gVisor, que pode ser usado no código Go e através dele interagir com a rede WireGuard correspondente.



É tudo? Em geral, sim. Por exemplo, aqui está como usamos esses mecanismos para trabalhar com DNS:



resolv: &net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
        return gNet.DialContext(ctx, network, net.JoinHostPort(dnsIP.String(), "53"))
    },
},

      
      





Este é um código de rede normal escrito em Go. Em geral, acabou bem.



Obviamente, todos deveriam fazer isso.



Graças a algumas centenas de linhas de código (isto é - além do código de implementação do modo de usuário do Linux que obtemos do gVisor; mas o que fazer - não há como escapar das dependências), você pode obter uma nova rede com criptografia autenticação à sua disposição. Uma rede acessível a qualquer momento e a partir de quase todos os programas.



É claro que essa rede é significativamente mais lenta do que aquela baseada na implementação central do TCP / IP. Mas muitas vezes é realmente importante? E, em particular, muitas vezes tem algum significado na resolução de problemas que surgem periodicamente, para cuja solução costumam ser construídos túneis TLS estranhos e desconhecidos de quê? Quando a velocidade é importante, você pode simplesmente mudar para a implementação normal do WireGuard.



Em todo caso, o que eu disse resolveu nosso grande problema. Afinal, esse sistema é adequado não apenas para organizar o trabalho do SSH. Também hospedamos bancos de dados Postgres. É muito conveniente quando é possível, executando um simples comando, abrir literalmente um shell de qualquer lugar psql



, independentemente de ser possível, no momento certo, instalar o WireGuard para macOS.



Você está usando o WireGuard?






All Articles