Как включить TUN/TAP в LXC-контейнере: /dev/net/tun, AppArmor, cgroups и настройка

Как включить TUN/TAP в LXC-контейнере

TUN/TAP — это виртуальные сетевые устройства Linux, которые используют VPN и туннельные решения (например OpenVPN, а также некоторые лабораторные сетевые сценарии и маршрутизация). Если вы запускаете VPN внутри LXC и видите ошибки “/dev/net/tun: No such file or directory”, “Operation not permitted” или “Cannot open TUN/TAP dev”, причина почти всегда одна: контейнер не видит /dev/net/tun или ему запрещено открывать это символьное устройство. В этой инструкции разберём пошагово: проверка хоста, проброс устройства в контейнер, разрешения cgroup, влияние AppArmor/SELinux и проверка результата после перезагрузки.

LXC — лёгкая виртуализация, но устройства уровня ядра требуют правильных разрешений. Если вы строите VPN-шлюз, туннель или сложную сетевую логику, убедитесь, что у вас подходящая платформа. Обычно для таких задач выбирают VPS, где можно управлять хостом. Для максимальной производительности и нестандартных топологий подходит выделенный сервер. А если вам нужна только стандартная среда для сайта без VPN на уровне ОС, чаще проще использовать хостинг.

Полезно мыслить “слоями”. TUN/TAP — это устройство на стороне хоста (в ядре), которое должно существовать как /dev/net/tun. Дальше контейнеру нужно: (1) видеть этот device node, (2) иметь разрешение cgroup на символьное устройство (major/minor), (3) не быть заблокированным AppArmor/SELinux. Если идти по этому порядку, вы быстро найдёте, где именно “сломалось”.

1) Проверка на хосте: есть ли TUN

Начните с хоста. Проверьте наличие /dev/net/tun и загружен ли модуль tun. На многих системах устройство создаётся автоматически через udev. Если оно отсутствует, его можно создать вручную (или исправить udev/пакеты). Модуль подгружается через modprobe.

# на хосте
ls -l /dev/net/tun
lsmod | grep tun
modprobe tun

# если /dev/net/tun отсутствует:
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun

Обычно TUN — это character device c 10:200. Если у вас другое значение (редко), уточните через “stat /dev/net/tun”. Когда хост в порядке, переходите к конфигурации контейнера.

2) Проброс /dev/net/tun в контейнер (LXC config)

У LXC есть конфиг контейнера (часто /var/lib/lxc/NAME/config), где описываются монтирования и разрешения. Обычно нужны две вещи: bind-mount, чтобы /dev/net/tun появился внутри контейнера, и правило cgroup, чтобы контейнеру разрешили использовать символьное устройство. Синтаксис может отличаться между cgroup v1 и v2, но смысл один: разрешить “c 10:200 rwm”.

# примеры для LXC config

# 1) проброс устройства внутрь контейнера
lxc.mount.entry = /dev/net/tun dev/net/tun none bind,create=file 0 0

# 2) разрешение устройства (cgroup v1)
lxc.cgroup.devices.allow = c 10:200 rwm

# 2b) вариант для cgroup v2
lxc.cgroup2.devices.allow = c 10:200 rwm

После правок перезапустите контейнер. Внутри контейнера /dev/net/tun должен появиться. Если не появился — проверьте путь, права, и что именно этот config реально используется (на некоторых платформах конфиги генерируются автоматически).

3) AppArmor/SELinux и ошибка “Operation not permitted”

Если /dev/net/tun внутри есть, но VPN всё равно падает с “Operation not permitted”, значит блокирует слой безопасности. На Ubuntu-хостах часто виноват AppArmor, на некоторых RHEL/CentOS-хостах — SELinux. LXC обычно запускает контейнер с дефолтным профилем AppArmor, который может ограничивать доступ к устройствам.

Практичная диагностика: попробуйте обратиться к устройству внутри контейнера (это не должно быть “No such file”). Затем проверьте логи на хосте: при AppArmor часто видны deny-сообщения. Если вы видите явный deny на /dev/net/tun, сделайте более подходящий профиль для контейнера или (только для теста) временно ослабьте профиль, чтобы подтвердить причину.

# внутри контейнера
ls -l /dev/net/tun
cat /dev/net/tun

Для SELinux подход зависит от политики, но идея та же: если mount и cgroup выглядят правильно, а доступ запрещён, смотрите security policy. Важно не путать причину: “нет устройства” и “устройство есть, но запрещено” — это разные ветки исправления.

4) Privileged vs unprivileged и нужные capabilities

Privileged контейнеры часто проще для доступа к устройствам, но они менее безопасны. Unprivileged безопаснее, но могут требовать дополнительных настроек для device access. Если вы строите VPN-шлюз и хотите предсказуемость, иногда проще запустить VPN на VM/VPS, чем бороться с частными случаями LXC.

Ещё момент: VPN часто требует сетевых прав. Например, для добавления маршрутов, включения форвардинга и управления iptables/nft может понадобиться CAP_NET_ADMIN (а иногда CAP_NET_RAW). В LXC можно “оставить” нужные capabilities. Делайте это минимально: не выдавайте лишнего, если сервису это не нужно.

# LXC config (пример, если вашему сервису это требуется)
lxc.cap.keep = net_admin net_raw

5) Проверка: создаётся ли tun/tap интерфейс и идёт ли трафик

После перезапуска контейнера убедитесь, что /dev/net/tun существует, затем запустите VPN. OpenVPN обычно создаёт tun0, а некоторые сценарии создают tap0. Проверьте “ip a” внутри контейнера и убедитесь, что интерфейс появился. Если интерфейс появился, но трафик не идёт, это уже маршрутизация и firewall, а не TUN/TAP.

Для сценариев “VPN + NAT в интернет” заранее решите, где будет NAT: на хосте или внутри контейнера. В LXC средах NAT часто удобнее делать на хосте, потому что контейнер не всегда должен управлять глобальными правилами сети. Если вы делаете NAT в контейнере, убедитесь, что у него есть необходимые capabilities и что политика хоста это допускает.

Типичные ошибки и рекомендации по безопасности

Чаще всего проблема в одном из пунктов: (1) на хосте нет модуля tun или /dev/net/tun не создан, (2) устройство не проброшено в контейнер через lxc.mount.entry, (3) cgroups не разрешают c 10:200, (4) AppArmor/SELinux блокирует доступ, (5) не хватает CAP_NET_ADMIN для маршрутов/NAT. Если проверять эти уровни последовательно, причина обычно находится быстро.

По безопасности: не делайте все контейнеры “unconfined” ради одного VPN. Лучше выдать доступ к TUN/TAP только нужному контейнеру и только минимальные capabilities. Идеально — держать VPN как отдельный сервис в отдельном контейнере/VM, чтобы ограничить последствия в случае ошибки конфигурации или компрометации.