Falha de client-connect no OpenVPN: a limpeza no disconnect é garantida?
Um cliente rejeitado não desfaz uma regra de firewall nem um registro escrito. Compare o contrato do callback com a reversão que o seu hook precisa fazer sozinho.
Quando um script client-connect termina com um código diferente de zero, o OpenVPN rejeita o cliente, e os operadores costumam supor que o script client-disconnect correspondente arruma tudo. Muitas vezes ele nunca é executado, porque o processamento do disconnect depende do que a fase connect conseguiu concluir. Tratar rejeição e reversão como um único mecanismo é o que transforma um login falho numa regra de firewall vazada ou num registro órfão. São duas obrigações separadas, e apenas uma delas é automática. Leia o caminho de saída do próprio hook antes de culpar o servidor por deixar estado para trás.
Uma saída diferente de zero rejeita o cliente; não desfaz o seu trabalho
O status de saída do client-connect é uma decisão de admissão. Zero significa admitir; qualquer outra coisa significa derrubar a instância antes de ela estar plenamente estabelecida. Nada nesse contrato inspeciona o que o script já fez no mundo externo. Se o script adicionou uma regra de iptables, escreveu uma linha ou incrementou um contador, essas mudanças sobrevivem à rejeição.
A mesma assimetria vale para plugins, onde várias funções connect podem rodar para uma única instância. Uma falha posterior não cancela retroativamente um sucesso anterior, e a função anterior não tem como saber que uma função irmã falhou.
A rejeição é automática, a limpeza não
O manual do OpenVPN 2.6 vincula o processamento do client-disconnect ao sucesso do processamento connect anterior. Uma exceção documentada cobre as cadeias em cascata: se um passo connect já retornou sucesso, os passos disconnect correspondentes ainda são executados quando a instância é derrubada, mesmo que um passo connect posterior tenha falhado. Essa exceção é estreita e depende de quantos callbacks estão configurados, por isso testar um único script de shell não diz nada sobre uma cadeia.
Mapeie a configuração real antes de projetar em torno dela: qual script, qual plugin e em que ordem eles rodam. A tabela abaixo lista o que uma configuração pela metade deixa para trás.
| What the hook did before failing | What the client sees | What stays behind |
|---|---|---|
| Created a firewall entry | A rejection, with no tunnel | A rule matching an address nobody owns |
| Wrote a state row | A rejection, with no tunnel | A record of a session that never started |
| Allocated a tunnel address | A rejection, with no tunnel | An address marked busy until something releases it |
Cada linha termina igual para o cliente e de forma diferente para o host. O cliente não consegue saber qual delas ocorreu, então o hook precisa desfazer o que fez antes de devolver o status de falha.
Compense dentro do script que falhou
Valide tudo o que puder antes de tocar no estado compartilhado: campos do certificado, disponibilidade do endereço, o grupo a que o cliente pertence. A rejeição mais barata é a que acontece antes de qualquer alocação. Quando a configuração abrange várias operações externas, mantenha um registro de quais foram concluídas e desfaça-as em ordem inversa antes de devolver o status de falha.
Um caso concreto: o script cria uma entrada de firewall por cliente e depois falha ao escrever a linha de estado que a descreve. O ramo que falha deve remover a entrada que acabou de criar e então sair com um código diferente de zero. Esperar por um evento disconnect que talvez nunca seja agendado é como uma regra sobrevive à sessão para a qual foi pensada.
Torne a compensação idempotente e com proprietário
A limpeza precisa ser segura de executar duas vezes e precisa quanto ao que pode apagar. Um evento tardio de uma tentativa anterior não deve remover a alocação de uma sessão mais nova só porque as duas compartilham um nome comum ou um endereço de túnel reciclado. Registre o identificador ou o id de operação específico junto com o recurso, e apague por essa chave em vez de pelo nome. As transações de banco de dados ajudam apenas com as mudanças próprias do banco; uma regra de firewall fora dessa fronteira exige um passo de compensação explícito.
Presuma que o processo pode morrer entre a mudança e a limpeza, e acrescente um reconciliador que compara o estado externo com a lista de clientes admitidos segundo um cronograma.
Teste os três caminhos que realmente diferem
Pratique três casos: uma rejeição antes de qualquer alocação, uma depois de uma alocação parcial e uma sessão admitida normal que termina mais tarde. Esses três produzem linhas de log diferentes e resíduos diferentes. Verifique o estado externo depois de cada um, não apenas a mensagem de falha do cliente, porque a falha do cliente é o resultado esperado em dois dos três.
Registre a fase do hook, o status de saída e o que foi alocado ou liberado, sem as credenciais. Um log de hook que nomeia o recurso que criou é o que torna uma regra vazada localizável seis meses depois.