Alimente todo el tráfico a través de OpenVPN solo para un espacio de nombres de red específico

16

Estoy tratando de configurar una VPN (usando OpenVPN) de modo que todo el tráfico, y solo el tráfico, hacia / desde procesos específicos pase por la VPN; otros procesos deberían continuar usando el dispositivo físico directamente. Tengo entendido que la forma de hacer esto en Linux es con espacios de nombres de red.

Si uso OpenVPN normalmente (es decir, canalizando todo el tráfico del cliente a través de la VPN), funciona bien. Específicamente, comienzo OpenVPN así:

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

(Una versión redactada de destination.ovpn se encuentra al final de esta pregunta).

Estoy atascado en el siguiente paso, escribiendo scripts que restringen el dispositivo del túnel a espacios de nombres. Yo he tratado:

  1. Poner el dispositivo de túnel directamente en el espacio de nombres con

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

    Estos comandos se ejecutan con éxito, pero el tráfico generado dentro del espacio de nombres (por ejemplo, con ip netns exec tns0 traceroute -n 8.8.8.8) cae en un agujero negro.

  2. Suponiendo que " todavía puede [todavía] asignar interfaces virtuales Ethernet (veth) a un espacio de nombres de red " (lo que, de ser cierto, se lleva el premio de este año por la restricción API más ridículamente innecesaria), creando un par veth y un puente, y poniendo un extremo del par veth en el espacio de nombres. Esto ni siquiera llega a dejar caer el tráfico en el piso: ¡no me permitirá poner el túnel en el puente! [EDITAR: Esto parece deberse a que solo los dispositivos de tap se pueden poner en puentes. A diferencia de la incapacidad de colocar dispositivos arbitrarios en un espacio de nombres de red, eso realmente tiene sentido, ya que los puentes son un concepto de capa Ethernet; desafortunadamente, mi proveedor de VPN no admite OpenVPN en modo tap, por lo que necesito una solución 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
    

Los guiones al final de esta pregunta son para el enfoque veth. Los scripts para el enfoque directo se pueden encontrar en el historial de edición. El programa configura las variables en los scripts que parecen usarse sin establecerlas primero en el entorno openvpn; sí, es descuidado y usa nombres en minúsculas.

Ofrezca consejos específicos sobre cómo hacer que esto funcione. Soy dolorosamente consciente de que estoy programando por el culto de carga aquí. ¿ Alguien ha escrito documentación exhaustiva para estas cosas? No puedo encontrar ninguno, por lo que también se agradece la revisión general del código de los scripts.

En caso de que 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

El kernel fue creado por mi proveedor de alojamiento virtual ( Linode ) y, aunque está compilado CONFIG_MODULES=y, no tiene módulos reales: la única CONFIG_*variable establecida de macuerdo con /proc/config.gzfue CONFIG_XEN_TMEM, y en realidad no tengo ese módulo (el kernel se almacena fuera de mi sistema de archivos; /lib/modulesestá vacío e /proc/modulesindica que no se cargó mágicamente de alguna manera). Extractos /proc/config.gzproporcionados a pedido, pero no quiero pegar todo aquí.

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
fuente
Comencemos con lo obvio: ¿son compatibles los dispositivos veth? ¿Están cargados los módulos del núcleo (veth)?
contramodo
@countermode grep veth /proc/modulesno enumera nada, pero no sé si eso es concluyente. Las instancias de Linode no tienen un núcleo instalado dentro de la partición del sistema operativo, por lo que no estoy seguro de poder cargar un módulo faltante de todos modos.
zwol
¿ lsmodProduce alguna salida? ¿Hay un directorio /lib/modules?
contramodo
lsmod: command not found. Hay un /lib/modules, pero no tiene ningún módulo , solo un montón de directorios por núcleo que contienen modules.deparchivos vacíos . Buscaré ayuda específica de Linode y averiguaré si así es como se supone que debe ser.
zwol
hmm ... muy extraño. No estoy familiarizado con Linode, pero a mí me parece que no se admiten varios dispositivos.
contramodo

Respuestas:

9

Puede iniciar el enlace OpenVPN dentro de un espacio de nombres y luego ejecutar cada comando que desee usar ese enlace OpenVPN dentro del espacio de nombres. Detalles sobre cómo hacerlo (no mi trabajo) aquí:

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

Lo probé y funciona; La idea es proporcionar un script personalizado para llevar a cabo las fases de subida y enrutamiento de la conexión OpenVPN dentro de un espacio de nombres específico en lugar del global. Cito del enlace anterior en caso de que se desconecte en el futuro:

Primero cree un script --up para OpenVPN. Este script creará la interfaz del túnel VPN dentro de un espacio de nombres de red llamado vpn, en lugar del espacio de nombres predeterminado.

$ 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

Luego inicie OpenVPN y dígale que use nuestro script --up en lugar de ejecutar ifconfig y route.

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

Ahora puede iniciar programas para que se tunelen así:

ip netns exec vpn command

El único inconveniente es que debes ser root para invocar ip netns exec ...y tal vez no quieras que tu aplicación se ejecute como root. La solución es simple:

comando sudo ip netns exec vpn sudo -u $ (whoami)
Asuranceturix
fuente
1
Hola y bienvenidos al sitio! Alentamos a los usuarios a que al menos resuman (si es posible) el contenido de los enlaces que pegan en las respuestas. Esto ayuda a mantener la calidad de respuesta en caso de que el enlace se vuelva obsoleto (por ejemplo, el sitio ya no es accesible). Mejore su respuesta incluyendo las partes / instrucciones más importantes del artículo vinculado.
Erathiel
Esto es genial, pero necesita poner comillas simples alrededor del delimitador de apertura doc para evitar que el shell expanda todas las variables.
ewatt
7

Resulta que se puede poner una interfaz de túnel en un espacio de nombres de red. Todo mi problema se debió a un error al abrir la interfaz:

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

El problema es el "enlace de alcance", que no entendí que solo afecta el enrutamiento. Hace que el núcleo establezca la dirección de origen de todos los paquetes enviados al túnel 0.0.0.0; presumiblemente el servidor OpenVPN los descartaría como inválidos según RFC1122; incluso si no fuera así, el destino obviamente no podría responder.

Todo funcionó correctamente en ausencia de espacios de nombres de red porque el script de configuración de red incorporado de openvpn no cometió este error. Y sin "enlace de alcance", mi script original también funciona.

(¿Cómo descubrí esto, me preguntas? Al ejecutar straceel proceso openvpn, configurar hexadecimal todo lo que lee del descriptor de túnel y luego decodificar manualmente los encabezados de los paquetes).

zwol
fuente
¿Alguna posibilidad de que puedas escribir una guía sobre esto? Estoy tratando de configurar algo similar, pero es difícil saber desde qué partes de su pregunta es bueno comenzar y cuáles son los caminos que condujeron al fracaso.
temblor
@tremby No es probable que tenga tiempo para hacerlo en el futuro cercano, pero puede que le resulte útil github.com/zackw/tbbscraper/blob/master/scripts/openvpn-netns.c .
zwol
Sí, no estoy seguro de que un programa de 1100 líneas C vaya a ayudar. ¿Qué tal solo la configuración final, los scripts y los encantamientos que hicieron el trabajo por usted? ... ¿O es ese programa C tu implementación final de esto?
temblor
@tremby Sí, ese programa C es mi implementación final. (En mi escenario de uso, tiene que ser setuid, ya ves). Es posible que puedas dejarlo caer; si el gran comentario en la parte superior no explica cómo usarlo, avísame.
zwol
@tremby Como alternativa, mire los "Scripts ejecutados desde dentro de openvpn", comenzando en github.com/zackw/tbbscraper/blob/master/scripts/… , para ver cómo se configura y desglosa el espacio de nombres de la red; y la invocación real del cliente ovpn se encuentra en github.com/zackw/tbbscraper/blob/master/scripts/… . El resto del código puede considerarse como una implementación de mini-shell para hacer que esas operaciones sean menos aburridas de escribir.
zwol
4

El error al intentar crear los dispositivos veth es causado por un cambio en la forma de ipinterpretar los argumentos de la línea de comandos.

La invocación correcta de ippara crear un par de dispositivos veth es

ip link add name veth0 type veth peer name veth1

( nameinstad de dev)

Ahora, ¿cómo sacar el tráfico del espacio de nombres al túnel VPN? Como solo tiene dispositivos tun a su disposición, el "host" debe enrutar. Es decir, crear el par veth y poner uno en el espacio de nombres. Conecte el otro a través del enrutamiento al túnel. Por lo tanto, habilite el reenvío y luego agregue las rutas necesarias.

Por ejemplo, supongamos que eth0es su interfaz principal, tun0es su interfaz de túnel VPN y veth0/ o veth1el par de interfaces de las cuales veth1está en el espacio de nombres. Dentro del espacio de nombres agrega solo una ruta predeterminada veth1.

En el host que necesita emplear el enrutamiento de políticas, consulte aquí, por ejemplo. Qué necesitas hacer:

Agregar / agregar una entrada como

1   vpn

a /etc/iproute2/rt_tables. Con esto, puede llamar a la tabla (aún por crear) por nombre.

Luego use las siguientes declaraciones:

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

No puedo probar eso aquí con una configuración como la tuya, pero esto debería hacer exactamente lo que quieres. Puede aumentar eso mediante reglas de filtro de paquetes de modo que ni la red vpn ni la red "invitada" se vean perturbadas.

Nota: tun0en primer lugar, pasar al espacio de nombres parece ser lo correcto. Pero como tú, no conseguí que eso funcionara. El enrutamiento de políticas parece ser el siguiente paso correcto. La solución de Mahendra es aplicable si conoce las redes detrás de la VPN y todas las demás aplicaciones nunca accederán a esas redes. Pero su condición inicial ("todo el tráfico, y solo el tráfico, hacia / desde procesos específicos pasa por la VPN") suena como si este último no se puede garantizar.

contramodo
fuente
Gracias, esto me lleva un poco más lejos, pero ahora estoy atrapado en la parte "y luego usas un puente para conectar el dispositivo veth al túnel". Por favor, consulta la pregunta revisada.
zwol
Según la respuesta que acabo de publicar, todo se reduce a un error tonto en mi guión original: "enlace de alcance" no significa lo que pensé que significaba. Pero te daré la recompensa, porque trabajaste mucho para ayudarme a probar varias posibilidades, y probablemente me habría dado por vencido si no lo hubieras hecho.
zwol
Hola Zack, muchas gracias. Los espacios de nombres y el enrutamiento de políticas fueron algo interesante de investigar. No hubiera puesto tanto esfuerzo en esto si no fuera emocionante por sí mismo.
contramodo
0

Si conoce las redes a las que accede a través de la VPN, puede editar su tabla de enrutamiento para lograr lo que desea.

  1. Tenga en cuenta su ruta predeterminada actual.

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

  2. Ejecute VPN y esto introducirá una entrada de enrutamiento.

  3. Elimine la ruta predeterminada actual (que agrega la VPN) donde, como la ruta predeterminada anterior, será la primera entrada predeterminada en la tabla.

    # 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. Agregue rutas personalizadas a las redes que se encuentran en la VPN para enrutar a través de tun0.

    # ip route add <net1>/16 dev tun0

    # ip route add <net2>/24 dev tun0

  5. Agregue ambas entradas de servidor de nombres (en resolv.conf) también para la conexión VPN y directa.

Ahora todas las conexiones net1 y net2 pasarán por la VPN y el reinicio irá directamente (a través de wlo1 en este ejemplo).

mahendra
fuente
Lamentablemente, las redes a las que se accede a través de la VPN no se conocen de antemano, por lo que esto no funcionará para mí.
zwol