Alimente todo o tráfego através do OpenVPN apenas para um namespace de rede específico

16

Estou tentando configurar uma VPN (usando OpenVPN), de modo que todo o tráfego, e somente o tráfego, de / para processos específicos passe pela VPN; outros processos devem continuar usando o dispositivo físico diretamente. Entendo que a maneira de fazer isso no Linux é com namespaces de rede.

Se eu usar o OpenVPN normalmente (ou seja, canalizar todo o tráfego do cliente através da VPN), ele funcionará bem. Especificamente, inicio o OpenVPN assim:

# openvpn --config destination.ovpn --auth-user-pass credentials.txt

(Uma versão editada de destination.ovpn está no final desta pergunta.)

Estou preso na próxima etapa, escrevendo scripts que restringem o dispositivo de túnel a namespaces. Eu tentei:

  1. Colocando o dispositivo de encapsulamento diretamente no espaço para nome com

    # ip netns add tns0
    # ip link set dev tun0 netns tns0
    # ip netns exec tns0 ( ... commands to bring up tun0 as usual ... )
    

    Esses comandos são executados com sucesso, mas o tráfego gerado dentro do espaço para nome (por exemplo, com ip netns exec tns0 traceroute -n 8.8.8.8) cai em um buraco negro.

  2. Supondo que " você ainda [pode] atribuir apenas interfaces Ethernet (veth) virtuais a um espaço para nome de rede " (que, se verdadeiro, recebe o prêmio deste ano pela restrição de API mais ridiculamente desnecessária), criando um par e uma ponte de veth e colocando uma extremidade do par veth no espaço para nome. Isso nem chega a diminuir o tráfego no chão: não me deixa colocar o túnel na ponte! [EDIT: Parece que apenas os dispositivos de toque podem ser colocados em pontes. Diferentemente da incapacidade de colocar dispositivos arbitrários em um espaço para nome de rede, isso realmente faz sentido, o que significa pontes como um conceito de camada Ethernet; infelizmente, meu provedor de VPN não oferece suporte ao OpenVPN no modo de toque, por isso preciso de uma solução alternativa.]

    # ip addr add dev tun0 local 0.0.0.0/0 scope link
    # ip link set tun0 up
    # ip link add name teo0 type veth peer name tei0
    # ip link set teo0 up
    # brctl addbr tbr0
    # brctl addif tbr0 teo0
    # brctl addif tbr0 tun0
    can't add tun0 to bridge tbr0: Invalid argument
    

Os scripts no final desta pergunta são para a abordagem veth. Os scripts para a abordagem direta podem ser encontrados no histórico de edições. Variáveis ​​nos scripts que parecem ser usadas sem defini-las primeiro são definidas no ambiente pelo openvpnprograma - sim, é desleixado e usa nomes em minúsculas.

Por favor, ofereça conselhos específicos sobre como fazer isso funcionar. Estou dolorosamente ciente de que estou programando por cult de carga aqui - alguém escreveu uma documentação abrangente para essas coisas? Não consigo encontrar nenhum - portanto, a revisão geral do código dos scripts também é apreciada.

Caso isso importe:

# uname -srvm
Linux 3.14.5-x86_64-linode42 #1 SMP Thu Jun 5 15:22:13 EDT 2014 x86_64
# openvpn --version | head -1
OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Mar 17 2014
# ip -V
ip utility, iproute2-ss140804
# brctl --version
bridge-utils, 1.5

O kernel foi construído pelo meu provedor de hospedagem virtual ( Linode ) e, embora compilado CONFIG_MODULES=y, não possui módulos reais - a única CONFIG_*variável definida mcomo /proc/config.gzera CONFIG_XEN_TMEM, e eu realmente não tenho esse módulo (o kernel é armazenado fora do meu sistema de arquivos; /lib/modulesestá vazio e /proc/modulesindica que não foi carregado magicamente de alguma forma). Trechos /proc/config.gzfornecidos a pedido, mas não quero colar a coisa toda aqui.

netns-up.sh

#! /bin/sh

mask2cidr () {
    local nbits dec
    nbits=0
    for dec in $(echo $1 | sed 's/\./ /g') ; do
        case "$dec" in
            (255) nbits=$(($nbits + 8)) ;;
            (254) nbits=$(($nbits + 7)) ;;
            (252) nbits=$(($nbits + 6)) ;;
            (248) nbits=$(($nbits + 5)) ;;
            (240) nbits=$(($nbits + 4)) ;;
            (224) nbits=$(($nbits + 3)) ;;
            (192) nbits=$(($nbits + 2)) ;;
            (128) nbits=$(($nbits + 1)) ;;
            (0)   ;;
            (*) echo "Error: $dec is not a valid netmask component" >&2
                exit 1
                ;;
        esac
    done
    echo "$nbits"
}

mask2network () {
    local host mask h m result
    host="$1."
    mask="$2."
    result=""
    while [ -n "$host" ]; do
        h="${host%%.*}"
        m="${mask%%.*}"
        host="${host#*.}"
        mask="${mask#*.}"
        result="$result.$(($h & $m))"
    done
    echo "${result#.}"
}

maybe_config_dns () {
    local n option servers
    n=1
    servers=""
    while [ $n -lt 100 ]; do
       eval option="\$foreign_option_$n"
       [ -n "$option" ] || break
       case "$option" in
           (*DNS*)
               set -- $option
               servers="$servers
nameserver $3"
               ;;
           (*) ;;
       esac
       n=$(($n + 1))
    done
    if [ -n "$servers" ]; then
        cat > /etc/netns/$tun_netns/resolv.conf <<EOF
# name servers for $tun_netns
$servers
EOF
    fi
}

config_inside_netns () {
    local ifconfig_cidr ifconfig_network

    ifconfig_cidr=$(mask2cidr $ifconfig_netmask)
    ifconfig_network=$(mask2network $ifconfig_local $ifconfig_netmask)

    ip link set dev lo up

    ip addr add dev $tun_vethI \
        local $ifconfig_local/$ifconfig_cidr \
        broadcast $ifconfig_broadcast \
        scope link
    ip route add default via $route_vpn_gateway dev $tun_vethI
    ip link set dev $tun_vethI mtu $tun_mtu up
}

PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH

set -ex

# For no good reason, we can't just put the tunnel device in the
# subsidiary namespace; we have to create a "virtual Ethernet"
# device pair, put one of its ends in the subsidiary namespace,
# and put the other end in a "bridge" with the tunnel device.

tun_tundv=$dev
tun_netns=tns${dev#tun}
tun_bridg=tbr${dev#tun}
tun_vethI=tei${dev#tun}
tun_vethO=teo${dev#tun}

case "$tun_netns" in
     (tns[0-9] | tns[0-9][0-9] | tns[0-9][0-9][0-9]) ;;
     (*) exit 1;;
esac

if [ $# -eq 1 ] && [ $1 = "INSIDE_NETNS" ]; then
    [ $(ip netns identify $$) = $tun_netns ] || exit 1
    config_inside_netns
else

    trap "rm -rf /etc/netns/$tun_netns ||:
          ip netns del $tun_netns      ||:
          ip link del $tun_vethO       ||:
          ip link set $tun_tundv down  ||:
          brctl delbr $tun_bridg       ||:
         " 0

    mkdir /etc/netns/$tun_netns
    maybe_config_dns

    ip addr add dev $tun_tundv local 0.0.0.0/0 scope link
    ip link set $tun_tundv mtu $tun_mtu up

    ip link add name $tun_vethO type veth peer name $tun_vethI
    ip link set $tun_vethO mtu $tun_mtu up

    brctl addbr $tun_bridg
    brctl setfd $tun_bridg 0
    #brctl sethello $tun_bridg 0
    brctl stp $tun_bridg off

    brctl addif $tun_bridg $tun_vethO
    brctl addif $tun_bridg $tun_tundv
    ip link set $tun_bridg up

    ip netns add $tun_netns
    ip link set dev $tun_vethI netns $tun_netns
    ip netns exec $tun_netns $0 INSIDE_NETNS

    trap "" 0
fi

netns-down.sh

#! /bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH

set -ex

tun_netns=tns${dev#tun}
tun_bridg=tbr${dev#tun}

case "$tun_netns" in
     (tns[0-9] | tns[0-9][0-9] | tns[0-9][0-9][0-9]) ;;
     (*) exit 1;;
esac

[ -d /etc/netns/$tun_netns ] || exit 1

pids=$(ip netns pids $tun_netns)
if [ -n "$pids" ]; then
    kill $pids
    sleep 5
    pids=$(ip netns pids $tun_netns)
    if [ -n "$pids" ]; then
        kill -9 $pids
    fi
fi

# this automatically cleans up the the routes and the veth device pair
ip netns delete "$tun_netns"
rm -rf /etc/netns/$tun_netns

# the bridge and the tunnel device must be torn down separately
ip link set $dev down
brctl delbr $tun_bridg

destination.ovpn

client
auth-user-pass
ping 5
dev tun
resolv-retry infinite
nobind
persist-key
persist-tun
ns-cert-type server
verb 3
route-metric 1
proto tcp
ping-exit 90
remote [REDACTED]
<ca>
[REDACTED]
</ca>
<cert>
[REDACTED]
</cert>
<key>
[REDACTED]
</key>
zwol
fonte
Vamos começar com o óbvio: os dispositivos veth são suportados? os módulos do kernel (veth) estão carregados?
countermode
O @countermode grep veth /proc/modulesnão lista nada, mas não sei se isso é conclusivo. As instâncias do Linode não têm um kernel instalado dentro da partição do SO, portanto, não tenho certeza se poderia carregar um módulo ausente de qualquer maneira.
Zwol
Não lsmodproduz nenhuma saída em tudo? Existe um diretório /lib/modules?
Countermode #
lsmod: command not found. Existe /lib/modules, mas ele não possui nenhum módulo , apenas um monte de diretórios por kernel contendo modules.deparquivos vazios . Vou procurar na ajuda específica do Linode e descobrir se é assim que deve ser.
Zwol
hmm ... muito estranho. Eu não estou familiarizado com o Linode, mas para mim parece que os dispositivos veth não são suportados.
Countermode #

Respostas:

9

Você pode iniciar o link OpenVPN dentro de um espaço para nome e, em seguida, executar todos os comandos que desejar para usar esse link OpenVPN dentro do espaço para nome. Detalhes sobre como fazê-lo (não é o meu trabalho) aqui:

http://www.naju.se/articles/openvpn-netns.html

Eu tentei e funciona; a idéia é fornecer um script personalizado para executar as fases de ativação e roteamento da conexão OpenVPN dentro de um espaço para nome específico, em vez do global. Cito o link acima para o caso de ficar offline no futuro:

Primeiro, crie um script --up para o OpenVPN. Este script criará a interface de túnel da VPN dentro de um espaço para nome da rede chamado vpn, em vez do espaço para nome padrão.

$ cat > netns-up << EOF
#!/bin/sh
case $script_type in
        up)
                ip netns add vpn
                ip netns exec vpn ip link set dev lo up
                mkdir -p /etc/netns/vpn
                echo "nameserver 8.8.8.8" > /etc/netns/vpn/resolv.conf
                ip link set dev "$1" up netns vpn mtu "$2"
                ip netns exec vpn ip addr add dev "$1" \
                        "$4/${ifconfig_netmask:-30}" \
                        ${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"}
                test -n "$ifconfig_ipv6_local" && \
          ip netns exec vpn ip addr add dev "$1" \
                        "$ifconfig_ipv6_local"/112
                ;;
        route-up)
                ip netns exec vpn ip route add default via "$route_vpn_gateway"
                test -n "$ifconfig_ipv6_remote" && \
          ip netns exec vpn ip route add default via \
                        "$ifconfig_ipv6_remote"
                ;;
        down)
                ip netns delete vpn
                ;;
esac
EOF

Então inicie o OpenVPN e diga para ele usar nosso script --up em vez de executar ifconfig e route.

openvpn --ifconfig-noexec --route-noexec --up netns-up --route-up netns-up --down netns-up

Agora você pode iniciar programas a serem encapsulados da seguinte maneira:

ip netns exec vpn command

O único problema é que você precisa ser root para invocar ip netns exec ... e talvez não queira que seu aplicativo seja executado como root. A solução é simples:

Comando sudo ip netns exec vpn sudo -u $ (whoami)
Asuranceturix
fonte
1
Olá e bem-vindo ao site! Incentivamos os usuários a pelo menos resumir (se possível) o conteúdo dos links que colam nas respostas. Isso ajuda a manter a qualidade da resposta caso o link fique obsoleto (por exemplo, o site não está mais acessível). Melhore sua resposta incluindo as partes / instruções mais importantes do artigo vinculado.
precisa saber é o seguinte
Isso é ótimo, mas você precisa colocar aspas simples em torno do delimitador heredoc de abertura para impedir que o shell expanda todas as variáveis.
ewatt
7

Acontece que você pode colocar uma interface de encapsulamento em um espaço para nome de rede. Todo o meu problema foi devido a um erro ao abrir a interface:

ip addr add dev $tun_tundv \
    local $ifconfig_local/$ifconfig_cidr \
    broadcast $ifconfig_broadcast \
    scope link

O problema é "link de escopo", que eu entendi errado como afetando apenas o roteamento. Faz com que o kernel defina o endereço de origem de todos os pacotes enviados para o túnel 0.0.0.0; presumivelmente, o servidor OpenVPN os descartaria como inválidos de acordo com a RFC1122; mesmo que não o fizesse, o destino seria obviamente incapaz de responder.

Tudo funcionou corretamente na ausência de namespaces de rede porque o script de configuração de rede interno do openvpn não cometeu esse erro. E sem o "link do escopo", meu script original também funciona.

(Como eu descobri isso, você pergunta? Ao executar straceo processo openvpn, configure como hexdump tudo o que lê no descritor de encapsulamento e decodifique manualmente os cabeçalhos dos pacotes.)

zwol
fonte
Alguma chance de você escrever um guia sobre isso? Estou tentando configurar algo semelhante, mas é difícil dizer quais partes da sua pergunta são boas para começar e quais são os caminhos que levaram ao fracasso.
tremby 31/12/14
@ tremby Não é provável que eu tenha tempo para fazer isso em um futuro próximo, mas você pode achar github.com/zackw/tbbscraper/blob/master/scripts/openvpn-netns.c útil.
Zwol 31/12/14
Sim, não tenho certeza se um programa C de 1100 linhas vai ajudar. Que tal apenas a configuração final, scripts e encantamentos que fizeram o trabalho para você? ... Ou esse programa C é sua implementação final disso?
tremby
@ tremby Sim, esse programa C é minha implementação final. (No meu cenário de uso, tem que ser definido, você vê.) Você pode simplesmente colocar a questão - se o grande comentário no topo não explicar como usá-lo, avise-me.
Zwol 31/12/14
@tremby Em alternativa, consulte os "Scripts executados a partir do openvpn", iniciando em github.com/zackw/tbbscraper/blob/master/scripts/… , para ver como o namespace da rede é configurado e desmontado; e a chamada real do cliente ovpn está em github.com/zackw/tbbscraper/blob/master/scripts/… . O restante do código pode ser visto como uma implementação de minis shell para tornar essas operações menos cansativas de escrever.
Zwol 31/12/14
4

O erro ao tentar criar os dispositivos veth é causado por uma alteração de como ipinterpreta os argumentos da linha de comando.

A chamada correta de ippara criar um par de dispositivos veth é

ip link add name veth0 type veth peer name veth1

( nameinstad de dev)

Agora, como retirar o tráfego do namespace para o túnel da VPN? Como você tem apenas dispositivos de ajuste à sua disposição, o "host" deve ser roteado. Ou seja, crie o par veth e coloque um no espaço para nome. Conecte o outro via roteamento ao túnel. Assim, habilite o encaminhamento e adicione as rotas necessárias.

Por uma questão de exemplo, suponha que essa eth0seja sua interface principal, tun0seja sua interface de túnel VPN e veth0/ veth1cujo par de interfaces veth1esteja no espaço para nome. Dentro do espaço para nome, você adiciona apenas uma rota padrão veth1.

No host, você precisa empregar o roteamento de políticas, veja aqui, por exemplo. O que você precisa fazer:

Adicionar / anexar uma entrada como

1   vpn

para /etc/iproute2/rt_tables. Com isso, você pode chamar a tabela (ainda a ser criada) pelo nome.

Em seguida, use as seguintes instruções:

ip rule add iif veth0 priority 1000 table vpn
ip rule add iif tun0 priority 1001 table vpn
ip route add default via <ip-addr-of-tun0> table vpn
ip route add <ns-network> via <ip-addr-of-veth0> table vpn

Não posso tentar isso aqui com uma configuração como a sua, mas isso deve fazer exatamente o que você deseja. Você pode aumentar isso por regras de filtro de pacotes, de modo que nem a VPN nem a rede "guest" sejam perturbadas.

Nota: Mover tun0para o espaço para nome em primeiro lugar parece a coisa certa a fazer. Mas como você, não consegui que isso funcionasse. O roteamento de políticas parece a próxima coisa certa a fazer. A solução da Mahendra é aplicável se você conhecer as redes por trás da VPN e todos os outros aplicativos nunca acessarão essas redes. Mas sua condição inicial ("todo o tráfego, e somente o tráfego, de / para processos específicos passa pela VPN") soa como se o último não pudesse ser garantido.

contra-modo
fonte
Obrigado, isso me leva um pouco mais longe, mas agora estou preso na parte "e depois você usa uma ponte para conectar o dispositivo veth ao túnel" - consulte a pergunta revisada.
Zwol 12/08/14
Pela resposta que acabei de postar, tudo se resume a um erro bobo no meu script original - "link de escopo" não significa o que eu pensava que isso significava. Mas vou lhe dar a recompensa, porque você se esforça muito para me ajudar a experimentar várias possibilidades, e eu provavelmente teria desistido completamente se não o fizesse.
Zwol 14/08/14
Hey Zack, muito obrigado. Os espaços para nome e o roteamento de políticas eram uma coisa interessante para pesquisar. Eu não teria me empenhado tanto nisso se não fosse empolgante por si só.
Contramodo
0

Se as redes que você acessa através da VPN são conhecidas, você pode editar sua tabela de roteamento para conseguir o que deseja.

  1. Anote sua rota padrão atual.

    # ip route | grep default default via 192.168.43.1 dev wlo1 proto static metric 1024

  2. Execute a VPN e isso introduzirá uma entrada de roteamento.

  3. Exclua a rota padrão atual (adicionada pela VPN) onde, como a rota padrão anterior, será a primeira entrada padrão na tabela.

    # ip route | grep default default dev tun0 scope link default via 192.168.43.1 dev wlo1 proto static metric 1024

    # ip route del default dev tun0 scope link

  4. Adicione rotas personalizadas às redes que estão na VPN para rotear através do tun0.

    # ip route add <net1>/16 dev tun0

    # ip route add <net2>/24 dev tun0

  5. Adicione as duas entradas do servidor de nomes (em resolv.conf), bem como a VPN e a conexão direta.

Agora todas as conexões net1 e net2 passam pela VPN e a redefinição passa diretamente (através do wlo1 neste exemplo).

mahendra
fonte
Lamentavelmente, as redes acessadas através da VPN não são conhecidas antecipadamente, portanto isso não funcionará para mim.
Zwol 14/08/14