Quando um projeto pessoal sai do controle: Criando seu próprio tun2socks e fechando brechas em VPNs Android
Um desenvolvedor compartilha sua jornada ao descobrir vulnerabilidades em VPNs Android, construindo sua própria solução tun2socks para mitigar riscos. O artigo detalha o processo de desenvolvimento, os desafios enfrentados e as lições aprendidas ao transformar um projeto pessoal em uma ferramenta útil para a comunidade.
MundiX News·12 de maio de 2026·10 min de leitura·👁 4 views
Tudo começou por diversão. Recentemente, surgiu um burburinho na internet sobre uma vulnerabilidade em clientes VLESS: descobriu-se que mesmo ao usar split-tunneling (quando a VPN está ativada apenas para aplicativos selecionados), qualquer aplicativo "espião" no telefone pode descobrir o endereço IP do seu servidor VPN.
A vulnerabilidade era trivial - o kernel do cliente abre um proxy SOCKS local, que não é protegido de forma alguma. Qualquer software no dispositivo pode acessar essa porta local e enviar um pacote para fora. Por interesse acadêmico, escrevi o aplicativo Android TeapodStream, que, por baixo do capô, conectava xray-core e tun2socks. Coloquei o proxy local em uma porta aleatória e o fechei com uma senha dinâmica (escrevi mais sobre isso no artigo anterior, a cerveja, aliás, já perdeu o gás)).
A publicação teve uma grande resposta da comunidade, gerou uma discussão animada e se espalhou pelos favoritos. Achei que meu experimento tinha terminado por aí.
Mas... eu mesmo não percebi como fui envolvido.
A brecha sobre a qual não se fala
Após a publicação do post, outro bug de segurança surgiu nos comentários. Muito mais desagradável, e está presente em quase todos os clientes VPN populares para Android.
O sintoma é o seguinte: um aplicativo espião, que não foi adicionado à lista de tunelamento, simplesmente executa uma chamada de sistema equivalente ao comando curl --interface tun0 https://checkip.amazonaws.com - e o pacote voa com segurança para o seu servidor, revelando seu IP.
Como isso é possível, se o split-tunneling está funcionando?
Vamos dar uma olhada no Android. Ao iniciar a VPN, seu aplicativo, através da API VpnService, cria uma interface de rede tun0. Em seguida, o sistema operacional usa o Policy-Based Routing (roteamento baseado em políticas). Quando um aplicativo normal abre um socket e tenta ir para a rede, o Android olha para seu UID. Se o UID estiver na lista de permissões para VPN, o pacote vai para tun0, se não, vai pela rota padrão (rede celular ou Wi-Fi).
Mas há uma nuance.
tun0 é um dispositivo de rede no nível do kernel do sistema. E o kernel do Linux não se importa com políticas de alto nível e Android UID. Se o aplicativo especificar forçosamente o parâmetro SO_BINDTODEVICE para o socket com o nome da interface tun0, o kernel enviará obedientemente o pacote exatamente para lá, contornando todas as regras do Android.
Antes, esse truque exigia direitos de root. Mas, a partir do kernel Linux versão 5.7, essa chamada foi permitida para aplicativos de usuário comuns. Bingo!
Escrevendo seu próprio tun2socks (fechando brechas do Android?)
É impossível combater essa brecha simplesmente definindo parâmetros de inicialização inteligentes para um tun2socks ou xray-core comum. O pacote já caiu no túnel, ele precisa ser interceptado e filtrado por dentro.
Percebendo isso, tomei uma decisão radical: escrever meu próprio kernel tun2socks do zero em Go, usando gVisor (stack de rede do espaço do usuário).
A lógica de funcionamento do meu mecanismo personalizado: após receber um pacote de tun0, coletamos dele um 5-tuple (protocolo, IP de origem, porta de origem, IP de destino, porta de destino). Em seguida, acessamos a API nativa do Android (ConnectivityManager.getConnectionOwnerUid) e perguntamos: "A quem, de fato, pertence este socket?".
Depois de receber o UID, verificamos com as listas de split-tunneling. Qualquer pacote cujo UID não nos serviu ou que não conseguimos determinar, é impiedosamente descartado.
No nível dos protocolos, isso se parece com:
Com TCP é simples.
Se um pacote SYN (tentativa de abrir uma conexão) cair no túnel, verificamos o UID. Se o aplicativo não puder usar a VPN, enviamos um RST (redefinição de conexão) em resposta. Se puder, envolvemos o tráfego em nosso SOCKS protegido por senha.
Com UDP é muito mais complicado.
Não há conexão como tal, é apenas um fluxo de datagramas (não há pacotes SYN). Temos que reagir a cada pacote. Verificamos o UID e armazenamos em cache o par de portas para não acionar a pesada API do Android em cada datagrama. Além disso, nesta fase, temos que fazer o rebinding de sockets (Strict Source Binding) para que os pacotes UDP não fiquem presos no loop de roteamento dentro do próprio túnel e não vazem.
Com ICMP - uma armadilha completa.
No kernel, determinar de qual aplicativo específico vem o pacote ICMP (o mesmo ping) é tão difícil e intensivo em recursos que simplesmente perde o sentido para um celular. Portanto, foi tomada a decisão de dar ao usuário uma caixa de seleção "Bloquear todo o ICMP no túnel" para evitar problemas.
E funcionou?
A resposta curta: sim. O tun2socks personalizado em Go provou ser muito flexível e impenetrável para SO_BINDTODEVICE.
De um projeto pessoal a um aplicativo completo (dores e alegrias)
Fiquei chocado com o feedback da comunidade. Tantas palavras de agradecimento, tantos desejos e... tantos relatórios de bugs.
Todos os dias, tentei reservar um tempo e refinar o projeto. A funcionalidade cresceu: apareceu o roteamento de tráfego por GeoIP/GeoSite, um bloco nas configurações rápidas, exportação de intents (para automações via Tasker ou Macrodroid), perfis de configurações (para que você possa compartilhar sua configuração com seus entes queridos), suporte Always-On VPN e muito mais.
O drama com o redesenho
Em algum momento, decidi que a interface do aplicativo parecia muito chata e lancei uma atualização de interface elegante (na minha opinião). A reação foi instantânea e implacável. Reclamações choveram nos issues, no tg, por e-mail:
"meus clientes ficaram desconfortáveis", "sou forçado a abandonar seu aplicativo se você não retornar ao design antigo" e tudo mais.
Nesse momento, percebi: o que eu estava construindo "por diversão para a alma", as pessoas já estão usando em todos os lugares, inclusive em negócios comerciais para seus clientes. Em alguns lugares, tive que procurar compromissos.
A batalha pela estabilidade
Mas a maior dor, o trabalho no qual ainda está em andamento, é a estabilidade da conexão VPN. Android é um ambiente severo. É necessário processar corretamente as mudanças de rede (Wi-Fi <-> LTE), a entrada do telefone em sono profundo (modo Doze) e despertar, iniciar e parar o túnel da cortina ou automações (quando a interface do aplicativo não é carregada na memória), suporte para heartbeats e uma série de outras nuances.
Eu lembro: TeapodStream não é um projeto comercial. Eu o faço no meu tempo livre. Seu código é aberto, não pretendo introduzir monetização e não coleto doações. Esta é apenas a minha maneira de exercitar meu cérebro, me divertir e (espero) tornar a internet um pouco mais livre e segura.
Fontes
As fontes do TeapodStream e do meu tun2socks personalizado estão no GitHub. O aplicativo tem muitos pequenos bugs e é mais uma versão alfa/beta. O que você acha, em que direção desenvolver o projeto ainda mais?
Para todos, o bem, e obrigado por ler!
🛡️⚡
Pare de pesquisar. Comece a hackear.
O MundiX é seu copiloto de pentest com IA: comandos exatos, análise de outputs e próximo passo na kill chain — em segundos.
Sem cartão para começar · Planos a partir de R$49/mês
Tudo começou por diversão. Recentemente, surgiu um burburinho na internet sobre uma vulnerabilidade em clientes VLESS: descobriu-se que mesmo ao usar split-tunneling (quando a VPN está ativada apenas para aplicativos selecionados), qualquer aplicativo "espião" no telefone pode descobrir o endereço IP do seu servidor VPN.
A vulnerabilidade era trivial - o kernel do cliente abre um proxy SOCKS local, que não é protegido de forma alguma. Qualquer software no dispositivo pode acessar essa porta local e enviar um pacote para fora. Por interesse acadêmico, escrevi o aplicativo Android TeapodStream, que, por baixo do capô, conectava xray-core e tun2socks. Coloquei o proxy local em uma porta aleatória e o fechei com uma senha dinâmica (escrevi mais sobre isso no artigo anterior, a cerveja, aliás, já perdeu o gás)).
A publicação teve uma grande resposta da comunidade, gerou uma discussão animada e se espalhou pelos favoritos. Achei que meu experimento tinha terminado por aí.
Mas... eu mesmo não percebi como fui envolvido.
A brecha sobre a qual não se fala
Após a publicação do post, outro bug de segurança surgiu nos comentários. Muito mais desagradável, e está presente em quase todos os clientes VPN populares para Android.
O sintoma é o seguinte: um aplicativo espião, que não foi adicionado à lista de tunelamento, simplesmente executa uma chamada de sistema equivalente ao comando curl --interface tun0 https://checkip.amazonaws.com - e o pacote voa com segurança para o seu servidor, revelando seu IP.
Como isso é possível, se o split-tunneling está funcionando?
Vamos dar uma olhada no Android. Ao iniciar a VPN, seu aplicativo, através da API VpnService, cria uma interface de rede tun0. Em seguida, o sistema operacional usa o Policy-Based Routing (roteamento baseado em políticas). Quando um aplicativo normal abre um socket e tenta ir para a rede, o Android olha para seu UID. Se o UID estiver na lista de permissões para VPN, o pacote vai para tun0, se não, vai pela rota padrão (rede celular ou Wi-Fi).
Mas há uma nuance.
tun0 é um dispositivo de rede no nível do kernel do sistema. E o kernel do Linux não se importa com políticas de alto nível e Android UID. Se o aplicativo especificar forçosamente o parâmetro SO_BINDTODEVICE para o socket com o nome da interface tun0, o kernel enviará obedientemente o pacote exatamente para lá, contornando todas as regras do Android.
Antes, esse truque exigia direitos de root. Mas, a partir do kernel Linux versão 5.7, essa chamada foi permitida para aplicativos de usuário comuns. Bingo!
Escrevendo seu próprio tun2socks (fechando brechas do Android?)
É impossível combater essa brecha simplesmente definindo parâmetros de inicialização inteligentes para um tun2socks ou xray-core comum. O pacote já caiu no túnel, ele precisa ser interceptado e filtrado por dentro.
Percebendo isso, tomei uma decisão radical: escrever meu próprio kernel tun2socks do zero em Go, usando gVisor (stack de rede do espaço do usuário).
A lógica de funcionamento do meu mecanismo personalizado: após receber um pacote de tun0, coletamos dele um 5-tuple (protocolo, IP de origem, porta de origem, IP de destino, porta de destino). Em seguida, acessamos a API nativa do Android (ConnectivityManager.getConnectionOwnerUid) e perguntamos: "A quem, de fato, pertence este socket?".
Depois de receber o UID, verificamos com as listas de split-tunneling. Qualquer pacote cujo UID não nos serviu ou que não conseguimos determinar, é impiedosamente descartado.
No nível dos protocolos, isso se parece com:
Com TCP é simples.
Se um pacote SYN (tentativa de abrir uma conexão) cair no túnel, verificamos o UID. Se o aplicativo não puder usar a VPN, enviamos um RST (redefinição de conexão) em resposta. Se puder, envolvemos o tráfego em nosso SOCKS protegido por senha.
Com UDP é muito mais complicado.
Não há conexão como tal, é apenas um fluxo de datagramas (não há pacotes SYN). Temos que reagir a cada pacote. Verificamos o UID e armazenamos em cache o par de portas para não acionar a pesada API do Android em cada datagrama. Além disso, nesta fase, temos que fazer o rebinding de sockets (Strict Source Binding) para que os pacotes UDP não fiquem presos no loop de roteamento dentro do próprio túnel e não vazem.
Com ICMP - uma armadilha completa.
No kernel, determinar de qual aplicativo específico vem o pacote ICMP (o mesmo ping) é tão difícil e intensivo em recursos que simplesmente perde o sentido para um celular. Portanto, foi tomada a decisão de dar ao usuário uma caixa de seleção "Bloquear todo o ICMP no túnel" para evitar problemas.
E funcionou?
A resposta curta: sim. O tun2socks personalizado em Go provou ser muito flexível e impenetrável para SO_BINDTODEVICE.
De um projeto pessoal a um aplicativo completo (dores e alegrias)
Fiquei chocado com o feedback da comunidade. Tantas palavras de agradecimento, tantos desejos e... tantos relatórios de bugs.
Todos os dias, tentei reservar um tempo e refinar o projeto. A funcionalidade cresceu: apareceu o roteamento de tráfego por GeoIP/GeoSite, um bloco nas configurações rápidas, exportação de intents (para automações via Tasker ou Macrodroid), perfis de configurações (para que você possa compartilhar sua configuração com seus entes queridos), suporte Always-On VPN e muito mais.
O drama com o redesenho
Em algum momento, decidi que a interface do aplicativo parecia muito chata e lancei uma atualização de interface elegante (na minha opinião). A reação foi instantânea e implacável. Reclamações choveram nos issues, no tg, por e-mail:
"meus clientes ficaram desconfortáveis", "sou forçado a abandonar seu aplicativo se você não retornar ao design antigo" e tudo mais.
Nesse momento, percebi: o que eu estava construindo "por diversão para a alma", as pessoas já estão usando em todos os lugares, inclusive em negócios comerciais para seus clientes. Em alguns lugares, tive que procurar compromissos.
A batalha pela estabilidade
Mas a maior dor, o trabalho no qual ainda está em andamento, é a estabilidade da conexão VPN. Android é um ambiente severo. É necessário processar corretamente as mudanças de rede (Wi-Fi <-> LTE), a entrada do telefone em sono profundo (modo Doze) e despertar, iniciar e parar o túnel da cortina ou automações (quando a interface do aplicativo não é carregada na memória), suporte para heartbeats e uma série de outras nuances.
Eu lembro: TeapodStream não é um projeto comercial. Eu o faço no meu tempo livre. Seu código é aberto, não pretendo introduzir monetização e não coleto doações. Esta é apenas a minha maneira de exercitar meu cérebro, me divertir e (espero) tornar a internet um pouco mais livre e segura.
Fontes
As fontes do TeapodStream e do meu tun2socks personalizado estão no GitHub. O aplicativo tem muitos pequenos bugs e é mais uma versão alfa/beta. O que você acha, em que direção desenvolver o projeto ainda mais?
Para todos, o bem, e obrigado por ler!
📤 Compartilhar & Baixar
🧰 Ferramentas recomendadas
Divulgação: alguns links são patrocinados. Podemos receber comissão se você comprar — sem custo extra para você. Só indicamos o que faz sentido para a comunidade.