Подача всего трафика через OpenVPN только для определенного сетевого пространства имен

16

Я пытаюсь настроить VPN (используя OpenVPN) таким образом, чтобы весь трафик, и только трафик, к / от определенных процессов проходил через VPN; другие процессы должны продолжать использовать физическое устройство напрямую. Насколько я понимаю, способ сделать это в Linux - использовать сетевые пространства имен.

Если я использую OpenVPN нормально (то есть направляя весь трафик от клиента через VPN), он работает нормально. В частности, я запускаю OpenVPN так:

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

(Отредактированная версия destination.ovpn находится в конце этого вопроса.)

Я застрял на следующем шаге, написав сценарии, которые ограничивают туннельное устройство пространствами имен. Я пытался:

  1. Помещение туннельного устройства непосредственно в пространство имен с помощью

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

    Эти команды выполняются успешно, но трафик, генерируемый внутри пространства имен (например, с помощью ip netns exec tns0 traceroute -n 8.8.8.8), попадает в черную дыру.

  2. Предполагая, что « вы можете [по-прежнему] назначать только виртуальные интерфейсы Ethernet (veth) для сетевого пространства имен » (что, если это правда, получает награду этого года за самые нелепо ненужные ограничения API), создавая пару veth и мост, и помещая один конец пары veth в пространство имен. Это даже не сводится к падению движения на полу: это не позволит мне проложить туннель к мосту! [EDIT: Это , как представляется , так как только ди устройство может быть введено в мосты. В отличие от невозможности поместить произвольные устройства в пространство имен сети, это действительно имеет смысл, поскольку мосты являются концепцией уровня Ethernet; к сожалению, мой провайдер VPN не поддерживает OpenVPN в режиме касания, поэтому мне нужно обойти это.]

    # 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
    

Сценарии в конце этого вопроса предназначены для ветеринарного подхода. Сценарии для прямого подхода могут быть найдены в истории редактирования. Переменные в сценариях, которые, кажется, используются без предварительной установки их, устанавливаются в средеopenvpn - да, она небрежная и использует строчные имена.

Пожалуйста, дайте конкретные советы о том, как заставить это работать. Я до боли осознаю, что я программирую культом грузов здесь - есть кто-нибудь написал исчерпывающую документацию для этого? Я не могу найти ни одного - поэтому общий обзор кода сценариев также приветствуется.

В случае, если это имеет значение:

# 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

Ядро было построено моим провайдером виртуального хостинга ( Linode ) и, хотя и скомпилировано с ним CONFIG_MODULES=y, не имеет реальных модулей - единственной CONFIG_*переменной, установленной в mсоответствии со значением /proc/config.gzбыло CONFIG_XEN_TMEM, и у меня фактически нет этого модуля (ядро хранится вне моей файловой системы; /lib/modulesпусто и /proc/modulesуказывает на то, что он не был каким-то волшебным образом загружен). Выдержки из /proc/config.gzпредоставленных по запросу, но я не хочу вставлять все это здесь.

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
источник
Давайте начнем с очевидного: поддерживаются ли veth устройства? загружены ли модули ядра (veth)?
контррежим
@countermode grep veth /proc/modulesничего не перечисляет, но я не знаю, насколько это убедительно. Экземпляры Linode не имеют установленного ядра внутри раздела ОС, поэтому я не уверен, что смог бы загрузить отсутствующий модуль в любом случае.
Звол
lsmodПроизводит ли вообще какой-либо вывод? Есть ли каталог /lib/modules?
контррежим
lsmod: command not found, Существует /lib/modules, но в нем нет никаких модулей , просто набор каталогов для ядра, содержащих пустые modules.depфайлы. Я покопаюсь в помощи Линоде и выясню, так ли это должно быть.
Звол
хм ... очень странно Я не знаком с Linode, но мне кажется, что эти устройства не поддерживаются.
контррежим

Ответы:

9

Вы можете запустить ссылку OpenVPN внутри пространства имен, а затем выполнить каждую команду, которую хотите использовать, эту ссылку OpenVPN внутри пространства имен. Подробности о том, как это сделать (не моя работа) здесь:

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

Я попробовал это, и это работает; Идея состоит в том, чтобы предоставить собственный сценарий для выполнения фаз up и route соединения OpenVPN внутри определенного пространства имен вместо глобального. Я цитирую приведенную выше ссылку на случай, если в будущем она отключится:

Сначала создайте скрипт --up для OpenVPN. Этот сценарий создаст интерфейс VPN-туннеля внутри сетевого пространства имен vpn вместо пространства имен по умолчанию.

$ 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

Затем запустите OpenVPN и скажите, чтобы он использовал наш скрипт --up вместо выполнения ifconfig и route.

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

Теперь вы можете запускать программы для туннелирования следующим образом:

ip netns exec vpn command

Единственная загвоздка в том, что вам нужно быть пользователем root для вызова ip netns exec ... и, возможно, вы не хотите, чтобы ваше приложение запускалось как root. Решение простое:

sudo ip netns exec vpn команда sudo -u $ (whoami)
Asuranceturix
источник
1
Привет и добро пожаловать на сайт! Мы рекомендуем пользователям по крайней мере обобщать (если возможно) содержание ссылок, которые они вставляют в ответы. Это помогает сохранить качество ответа в случае, если ссылка устарела (например, сайт больше не доступен). Пожалуйста, улучшите свой ответ, включив наиболее важные части / инструкции из связанной статьи.
Эратиэль
Это замечательно, но вам нужно поместить одинарные кавычки вокруг открывающего разделителя heredoc, чтобы оболочка не раскрыла все переменные.
Ewatt
7

Оказывается, вы можете поместить туннельный интерфейс в пространство имен сети. Вся моя проблема заключалась в ошибке при поднятии интерфейса:

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

Проблема в «ссылке на область», которую я неправильно понял, поскольку она влияет только на маршрутизацию. Это заставляет ядро ​​устанавливать адрес источника всех пакетов, отправляемых в туннель 0.0.0.0; предположительно, сервер OpenVPN затем отбросит их как недействительные согласно RFC1122; даже если это не так, пункт назначения, очевидно, не сможет ответить.

Все работало правильно в отсутствие сетевых пространств имен, потому что встроенный скрипт конфигурации сети openvpn не допустил этой ошибки. И без «ссылки на область действия», мой оригинальный сценарий работает также.

(Вы спросите, как я это обнаружил? straceЗапустив процесс openvpn, установите в hexdump все, что он читает из туннельного дескриптора, а затем вручную декодируйте заголовки пакетов.)

zwol
источник
Есть ли шанс, что вы можете написать руководство по этому вопросу? Я пытаюсь настроить что-то подобное, но трудно сказать, с каких частей вашего вопроса хорошо начинать, и какие пути привели к провалу.
дрожь
@tremby У меня вряд ли будет время сделать это в ближайшем будущем, но вы можете найти полезным github.com/zackw/tbbscraper/blob/master/scripts/openvpn-netns.c .
Звол
Да, я не уверен, что программа C на 1100 строк поможет. Как насчет только финальной конфигурации, скриптов и заклинаний, которые сделали работу за вас? ... Или эта программа на C - ваша окончательная реализация?
Тремби
@tremby Да, эта программа на C - моя последняя реализация. (В моем сценарии использования это должно быть setuid, вы видите.) Вы могли бы просто добавить вещь - если большой комментарий вверху не объясняет, как его использовать, дайте мне знать.
zwol
@tremby В альтернативном варианте посмотрите на «Сценарии, выполняемые изнутри openvpn», начиная с github.com/zackw/tbbscraper/blob/master/scripts/… , чтобы увидеть, как сетевое пространство имен установлено и разрушено; и фактический вызов клиента ovpn находится по адресу github.com/zackw/tbbscraper/blob/master/scripts/… . Оставшуюся часть кода можно представить как реализацию мини-оболочки, чтобы сделать эти операции менее утомительными для написания.
zwol
4

Ошибка при попытке создания устройств veth вызвана изменением ipинтерпретации аргументов командной строки.

Правильный вызов ipдля создания пары устройств veth

ip link add name veth0 type veth peer name veth1

( nameвместо dev)

Теперь, как вывести трафик из пространства имен в VPN-туннель? Поскольку в вашем распоряжении только устройства настройки, «хост» должен маршрутизировать. Т.е. создайте пару veth и поместите одну в пространство имен. Подключите другой через маршрутизацию к туннелю. Таким образом, включите переадресацию, а затем добавьте необходимые маршруты.

Для примера предположим, что eth0это ваш основной интерфейс, tun0ваш интерфейс VPN-туннеля и veth0/ veth1пара интерфейсов которого veth1находятся в пространстве имен. В пространстве имен вы добавляете только маршрут по умолчанию для veth1.

На хосте вам нужно использовать политику маршрутизации, см. Здесь, например. Что тебе необходимо сделать:

Добавить / добавить запись как

1   vpn

к /etc/iproute2/rt_tables. Этим вы можете назвать (еще не созданную) таблицу по имени.

Затем используйте следующие утверждения:

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

Я не могу попробовать это здесь с такой настройкой, как ваша, но это должно делать именно то, что вы хотите. Вы можете увеличить это с помощью правил фильтрации пакетов, так что ни vpn, ни "гостевая" сеть не нарушаются.

NB. Перемещение tun0в пространство имен в первую очередь выглядит правильным решением. Но, как и ты, я не получил это на работу. Политика маршрутизации выглядит следующим правильным решением. Решение Mahendra применимо, если вы знаете сети VPN, и все другие приложения никогда не получат доступ к этим сетям. Но ваше начальное состояние («весь трафик и только трафик к / от определенных процессов проходит через VPN») звучит так, как будто последнее не может быть гарантировано.

countermode
источник
Спасибо, это продвинуло меня немного дальше, но я застрял в части «а затем вы используете мост для подключения устройства veth к туннелю» - пожалуйста, посмотрите исправленный вопрос.
zwol
Согласно ответу, который я только что опубликовал, все это сводится к глупой ошибке в моем оригинальном сценарии - «ссылка на область действия» не означает, что я думал, что это означало. Но я собираюсь дать вам награду, потому что вы вложили много работы, чтобы помочь мне опробовать различные возможности, и я бы, наверное, вообще отказался от этого.
zwol
Привет, Зак, спасибо большое. Пространства имен и маршрутизация политики были интересной вещью для исследования. Я бы на самом деле не приложил столько усилий, если бы это не было захватывающим само по себе.
контррежим
0

Если сети, к которым вы получаете доступ через VPN, известны, вы можете отредактировать таблицу маршрутизации, чтобы добиться желаемого.

  1. Обратите внимание на ваш текущий маршрут по умолчанию.

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

  2. Выполните VPN, и это представит запись маршрутизации.

  3. Удалите текущий маршрут по умолчанию (который добавлен VPN), где в качестве предыдущего маршрута по умолчанию используется первая запись по умолчанию в таблице.

    # 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. Добавьте пользовательские маршруты в сети, которые находятся в VPN для маршрутизации через tun0.

    # ip route add <net1>/16 dev tun0

    # ip route add <net2>/24 dev tun0

  5. Добавьте обе записи сервера имен (в resolv.conf), а также для VPN и прямого соединения.

Теперь все соединения net1 и net2 будут проходить через VPN, а сброс будет происходить напрямую (через wlo1 в этом примере).

Мээндра
источник
К сожалению, сети, к которым осуществляется доступ через VPN, не известны заранее, поэтому это не будет работать для меня.
zwol