Fixar um app Linux a uma VPN com regras UID
Uma regra UID só vê sockets locais, e a exceção habitual de loopback mata o túnel. Encaminhe primeiro a conta e depois filtre e verifique cada passo.
Fixar um único programa a uma VPN no Linux é sobretudo um problema de roteamento, e a regra do firewall decide apenas o que acontece depois de a rota ser escolhida. Se você casar o UID do programa no OUTPUT antes de o túnel possuir a rota padrão, a regra descarta exatamente o tráfego que você queria proteger. A mesma regra também não vê o processo do resolver que responde às consultas DNS do programa. Acerte a ordem e uma única conta sem privilégios se torna um seletor confiável do túnel dividido.
Dê uma rota à conta antes do veredito
A correspondência de proprietário responde a uma pergunta: um socket local pertencente a este UID construiu o pacote? Ela não escolhe interface e não pode mover o tráfego que a tabela principal já envia pela NIC física. Por isso uma regra UID sem rota correspondente bloqueia o programa em vez de redirecioná-lo.
Envie a conta primeiro pela sua própria tabela e depois deixe o firewall confirmar o resultado. Isso funciona num host cujo iproute2 entende uidrange, o que ip rule help informa numa linha. A tabela 100 contém uma rota padrão pelo túnel, e a regra envia apenas o UID 1000 para essa tabela. Todo o resto fica na tabela principal e continua a usar o link físico.
ip rule add uidrange 1000-1000 table 100 ip route add default dev tun0 table 100
Uma segunda conta que precisa de outro túnel recebe a tabela 101 e a sua própria regra, porque dois perfis não podem ambos possuir a rota padrão na mesma tabela. Os perfis do NetworkManager são de todo o sistema, então esta parte continua manual, e as regras desaparecem ao reiniciar a menos que algo as restaure. Escreva as tabelas num script de arranque ao lado da definição do túnel para que uma reconexão não possa deixar uma conta no caminho errado.
Porque é que a exceção de loopback mata o túnel
A maioria das versões copiadas da regra descarta cada pacote que não sai por loopback. O túnel não é loopback, portanto essa forma também descarta o tráfego do túnel, e o resultado parece uma VPN morta e não um erro de firewall.
Quando o OUTPUT corre, o kernel já escolheu uma interface de saída, por isso -o é comparado com a resposta da rota e não com o cabeçalho do pacote. A correspondência de proprietário só é consultada em OUTPUT e POSTROUTING, e os pacotes encaminhados não transportam nenhum socket para ela ler. Essa combinação significa que a exceção tem de nomear a interface do túnel e negar essa correspondência, em vez de negar loopback.
O loopback ainda merece o seu próprio ACCEPT acima do DROP, porque o primeiro pacote DNS do programa vai para o stub local através de lo. Coloque as duas regras nesta ordem e a conta chega ao stub, depois ao túnel, e a mais nada.
iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP
IPv6 é um conjunto de regras separado, e um par só para v4 deixa ip6tables intacto. Um programa que prefira registos AAAA sairia então pelo link físico sem nunca chegar ao DROP. Espelhe ambas as regras em ip6tables, ou desative IPv6 no host quando o túnel não transportar nenhum endereço v6. As regras adicionadas à mão também desaparecem ao reiniciar, por isso guarde-as com netfilter-persistent save ou reconstrua a mesma lógica como uma tabela nftables carregada pela unidade de arranque.
As respostas DNS vêm de outro processo
A consulta do programa a 127.0.0.53 passa o ACCEPT de loopback, e é aí que a correspondência UID para. O resolver stub reencaminha para cima a partir da sua própria conta de serviço, por isso uma regra ligada a vpnapp nunca vê a segunda consulta. Se o resolver escolher um servidor LAN para o link, as respostas voltam pela NIC física enquanto os pacotes do próprio programa ficam no túnel. O resultado visível é um túnel a funcionar com nomes quebrados, ou nomes respondidos por um servidor que você não escolheu.
Corrija a resolução de nomes no link do túnel, não na aplicação. Dê ao perfil do túnel um servidor DNS e um domínio de encaminhamento, e depois confirme com resolvectl query e resolvectl status tun0 que o resolver o usa para os nomes que importam. Dentro de um espaço de nomes de rede o problema desaparece, porque o resolver do espaço só conhece o túnel.
Verifique a regra a partir da própria conta
Pergunte ao kernel que rota recebe um UID, faça depois um pedido como essa conta e compare o endereço de saída com o da sua própria conta. Se ambos reportarem o mesmo endereço, a regra está a casar algo diferente do tráfego que testou. O primeiro comando abaixo mostra a pesquisa de rota sem enviar um pacote, o que separa um erro de encaminhamento de um erro de firewall.
ip route get 1.1.1.1 uid 1000 sudo -u vpnapp curl -s https://ifconfig.co
Os contadores mostram se uma regra alguma vez disparou. Execute iptables -vnL OUTPUT, leia a contagem de pacotes ao lado de cada regra e trate um zero ao lado do DROP depois de tráfego real como um erro de correspondência.
O tráfego encaminhado fica fora deste desenho. Um contentor ou VM atrás de uma ponte não tem socket local pertencente a vpnapp, por isso a correspondência de proprietário nunca dispara e esses pacotes seguem antes a política FORWARD. Se essa carga tiver de ser coberta, teste-a em separado e escreva uma política ligada à ponte ou ao endereço do contentor. O desenho mantém-se quando a tabela, a exceção de interface, o resolver e as regras v6 apontam todos na mesma direção; deixe de fora qualquer um deles e a conta ou fica exposta ou perde a conectividade.