Configurando OpenWrt #3: Policy Based Routing (PBR)

No último post, preparamos a integração da ProtonVPN com o OpenWrt para que seja possível passar todo o tráfego da sua rede local através da VPN.

Porém, como já dito, isso não é o ideal. O melhor dos mundos é rotear com base em certas condições, como domínios específicos, portas, etc., porque ainda existem sites cujo tráfego gostaríamos que tivesse como origem o nosso IP real (sites com CDN, speedtests, entre outros). E é exatamente isso que o pacote PBR (Policy Based Routing) se propõe a fazer. O funcionamento é parecido com o da imagem a seguir: diagram

Nota: Este post parte do pressuposto de que você configurou a interface WireGuard como mostrado na parte #2.

Escolha dos domínios

Ok, mas afinal, qual seria o caso de uso disso? Quais domínios eu usaria?

Isso depende muito do seu uso de internet.

Por exemplo, com o advento da Lei Felca (ECA Digital) no Brasil e similares em outros países, alguns sites podem pedir verificação de identidade para acessá-los. Caso sejam sites que você não gostaria de fazer essa verificação, você pode usar esse domínio no PBR para que ele identifique você como usuário de outro país.

Outro exemplo são os sistemas P2P. Você pode querer rotear todo o tráfego de sites P2P para a VPN. Mas não vou entrar em mais detalhes.

Basicamente, isso vai ser útil para qualquer site que você queira passar pela VPN por toda a rede sem precisar usar os clientes de VPN em cada dispositivo.

Pré-configuração

De início, precisamos de alguns pacotes, entre eles o dnsmasq-full. Normalmente, o OpenWrt vem somente com o pacote padrão do dnsmasq, então precisamos excluí-lo para instalar a variante completa. Esse pacote é importante porque oferece suporte a nft sets, permitindo que o PBR crie condições baseadas em domínios.

apk update
apk del dnsmasq
apk add dnsmasq-full pbr luci-app-pbr

Como sempre, vamos criar alguns backups primeiro:

TS="$(date +%Y%m%d-%H%M%S)"
[ -f /etc/config/pbr ] && cp /etc/config/pbr "/etc/config/pbr.backup.$TS"
[ -f /etc/config/firewall ] && cp /etc/config/firewall "/etc/config/firewall.backup.$TS"
[ -f /etc/config/dhcp ] && cp /etc/config/dhcp "/etc/config/dhcp.backup.$TS"

Repetindo uma informação importante do post anterior: quem decide o que vai sair pela VPN será o PBR, então a interface WireGuard não deve usar a rota padrão automaticamente. Certifique-se de que a opção abaixo esteja definida como falsa:

uci set network.proton0.route_allowed_ips='0'
uci commit network
/etc/init.d/network reload

Configuração

Para dar início à configuração do PBR, vamos ativá-lo com o comando abaixo:

uci set pbr.config.enabled='1'

Como instalamos o dnsmasq-full, podemos configurar o PBR para usar o suporte do dnsmasq a nft sets. Esse é principal pacote para usar domínios como condições para o PBR.

uci set pbr.config.resolver_set='dnsmasq.nftset'

Agora, temos alguns detalhes importantes a decidir.

Uma das opções relevantes que podemos escolher é bloquear os domínios caso a VPN não esteja funcionando. Claro, isso é uma escolha pessoal a depender dos domínios utilizados, porém eu considero importante para evitar qualquer tipo de vazamento. Na minha experiência de uso (ao menos com a ProtonVPN) é muito raro que a VPN deixe de funcionar, às vezes acontece de ficar um ou dois segundos fora, mas rapidamente volta já que a conexão sempre é restabelecida automaticamente. Isso é configurado pela opção strict_enforcement:

uci set pbr.config.strict_enforcement="1"

Outra opção importante é ativar o IPv6 no PBR. Quando eu estava usando o PBR pela primeira vez, eu havia esquecido de ativar e muita coisa não funcionava. Demorei para lembrar que era devido ao IPv6. Então, recomendo ativar.

uci set pbr.config.ipv6_enabled="1"
Recomendo ativar a opção de IPv6 somente se a interface WireGuard tiver um IPv6 atribuído para não ocorrer nenhum vazamento.

Com o PBR configurado, precisamos criar as condições/policies. Aqui o importante é definir os domínios. Vamos usar o serviço ifconfig.me como teste para o post e aplicar as configurações. Caso seja mais de um domínio, basta separar por espaço. Basicamente, o bloco abaixo define que todo o tráfego destinado aos domínios definidos em dest_addr deve ir para a interface proton0.

uci add pbr policy
uci set pbr.@policy[-1].name="Selected domains via Proton"
uci set pbr.@policy[-1].interface="proton0"
uci set pbr.@policy[-1].dest_addr="ifconfig.me"
uci commit pbr
/etc/init.d/pbr restart

A partir daqui já deve estar tudo funcionando corretamente.

Testes

Como utilizamos somente o ifconfig.me para passar pelo túnel, vamos fazer requisições a ele para verificar se tanto o IPv4 quanto o IPv6 estão com IPs do país escolhido na ProtonVPN.

curl -4 ifconfig.me
curl -6 ifconfig.me

Compare o resultado com o valor atual do seu provedor. Como exemplo, utilize o icanhazip.com:

curl -4 icanhazip.com
curl -6 icanhazip.com

Se os resultados forem diferentes, está funcionando corretamente, já que um passa pela VPN e outro não. Para ter certeza, você pode jogar o IP retornado pelo ifconfig.me no ipinfo.io para ver se o IP é do país escolhido.

Conclusão

Tudo o que foi mostrado aqui eu utilizo na minha rede local diariamente. Tem me ajudado a manter um certo grau de opsec em alguns sites que frequento.

Abaixo, deixo um script completo que uso para configurar o PBR. Sempre que adiciono um domínio novo, eu executo o script para sincronizar com a configuração no OpenWrt.

./setup-pbr.sh
#!/bin/sh
set -e

# ---------- EDIT THESE ----------
VPN_IFACE="proton0"   # your existing OpenWrt WireGuard interface name
POLICY_NAME="Selected domains via Proton"
DOMAINS="ifconfig.me icanhazip.com test-ipv6.com" # domínios de exemplo, use os de sua escolha

# 1 = block these domains if VPN is down; 0 = allow fallback
STRICT_ENFORCEMENT="1"

# 0 unless ProtonVPN interface has working IPv6
IPV6_ENABLED="0"

# Set to 1 only if LAN -> VPN firewall forwarding is not already configured
CREATE_FIREWALL_ZONE="0"
VPN_ZONE_NAME="protonvpn"
LAN_ZONE_NAME="lan"
# -------------------------------

echo ">>> Checking interface: $VPN_IFACE"
uci -q get network."$VPN_IFACE" >/dev/null || {
  echo "ERROR: network.$VPN_IFACE does not exist. Set VPN_IFACE to your WireGuard interface name."
  exit 1
}

echo ">>> Backing up configs"
TS="$(date +%Y%m%d-%H%M%S)"
[ -f /etc/config/pbr ] && cp /etc/config/pbr "/etc/config/pbr.backup.$TS"
[ -f /etc/config/firewall ] && cp /etc/config/firewall "/etc/config/firewall.backup.$TS"
[ -f /etc/config/dhcp ] && cp /etc/config/dhcp "/etc/config/dhcp.backup.$TS"

echo ">>> Installing pbr and dnsmasq-full"
apk update

if ! apk info -e dnsmasq-full >/dev/null 2>&1; then
  apk del dnsmasq 2>/dev/null || true
  apk add dnsmasq-full
fi

apk add pbr luci-app-pbr

if [ "$CREATE_FIREWALL_ZONE" = "1" ]; then
  echo ">>> Creating firewall zone/forwarding for $VPN_IFACE"

  ZONE_EXISTS="0"
  i=0
  while name="$(uci -q get firewall.@zone[$i].name)"; do
    [ "$name" = "$VPN_ZONE_NAME" ] && ZONE_EXISTS="1"
    i=$((i + 1))
  done

  if [ "$ZONE_EXISTS" = "0" ]; then
    uci add firewall zone
    uci set firewall.@zone[-1].name="$VPN_ZONE_NAME"
    uci set firewall.@zone[-1].input='REJECT'
    uci set firewall.@zone[-1].output='ACCEPT'
    uci set firewall.@zone[-1].forward='REJECT'
    uci add_list firewall.@zone[-1].network="$VPN_IFACE"
    uci set firewall.@zone[-1].masq='1'
    uci set firewall.@zone[-1].mtu_fix='1'
  fi

  FWD_EXISTS="0"
  i=0
  while src="$(uci -q get firewall.@forwarding[$i].src)"; do
    dest="$(uci -q get firewall.@forwarding[$i].dest)"
    [ "$src" = "$LAN_ZONE_NAME" ] && [ "$dest" = "$VPN_ZONE_NAME" ] && FWD_EXISTS="1"
    i=$((i + 1))
  done

  if [ "$FWD_EXISTS" = "0" ]; then
    uci add firewall forwarding
    uci set firewall.@forwarding[-1].src="$LAN_ZONE_NAME"
    uci set firewall.@forwarding[-1].dest="$VPN_ZONE_NAME"
  fi

  uci commit firewall
fi

echo ">>> Configuring pbr with dnsmasq.nftset"
uci set pbr.config.enabled='1'
uci set pbr.config.resolver_set='dnsmasq.nftset'
uci set pbr.config.strict_enforcement="$STRICT_ENFORCEMENT"
uci set pbr.config.ipv6_enabled="$IPV6_ENABLED"

echo ">>> Removing existing policy with same name, if present"
i=0
while name="$(uci -q get pbr.@policy[$i].name)"; do
  if [ "$name" = "$POLICY_NAME" ]; then
    uci delete pbr.@policy[$i]
  else
    i=$((i + 1))
  fi
done

echo ">>> Adding policy: $POLICY_NAME"
uci add pbr policy
uci set pbr.@policy[-1].name="$POLICY_NAME"
uci set pbr.@policy[-1].interface="$VPN_IFACE"
uci set pbr.@policy[-1].dest_addr="$DOMAINS"

uci commit pbr

echo ">>> Applying changes"
uci set network."$VPN_IFACE".route_allowed_ips='0'
uci commit network

/etc/init.d/network reload
ifup "$VPN_IFACE" || true
/etc/init.d/firewall reload
/etc/init.d/pbr restart

echo ">>> Done"
echo "Run:"
echo "  pbr status"
echo "  nft list set inet fw4 pbr_${VPN_IFACE}_4_dst_ip"