Marcar o tráfego de um cgroup é a parte fácil. Três decisões separadas precisam estar alinhadas antes que uma única aplicação saia pela interface física enquanto todo o resto fica no túnel: qual socket pertence à exceção, qual consulta de rota a marca dispara e qual endereço de origem o pacote carrega quando sai. A maioria das tentativas fracassadas acerta a primeira, acerta pela metade a segunda e nunca olha para a terceira.

Três decisões, não uma

A classificação responde quem está isento. O encaminhamento responde para onde o pacote marcado vai em vez disso. A seleção de origem responde o que a outra ponta verá e para onde a resposta deve voltar.

Essas coisas falham de forma independente. Um pacote pode ser classificado, reencaminhado para a interface física e ainda carregar um endereço privado que nenhum roteador a montante consegue devolver, o que produz um timeout que parece exatamente um bloqueio de firewall. Nada na etapa de classificação avisa sobre isso. Nada na etapa de encaminhamento também.

A ordem também importa. A classificação acontece quando o pacote é gerado, o encaminhamento acontece depois, e a tradução acontece no ponto de saída.

Se você não consegue nomear qual das três etapas está quebrada, você não está depurando, está reorganizando regras e torcendo.

Combine um cgroup que ainda exista amanhã

O nftables consegue testar a associação de um socket ao cgroup v2, o que é realmente útil porque segue o processo e não o destino. A pegadinha é o identificador. Um aplicativo de desktop iniciado por um menu costuma viver em um scope transitório cujo nome muda no próximo início.

nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup

Copiar o nome de um scope de uma sessão e codificá-lo fixamente produz uma regra que funciona hoje e silenciosamente deixa de casar amanhã. Coloque o aplicativo em um contexto de execução estável e deliberado, como um scope ou unit dedicado que você controla, e combine com ele. A regra então sobrevive a reinicializações e atualizações de pacotes. Também se torna algo sobre o qual um colega consegue raciocinar sem o seu histórico de sessão.

Também seja preciso sobre qual mecanismo você está usando. A classificação net_cls do cgroup v1 e a combinação de sockets do cgroup v2 são coisas diferentes, e um campo de nome parecido na documentação não as torna intercambiáveis.

É a cadeia de route que dispara a segunda consulta

Para tráfego gerado localmente, o tipo de cadeia decide se algo acontece depois que a marca é definida. Uma cadeia output do tipo filter pode marcar um pacote e o kernel continua usando a rota que já escolheu. Uma cadeia do tipo route é o mecanismo que pede uma nova consulta.

table inet bypass {
  chain mark {
    type route hook output priority mangle; policy accept;
    socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
  }
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200

A marca então precisa de uma regra de política que a envie para uma tabela, e essa tabela precisa estar completa: uma rota padrão, os destinos locais de que o aplicativo ainda precisa e um gateway a montante que realmente responda.

Escolha o valor da marca deliberadamente. O WireGuard usa seu próprio fwmark para o tráfego de transporte externo, e cair no mesmo número por acidente pode interferir na exceção de rota do próprio túnel de formas dolorosas de diagnosticar.

O endereço de origem não segue a rota

Um socket que já escolheu um endereço de origem para o caminho do túnel mantém esse endereço depois do reencaminhamento. Nada na mudança de rota o reescreve, e é por isso que o pacote sai pela interface física com uma origem privada que a rede a montante não consegue devolver.

O sintoma visível é um aplicativo que parece enviar dados e nunca recebe resposta, e um log de firewall que não mostra nada porque o pacote nunca voltou.

A correção estreita é uma regra de tradução de origem limitada à marca daquele aplicativo e à sua interface de saída real, integrada ao NAT que já existe em vez de substituí-lo:

table inet bypass {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    meta mark 0x1 oifname "wlan0" masquerade
  }
}

Traduza a exceção e nada mais. Uma regra masquerade abrangente corrige o sintoma e destrói sua capacidade de dizer qual tráfego tomou qual caminho. Combine com a marca, a interface de saída real e, se puder, o endereço do próprio aplicativo. Regras amplas são fáceis de escrever e quase impossíveis de auditar depois.

O que ainda escapa da exceção

A resolução de nomes pode vir de um processo resolver separado em outro cgroup, então classificar o socket do navegador não classifica suas consultas. Se as consultas do resolver ficam no túnel enquanto a conexão sai fora dele, respostas split-horizon podem não casar com o caminho. Essa incompatibilidade produz falhas que parecem bugs de DNS e não são.

Processos filhos herdam o cgroup na maioria das configurações, mas um auxiliar que se move para o próprio scope não. Verifique em vez de presumir, especialmente para qualquer coisa que se reexecute.

Por fim, decida o que deve acontecer quando o túnel está fora do ar. Uma exceção que silenciosamente se torna um bypass completo é uma postura de segurança diferente de uma que falha fechada, e apenas uma delas é o que você quis dizer.

Artigos relacionados