Alimentez tout le trafic via OpenVPN pour un espace de noms de réseau spécifique uniquement

16

J'essaie de configurer un VPN (en utilisant OpenVPN) de telle sorte que tout le trafic, et seulement le trafic, vers / depuis des processus spécifiques passe par le VPN; les autres processus devraient continuer d'utiliser directement le périphérique physique. Je crois comprendre que la façon de procéder sous Linux est d'utiliser des espaces de noms réseau.

Si j'utilise OpenVPN normalement (c.-à-d. Acheminer tout le trafic du client via le VPN), cela fonctionne bien. Plus précisément, je démarre OpenVPN comme ceci:

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

(Une version expurgée de destination.ovpn se trouve à la fin de cette question.)

Je suis coincé à l'étape suivante, j'écris des scripts qui limitent le périphérique tunnel aux espaces de noms. J'ai essayé:

  1. Mettre le périphérique tunnel directement dans l'espace de noms avec

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

    Ces commandes s'exécutent avec succès, mais le trafic généré à l'intérieur de l'espace de noms (par exemple avec ip netns exec tns0 traceroute -n 8.8.8.8) tombe dans un trou noir.

  2. En supposant que " vous ne pouvez [encore] affecter que des interfaces Ethernet virtuelles (veth) à un espace de noms de réseau " (ce qui, si cela est vrai, remporte le prix de cette année pour la restriction API la plus ridiculement inutile), créant une paire de veth et un pont, et mettre une extrémité de la paire veth dans l'espace de noms. Cela ne va même pas jusqu'à laisser tomber la circulation sur le sol: cela ne me permettra pas de mettre le tunnel dans le pont! [EDIT: Cela semble être dû au fait que seuls les appareils de prise peuvent être mis en pont. Contrairement à l'incapacité de placer des périphériques arbitraires dans un espace de noms de réseau, cela a du sens, les ponts étant un concept de couche Ethernet; malheureusement, mon fournisseur VPN ne prend pas en charge OpenVPN en mode tap, j'ai donc besoin d'une solution de contournement.]

    # 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
    

Les scripts à la fin de cette question sont pour l'approche veth. Les scripts pour l'approche directe se trouvent dans l'historique des modifications. Les variables dans les scripts qui semblent être utilisées sans les avoir définies au préalable sont définies dans l'environnement par le openvpnprogramme - oui, c'est bâclé et utilise des noms en minuscules.

Veuillez offrir des conseils spécifiques sur la façon de faire fonctionner cela. Je suis douloureusement conscient que je programme par cargo culte ici - quelqu'un a -t-il écrit une documentation complète pour ce genre de choses? Je ne trouve aucun - donc la révision générale du code des scripts est également appréciée.

Au cas où cela compte:

# 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

Le noyau a été construit par mon fournisseur d'hébergement virtuel ( Linode ) et, bien que compilé avec CONFIG_MODULES=y, n'a pas de modules réels - le seul CONFIG_*ensemble variable mselon la /proc/config.gzétait CONFIG_XEN_TMEM, et je ne fait que ce module (le noyau est stocké en dehors de mon système de fichiers; /lib/modulesest vide et /proc/modulesindique qu'il n'a pas été chargé comme par magie). Extraits de /proc/config.gzfournis sur demande, mais je ne veux pas coller le tout ici.

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
la source
Commençons par l'évidence: les appareils Veth sont-ils pris en charge? les modules du noyau (veth) sont-ils chargés?
contre-mode
@countermode grep veth /proc/modulesne répertorie rien, mais je ne sais pas si c'est concluant. Les instances de Linode n'ont pas de noyau installé dans la partition du système d'exploitation, donc je ne suis pas sûr de pouvoir de toute façon charger un module manquant.
zwol
Produit-il lsmodune sortie? Existe-t-il un répertoire /lib/modules?
contre-mode
lsmod: command not found. Il y en a un /lib/modules, mais il ne contient aucun module , juste un tas de répertoires par noyau contenant des modules.depfichiers vides . Je vais fouiller dans l'aide spécifique à Linode et voir si c'est comme ça que ça devrait être.
zwol
hmm ... très étrange. Je ne connais pas Linode, mais pour moi, il semble que les périphériques Veth ne soient pas pris en charge.
contre-mode

Réponses:

9

Vous pouvez démarrer le lien OpenVPN à l'intérieur d'un espace de noms, puis exécuter toutes les commandes que vous souhaitez utiliser ce lien OpenVPN à l'intérieur de l'espace de noms. Détails sur la façon de le faire (pas mon travail) ici:

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

Je l'ai essayé et ça marche; l'idée est de fournir un script personnalisé pour effectuer les phases de montée et de route de la connexion OpenVPN à l'intérieur d'un espace de noms spécifique au lieu du global. Je cite le lien ci-dessus au cas où il se déconnecterait à l'avenir:

Créez d'abord un script --up pour OpenVPN. Ce script créera l'interface du tunnel VPN à l'intérieur d'un espace de noms réseau appelé vpn, au lieu de l'espace de noms par défaut.

$ 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

Ensuite, démarrez OpenVPN et dites-lui d'utiliser notre script --up au lieu d'exécuter ifconfig et route.

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

Vous pouvez maintenant démarrer des programmes à tunneler comme ceci:

ip netns exec vpn command

Le seul inconvénient est que vous devez être root pour invoquer ip netns exec ...et peut-être que vous ne voulez pas que votre application s'exécute en tant que root. La solution est simple:

sudo ip netns exec vpn commande sudo -u $ (whoami)
Asuranceturix
la source
1
Bonjour et bienvenue sur le site! Nous encourageons les utilisateurs à résumer au moins (si possible) le contenu des liens qu'ils collent dans les réponses. Cela permet de conserver la qualité des réponses au cas où le lien deviendrait périmé (par exemple, le site n'est plus accessible). Veuillez améliorer votre réponse en incluant les parties / instructions les plus importantes de l'article lié.
Erathiel
C'est très bien mais vous devez mettre des guillemets simples autour du délimiteur heredoc d'ouverture pour empêcher le shell d'étendre toutes les variables.
ewatt
7

Il s'avère que vous pouvez mettre une interface de tunnel dans un espace de noms de réseau. Tout mon problème était dû à une erreur lors de l'affichage de l'interface:

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

Le problème est le "lien de portée", que j'ai mal compris comme n'affectant que le routage. Il oblige le noyau à définir l'adresse source de tous les paquets envoyés dans le tunnel à 0.0.0.0; vraisemblablement le serveur OpenVPN les rejetterait alors comme invalides selon RFC1122; même si ce n'était pas le cas, la destination ne serait évidemment pas en mesure de répondre.

Tout fonctionnait correctement en l'absence d'espaces de noms réseau car le script de configuration réseau intégré à openvpn n'a pas fait cette erreur. Et sans "lien de portée", mon script d'origine fonctionne également.

(Comment ai-je découvert cela, demandez-vous? En exécutant stracele processus openvpn, définissez sur hexdump tout ce qu'il a lu dans le descripteur de tunnel, puis décodez manuellement les en-têtes de paquets.)

zwol
la source
Avez-vous une chance de rédiger un guide à ce sujet? J'essaie de mettre en place quelque chose de similaire, mais il est difficile de dire quelles parties de votre question sont bonnes pour commencer et quels sont les chemins qui ont conduit à l'échec.
tremby
@tremby Je ne suis pas susceptible d'avoir le temps de le faire dans un proche avenir, mais vous trouverez peut-être utile github.com/zackw/tbbscraper/blob/master/scripts/openvpn-netns.c .
zwol
Yeesh, je ne suis pas sûr qu'un programme de 1100 lignes C va aider. Que diriez-vous juste de la configuration finale, des scripts et des incantations qui ont fait le travail pour vous? ... Ou est-ce que ce programme C est votre implémentation finale de cela?
tremby
@tremby Oui, ce programme C est ma dernière implémentation. (Dans mon scénario d'utilisation, il doit être défini, vous voyez.) Vous pourriez peut-être simplement déposer la chose - si le gros commentaire en haut n'explique pas comment l'utiliser, faites-le moi savoir.
zwol
@tremby Dans l'alternative, regardez les "Scripts exécutés depuis openvpn", en commençant à github.com/zackw/tbbscraper/blob/master/scripts/… , pour voir comment l'espace de noms réseau est configuré et détruit; et l'invocation réelle du client ovpn se trouve sur github.com/zackw/tbbscraper/blob/master/scripts/… . Le reste du code peut être considéré comme une implémentation de mini-shell pour rendre ces opérations moins fastidieuses à écrire.
zwol
4

L'erreur sur la tentative de création des périphériques veth est provoquée par un changement de la façon dont ipinterprète les arguments de la ligne de commande.

L'invocation correcte de ippour créer une paire de périphériques Veth est

ip link add name veth0 type veth peer name veth1

(au namelieu de dev)

Maintenant, comment acheminer le trafic de l'espace de noms vers le tunnel VPN? Comme vous ne disposez que de périphériques tun, votre "hôte" doit être acheminé. C'est-à-dire créer la paire veth et en mettre une dans l'espace de noms. Connectez l'autre via le routage au tunnel. Ainsi, activez le transfert, puis ajoutez les itinéraires nécessaires.

Par exemple, supposons qu'il eth0s'agit de votre interface principale, de tun0votre interface de tunnel VPN et veth0/ dont veth1la paire d'interfaces se veth1trouve dans l'espace de noms. Dans l'espace de noms, vous ajoutez juste une route par défaut pour veth1.

Sur l'hôte dont vous avez besoin pour utiliser le routage de stratégie, voir ici par exemple. Qu'as tu besoin de faire:

Ajouter / ajouter une entrée comme

1   vpn

à /etc/iproute2/rt_tables. Par cela, vous pouvez appeler la table (à créer) par son nom.

Utilisez ensuite les instructions suivantes:

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

Je ne peux pas essayer cela ici avec une configuration comme la vôtre, mais cela devrait faire exactement ce que vous voulez. Vous pouvez augmenter cela par des règles de filtrage de paquets telles que ni le vpn ni le réseau "invité" ne soient perturbés.

NB En tun0premier lieu, entrer dans l'espace de noms semble être la bonne chose à faire. Mais comme vous, je ne l'ai pas fait fonctionner. Le routage des stratégies semble être la prochaine chose à faire. La solution de Mahendra est applicable si vous savez que les réseaux derrière le VPN et toutes les autres applications n'accéderont jamais à ces réseaux. Mais votre condition initiale ("tout le trafic, et seulement le trafic vers / depuis des processus spécifiques passe par le VPN") semble que ce dernier ne peut pas être garanti.

contre-mode
la source
Merci, cela m'amène un peu plus loin, mais je suis maintenant bloqué sur la partie "et ensuite vous utilisez un pont pour connecter le périphérique veth au tunnel" - veuillez voir la question révisée.
zwol
Selon la réponse que je viens de poster, tout cela se résume à une erreur stupide dans mon script d'origine - "lien de portée" ne signifie pas ce que je pensais que cela signifiait. Mais je vais vous donner la récompense, parce que vous avez mis beaucoup de travail pour m'aider à essayer diverses possibilités, et j'aurais probablement abandonné tout à fait si vous ne l'aviez pas fait.
zwol
Hé Zack, merci beaucoup. Les espaces de noms et le routage des politiques étaient une chose intéressante à rechercher. Je n'aurais pas mis autant d'efforts à ce sujet si ce n'était pas excitant en soi.
contre-mode
0

Si les réseaux auxquels vous accédez via le VPN sont connus, vous pouvez modifier votre table de routage pour obtenir ce que vous voulez.

  1. Notez votre itinéraire par défaut actuel.

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

  2. Exécutez VPN et cela introduira une entrée de routage.

  3. Supprimez la route par défaut actuelle (qui est ajoutée par le VPN) où comme la route par défaut précédente pour être la première entrée par défaut dans le tableau.

    # 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. Ajoutez des itinéraires personnalisés aux réseaux qui se trouvent dans le VPN pour acheminer via tun0.

    # ip route add <net1>/16 dev tun0

    # ip route add <net2>/24 dev tun0

  5. Ajoutez les deux entrées de serveur de noms (dans resolv.conf) ainsi que pour le VPN et la connexion directe.

Maintenant, toutes les connexions net1 et net2 passeront par le VPN et la réinitialisation se fera directement (via wlo1 dans cet exemple).

Mahendra
la source
Malheureusement, les réseaux accessibles via le VPN ne sont pas connus à l'avance, donc cela ne fonctionnera pas pour moi.
zwol