Featured image of post wg-quick network namespace

wg-quick network namespace

Костылим wg-quick для создания net namespace

TL;DR

1
2
3
PreUp = ip netns list | grep -qF -- %i && ip netns delete %i || true
PostUp = (ip netns list | grep -qF -- %i || ip netns add %i) && ip -n %i link set up lo && IPS="$(ip address save dev %i | base64)" && ip link set %i netns %i && echo "$IPS" | base64 -d | ip -n %i address restore dev %i && ip -n %i link set up %i && (ip -n %i route show default | grep -qF default || ip -n %i route add default dev %i) && echo  'IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1lCgpORVROUz0nJXMnCgpjYXQgPDxFT0YgfCB4YXJncyAtMCBzdWRvIC1FIG5zZW50ZXIgIi0tbmV0PS92YXIvcnVuL25ldG5zLyRORVROUyIgdW5zaGFyZSAtLW1vdW50IHNoIC1jCm1vdW50IC0tYmluZCAiL3Zhci9ydW4vcmVzb2x2Y29uZi9pbnRlcmZhY2VzLyRORVROUyIgL2V0Yy9yZXNvbHYuY29uZiAmJgpzdWRvIC1FIC11ICcjJHtTVURPX1VJRDotJChpZCAtdSl9JyAtZyAnIyR7U1VET19HSUQ6LSQoaWQgLWcpfScgLS0gJEAKRU9GCg==' | base64 -d | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-exec && echo  'IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1lCgpORVROUz0nJXMnCgpjYXQgPDxFT0YgfCB4YXJncyAtMCBzdWRvIC1FIG5zZW50ZXIgIi0tbmV0PS92YXIvcnVuL25ldG5zLyRORVROUyIgdW5zaGFyZSAtLW1vdW50IHNoIC1jCm1vdW50IC0tYmluZCAiL3Zhci9ydW4vcmVzb2x2Y29uZi9pbnRlcmZhY2VzLyRORVROUyIgL2V0Yy9yZXNvbHYuY29uZiAmJgpzdWRvIC1FIC11ICcjJHtTVURPX1VJRDotJChpZCAtdSl9JyAtZyAnIyR7U1VET19HSUQ6LSQoaWQgLWcpfScgLS0gd2ctcXVpY2sgZG93biAiJHtORVROU30iCkVPRgo=' | base64 -d | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-down && chmod 0755 /usr/local/bin/wg-%i-* && chown root:root /usr/local/bin/wg-%i-* && echo 'TkFNRT0nJXMnCmNhdCA8PEVPRgojICDilJAg4pSsb+KUrOKUgOKUkOKUrOKUgOKUkOKUjOKUgOKUkOKUrCDilJDilKzilIDilJDilKzilIDilJDilKzilIDilJAgIOKUjOKUkOKUkOKUrOKUgOKUkOKUjOKUkOKUkOKUjOKUkOKUkOKUkOKUgOKUkAojICDilILilILilILilILilILilKzilJjilJzilIAg4pSCIOKUrOKUgiDilILilILilIDilKTilILilKzilJjilIIg4pSCICDilILilILilILilJzilIAgIOKUgiDilILilILilILilJTilIDilJAKIyAg4pSU4pS04pSY4pSY4pSY4pSU4pSY4pS04pSA4pSY4pSY4pSA4pSY4pSY4pSA4pSY4pSYIOKUmOKUmOKUlOKUmOKUmOKUgOKUmCAg4pSY4pSU4pSY4pS04pSA4pSYIOKUmCDilJjilJTilJjilIDilIDilJgKVGhhbmtzIGZvciB1c2luZyBuZXRucyB3Zy1xdWljayB3cmFwcGVyIQoKL3Vzci9sb2NhbC9iaW4vd2ctJHtOQU1FfS1leGVjIENPTU1BTkQgICAtIGV4ZWN1dGUgc2hlbGwgY29tbWFuZCBpbiAke05BTUV9IG5ldHdvcmsgbmFtZXNwYWNlCi91c3IvbG9jYWwvYmluL3dnLSR7TkFNRX0tZG93biAgICAgICAgICAgLSB1c2UgaXQgaW5zdGVhZCBvZiB3Zy1xdWljayBkb3duICR7TkFNRX0KRU9G' | base64 -d | xargs -0 -I{} printf {} %i | sh - 
PreDown = rm -f /usr/local/bin/wg-%i-* || true

Введение

Wireguard – обалденный, простой VPN, который уже включён в состав ядра. Очень мощный и простой инструмент. Ставиться в разы проще чем OpenVPN. Для его простой настройки используется утилита wg-quick, которая входит в состав wireguard tools. Для более полного понимания, дополнительно уточню, что из моего опыта большинство решений, таких как WGDashboard и wireguard-ui генерируют клиентские конфиги именно под wg-quick.

Постановка задачи

Переносить wireguard интерфейс в отдельное сетевое пространство имён, чтобы потом в нём запускать приложения. Зачем это делать? Это избавляет от необходимости проксировать трафик на заблокированные сайты через Wireguard какими-то сложными решениями. Нужно зайти куда-то в такое место - просто запустил ещё один браузер в пространстве Wireguard и всё! Я так, к примеру, переписал Exec строку в ярлыке Thunderbird, которому альтернативы пока столь же функциональной нет, и теперь он всегда запускается через VPN и может спокойно собирать почту с любых SMTP. Использовать нужно ванильный wg-quick без доработок, чтобы это работало с ходу.

Разбор решения

В документации, есть 4 пути к дополнительной настройке wg-quick интерфейса. PreUp, PostUp, PreDown, PostDown. Значит нужно написать скрипты таким образом, чтобы они делали всё что нужно. Готовое решение представлено в начале статьи, а теперь его разберём. Я преобразую этот ужатый код в человекочитаемый вид, выглядит иначе, но содержательно буква в букву. Замечание: в коде встречается %i, это макрос самого wg-quick, везде где он встретит этот макрос, подставит имя создаваемого интерфейса.

PreUp

Тут всё просто.

1
2
3
4
5
# в списке сетевых пространств ищем наше
if ip netns list | grep -qF -- %i; then
  # если нашли - удаляем
  ip netns delete %i
fi

PostUp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
if ! ip netns list | grep -qF -- %i; then
  # создаём сетевое пространство если его ещё нет (а его и не должно быть)
  ip netns add %i
fi
# запускаем там loopback интерфейс (127.0.0.1 который)
ip -n %i link set up lo
# сохраняем список сетевых адресов которые wg-quick навесил на интерфейс при создании
# и кодируем это в base64, потому что это бинарный поток в формате утилиты ip
IPS="$(ip address save dev %i | base64)" 
# переносим интерфейс в наше специальное пространство имён
ip link set %i netns %i
# навешиваем адреса обратно, раскодируя их из переменной созданной ранее
echo "$IPS" | base64 -d | ip -n %i address restore dev %i 
# запускаем сетевой интерфейс в том пространстве имён
ip -n %i link set up %i
# ищем маршрут на 0.0.0.0/0 в списке путей нашего пространства имён
if ! ip -n %i route show default | grep -qF default; then
  # добавляем если не нашли
  ip -n %i route add default dev %i
fi
# тут был скрипт в base64, я его прямо для вас в тексте раскодировал, чтобы просто было понятно что там
# скрипт этот раскодируется и в него с помощью printf вставляется имя интерфейса с которым всегда будет
# работать скрипт
cat <<EOT | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-exec
#!/usr/bin/env sh
set -e

# вот сюда это имя интерфейса залетит в переменную
NETNS='%s'

# а вот эту лабуду я отдельно поясню
cat <<EOF | xargs -0 sudo -E nsenter "--net=/var/run/netns/$NETNS" unshare --mount sh -c
mount --bind "/var/run/resolvconf/interfaces/$NETNS" /etc/resolv.conf &&
sudo -E -u '#${SUDO_UID:-$(id -u)}' -g '#${SUDO_GID:-$(id -g)}' -- [email protected]
EOF
EOT
# вот такой же скрипт, но мы там выполняем не пользовательскую команду, а wg-quick down
cat <<EOT | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-down
#!/usr/bin/env sh
set -e

NETNS='%s'

cat <<EOF | xargs -0 sudo -E nsenter "--net=/var/run/netns/$NETNS" unshare --mount sh -c
mount --bind "/var/run/resolvconf/interfaces/$NETNS" /etc/resolv.conf &&
sudo -E -u '#${SUDO_UID:-$(id -u)}' -g '#${SUDO_GID:-$(id -g)}' -- wg-quick down "${NETNS}"
EOF
EOT
# меняем собственника и права на файлы
chmod 0755 /usr/local/bin/wg-%i-*
chown root:root /usr/local/bin/wg-%i-*
# просто выводим текст на экран в момент поднятия интерфейса - справка для пользователя,
# которая рассказывает о двух скриптах созданных ранее. Этот текст раскодируется и прямо
# кидается в sh, то есть выполняется на ходу, а там только и есть что один cat
cat <<EOT | xargs -0 -I{} printf {} %i | sh - 
NAME='%s'
cat <<EOF
#  ┐ ┬o┬─┐┬─┐┌─┐┬ ┐┬─┐┬─┐┬─┐  ┌┐┐┬─┐┌┐┐┌┐┐┐─┐
#  │││││┬┘├─ │ ┬│ ││─┤│┬┘│ │  │││├─  │ │││└─┐
#  └┴┘┘┘└┘┴─┘┘─┘┘─┘┘ ┘┘└┘┘─┘  ┘└┘┴─┘ ┘ ┘└┘──┘
Thanks for using netns wg-quick wrapper!

/usr/local/bin/wg-${NAME}-exec COMMAND   - execute shell command in ${NAME} network namespace
/usr/local/bin/wg-${NAME}-down           - use it instead of wg-quick down ${NAME}
EOF
EOT

А вот тут самое сумашествие. Поясняю основные моменты.

  1. Когда переносишь сетефой интерфейс в другое пространство, с него, к сожалению, слетают все IP адреса. По-этому, мы их в начале сохраняем, а потом восстанавливаем, после переноса.
  2. Loopback интерфейс в новом пространстве по умолчанию выключен, по-этому мы его включаем сами
  3. Добавляется маршрут только на 0.0.0.0/0 потому что у нас в принципе трафику кроме как на wireguard ходить некуда, по-этому весь туда пускаем и без разницы, что в AllowedIPs на интерфейсе прописано. Так скрипт проще, работает так же, уязвимостей не создаёт.
  4. Создаются два скрипта: wg-%i-exec, wg-%i-down. Первый нужен чтобы ним запускать программы в нашем сетевом пространстве, а второй нужен чтобы просто сделать wg-quick down, потому что просто выполнив wg-quick down остановить интерфейс не выйдет, нужно wg-quick выполнять внутри нашего пространства имён

Как работает wg-%i-exec

Тут есть ряд проблем, которые решает скрипт:

  1. Внутри пространства имён нет resolv.conf-а, или может не быть. Всегда его создаёт себе сам wg-quick через resolvconf, даже без всех наших ухищрений. То есть он создаётся для интерфейса. Это нужно для разрешение DNS имён.
  2. Нужно сделать так, чтобы даже запукая exec пользователь, не получал права root в подарок, а оставался со своими привилегиями.

Я раскидаю скрипт по строкам в, возможно, даже не рабочий shell код, но так удобнее будет коментировать.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# выполняем от имени судо, сохраняя env текущего пользователя, это будет всё равно с правами рута
sudo -E 
# nsenter запустит нам в этом пространстве имён
nsenter "--net=/var/run/netns/$NETNS"
# а потом размонтирует, всё что монтировалось
unshare --mount 
# команду shell-a, который выполнит скрипт
sh -c
'
# где мы монтируем resolv.conf созданный wg-quick-ом
mount --bind "/var/run/resolvconf/interfaces/$NETNS" /etc/resolv.conf &&
# и с помощью sudo сохраняя всё тот же env текущего пользователя
sudo -E 
# не не от имени рута, а уже от имени пользователя, который запустил exec скрипт
-u '#${SUDO_UID:-$(id -u)}' 
-g '#${SUDO_GID:-$(id -g)}' 
# выполняем то, что он попросил
-- [email protected]
'

Итог

Просто вставляем эти 3 строки в конфиг и всё! Никакого Ansible, танцев с бубном, сложной настройки и поддержки.

All rights reserved
Создано при помощи Hugo
Тема Stack, дизайн Jimmy