Розділення робочих середовищ в Linux
Привіт, мене звати В’ячеслав. Я працюю інженером в компанії GlobalLogic. В даній статті я б хотів поділитись досвідом створення розділених робочих середовищ в Linux для випадків, коли ви працюєте на кількох проєктах одночасно. На пошук описаного тут рішення мене підштовхнула стаття на DOU із рубрики «як я працюю» (чи, можливо, коментарі до неї), де один з героїв розказував, що у нього на робочому столі завжди є два ноутбуки, оскільки він працює на двох різних клієнтів і тому зручніше розділяти роботу, використовуючи два девайси.
Загалом ідея сегрегації робочих середовищ у випадку, якщо ви працюєте на двох абсолютно різних клієнтів, або, наприклад, ви є студентом чи аспірантом і мусите поєднувати навчання і роботу, досить правильна і розумна. Як з точки зору безпеки, так і з точки зору зручності використання. Розділяючи середовища, ви підвищуєте безпеку, запобігаючи можливому витоку чутливої інформації, як-то паролів, ключів, різної NDA-документації від одного клієнта до іншого. Налаштування доступів, закладки в браузері, профілі в месенджерах, поштові скриньки і так далі. Також ви запобігаєте класичним факапам типу «переплутав вікна і відправив повідомлення не тому клієнту».
Але що робити, коли у вас з’являється третій, а потім четвертий клієнт? Виділяти окремий комп’ютер під кожного? Це зовсім не зручно і не практично. Чи не так? Це зовсім неінженерний підхід, подумав я і замислився над тим, що ж ми можемо зробити в даному випадку, маючи лише одну робочу машину.
На перший погляд рішення доволі просте, очевидне і лежить воно на поверхні. Ми можемо створити кілька різних користувачів в системі під кожен зі своїх «проєктів». Наприклад, ви є студентом і одночасно працюєте в компанії. Тоді ви можете створити користувачів student
та emploee
. Далі ви можете одночасно залогінитись в Х-сесію кожного з акаунтів і працювати в них паралельно, перемикаючись між «навчанням» і «роботою» в будь-який момент простою комбінацією Ctrl+Alt+F в Linux. Просто, зручно, безпечно.
А ви вже чули, що 21 червня DOU Mobile Day?
Але, як завжди, є одне «але». Що, якщо вам на роботі для доступу до корпоративних ресурсів необхідно підключатись до VPN? Підключившись до приватної мережі з акаунту emploee
з великою ймовірністю ваш трафік з акаунту student
теж буде йти через VPN-тунель. Що є недобре з точки зору безпеки і в деяких випадках може бути зовсім неробочим варіантом, адже корпоративні налаштування можуть блокувати велику кількість ресурсів, які вам потрібні для навчання.
Звісно, вам може пощастити, і у вас буде «демократичний» VPN-профіль, який не буде чіпати ваш default route і VPN буде використовуватись тільки для окремих корпоративних ресурсів. Але, як показує практика, переважно корпоративні VPN-налаштування є скоріше «авторитарними» (коли default route вашої таблиці маршрутизації направляється через VPN-тунель), або й «диктаторськими» (коли на додачу до зміни default route ваш VPN-клієнт ще й моніторить вашу таблицю маршрутизації, і щойно ви спробуєте внести туди зміни, він відкотить їх назад). Ну і зовсім складний випадок — вам також потрібен VPN для доступу до ресурсів університету. Тобто нам потрібно тримати запущеними обидва VPN-з’єднання.
Пошук рішення
Що ж ми можемо зробити? Чи можливо якось налаштувати наші середовища так, щоб можна було працювати одночасно?
Одним з можливих простих рішень може бути розгортання proxy-сервера в своїй локальній мережі, і далі налаштування своїх програм таким чином, щоб вони йшли в світ через проксі, коли ви хочете обійти VPN. Але в цього рішення є купа недоліків:
- Необхідність додаткового сервера в локальній мережі.
- При зміні локації (прийшли в кав’ярню чи співробітню) ваш proxy-сервер вже недоступний.
- Необхідно налаштовувати кожну програму окремо для роботи з proxy. Не всі це можуть підтримувати.
Що ж, нам потрібне надійніше і більш універсальне рішення. І тут на допомогу можуть прийти namespaces. Так, саме ті namespaces-ядра Linux, на яких базуються контейнери Linux. Namespaces — це спосіб ізоляції ресурсів, доступних процесу. Так, процес запущений в namespace за умовчанням (default namespace) зазвичай має доступ до глобальних ресурсів доступних ядру. Водночас процес, запущений в новоствореному namespace, може мати свою окрему інстансу цих глобальних ресурсів, змінювати їх, не зачіпаючи глобальні ресурси. Так з поміж
Іншими словами, якщо ми створимо окремий network namespace і запустимо наш VPN в ньому, то він змінить налаштування мережі виключно для тих процесів, які будуть запущені в цьому ж таки namespace. Процеси з інших namespace (в тому числі з default namespace) «не помітять» змін в мережі і будуть працювати як зазвичай. Під час запуску кожен новий процес успадковує namespace-и свого батьківського процесу.
Яким же чином нам це реалізувати? Ми можемо спробувати використати більш високорівневий інструмент, як-от firejail, або ж працювати на нижчому рівні, створюючи namespase-и і конфігуруючи мережу самостійно. З firejail нам необхідно буде запустити наш VPN-клієнт в «пісочниці» і далі домогтися того, щоб всі інші програми, яким потрібен доступ до мережі, запускались в тій самій «пісочниці». Даний підхід може бути заскладними в практичному використанні для нашої задачі. Тож спробуємо реалізувати її на нижчому рівні, забезпечуючи максимальну простоту і прозорість рішення. Отже спробуємо запустити
Конфігурація network namespace
Для створення ізольованої мережі в namespace з доступом назовні нам необхідно реалізувати ось таку схему:
+--------------------------------------------------------------+ | Default network namespace | | | | +------------------------+ +-----------+ | | | Network namespace A | | lo | | | | +---------+ | +-----------+ | | | | lo | | | | | +---------+ | +------------+ | | | | eth0 |<--------------+ | | +---------+ | +-----------+ +------------+ | | | | eth0 |<---------+--->| vethA | ^ | v | | +---------+ | +-----------+ | | +-------------+ | +------------------------+ ^ | | | | | | | | | | | v | | | Internet | | +------------------------+ +-----------+ | | | | | | Network namespace B | | bridge |<----------+ | | | | | +---------+ | +-----------+ | | | | | | | lo | | ^ | | +-------------+ | | +---------+ | | v | ^ | | | v +------------+ | | | +---------+ | +-----------+ | wlan0 |<--------------+ | | | eth0 |<---------+--->| vethB | +------------+ | | +---------+ | +-----------+ | | +------------------------+ | +--------------------------------------------------------------+
В default namespace нам необхідно створити bridge
-інтерфейс, який буде забезпечувати комунікацію між підмережами новостворених namespace та підмережею хоста, а також по одному віртуальному ethernet
-інтерфейсу, який буде іншим «кінцем» інтерфейсу з відповідного namespace. В кожному з новостворених namespace буде свій loopback
та ethernet
.
Надалі ми будемо оперувати лише користувачем
student
. Подібну конфігурацію потрібно виконати для кожного користувача, якого ми хочемо ізолювати.
Створимо bridge
на хості:
root # ip link add name br-netns type bridge root # ip addr add 192.168.8.1/24 brd 192.168.8.255 dev br-netns root # ip link set br-netns up
Створимо новий network namespace для користувача student
з відповідними інтерфейсами всередині:
root # ip netns add student root # ip link add veth-student type veth peer name eth0 netns student root # ip netns exec student ip link set lo up root # ip netns exec student ip link set eth0 up root # ip netns exec student ip addr add 192.168.8.2/24 brd 192.168.8.255 dev eth0 root # ip netns exec student ip route add default via 192.168.8.1 dev eth0 root # ip netns exec student ip route add 192.168.1.0/24 via 192.168.8.1 dev eth0
Зауважте, що при створенні namespace автоматично створився відповідний файл в
/run/netns
, який можна використовувати для переключення в цей namespace, наприклад, використовуючи командуnsenter
(дивіться далі).
Зв’яжемо наш namespace з хостом:
root # ip link set veth-student master br-netns root # ip link set veth-student up
Додатково, щоб наш хост міг форвардити трафік в світ і пакети, які приходять у відповідь, знаходили шлях до відповідної підмережі namespace, нам потрібно зробити наступне:
root # sysctl -w net.ipv4.ip_forward = 1 root # iptables -t nat -A POSTROUTING -s 192.168.8.0/24 -j MASQUERADE
Перевіримо, що у нас вийшло. На хості в default namespace маємо:
root # ip addr show br-netns 60: br-netns: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 26:70:40:10:05:a8 brd ff:ff:ff:ff:ff:ff inet 192.168.8.1/24 brd 192.168.8.255 scope global br-netns valid_lft forever preferred_lft forever inet6 fe80::2470:40ff:fe10:5a8/64 scope link proto kernel_ll valid_lft forever preferred_lft forever root # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 600 0 0 wlan0 192.168.0.0 0.0.0.0 255.255.255.0 U 600 0 0 wlan0 192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 br-netns root # curl icanhazip.com xxx.xxx.14.162
Перейдемо в новостворений network namespace:
root # nsenter --net=/run/netns/student bash root # ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever 2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0.0.0 brd 0.0.0.0 3: eth0@if61: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether f6:56:c9:89:c2:f5 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 192.168.8.2/24 brd 192.168.8.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::f456:c9ff:fe89:c2f5/64 scope link proto kernel_ll valid_lft forever preferred_lft forever root # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 eth0 192.168.1.0 192.168.8.1 255.255.255.0 UG 0 0 0 eth0 192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 root # traceroute 8.8.8.8 traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets 1 192.168.8.1 (192.168.8.1) 0.547 ms 0.434 ms 0.409 ms 2 192.168.0.1 (192.168.0.1) 4.956 ms 5.729 ms 6.641 ms 3 * * * 4 * * * 5 * * * 6 * * * 7 * * * 8 * * * 9 * * * 10 * * * 11 * * * 12 * * * 13 dns.google (8.8.8.8) 17.450 ms * 19.890 ms
Як бачимо, ми маємо eth0
-інтерфейс з відповідною IP-адресою, а також default route, який веде нас через створений br-netns
bridge інтерфейс.
Тепер можемо спробувати запустити VPN в нашій «пісочниці»:
root # openvpn /tmp/119.82.254.66_tcp_1735.ovpn 2025-05-20 14:36:21 OpenVPN 2.6.12 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD] 2025-05-20 14:36:21 library versions: OpenSSL 3.5.0 8 Apr 2025, LZO 2.10 ......................................................................... 2025-05-20 14:36:24 net_route_v4_best_gw query: dst 0.0.0.0 2025-05-20 14:36:24 net_route_v4_best_gw result: via 192.168.8.1 dev eth0 2025-05-20 14:36:24 ROUTE_GATEWAY 192.168.8.1/255.255.255.0 IFACE=eth0 HWADDR=f6:56:c9:89:c2:f5 2025-05-20 14:36:24 TUN/TAP device tun0 opened 2025-05-20 14:36:24 net_iface_mtu_set: mtu 1500 for tun0 2025-05-20 14:36:24 net_iface_up: set tun0 up 2025-05-20 14:36:24 net_addr_ptp_v4_add: 10.211.1.121 peer 10.211.1.122 dev tun0 2025-05-20 14:36:24 net_route_v4_add: 119.82.254.66/32 via 192.168.8.1 dev [NULL] table 0 metric -1 2025-05-20 14:36:24 net_route_v4_add: 0.0.0.0/1 via 10.211.1.122 dev [NULL] table 0 metric -1 2025-05-20 14:36:24 net_route_v4_add: 128.0.0.0/1 via 10.211.1.122 dev [NULL] table 0 metric -1 2025-05-20 14:36:24 Initialization Sequence Completed 2025-05-20 14:36:24 Data Channel: cipher 'AES-128-CBC', auth 'SHA1' 2025-05-20 14:36:24 Timers: ping 3, ping-restart 10
В сусідній консолі ще раз заходимо в нашу «пісочницю»:
root # nsenter --net=/run/netns/student bash root # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.211.1.122 128.0.0.0 UG 0 0 0 tun0 0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 eth0 10.211.1.122 0.0.0.0 255.255.255.255 UH 0 0 0 tun0 119.82.254.66 192.168.8.1 255.255.255.255 UGH 0 0 0 eth0 128.0.0.0 10.211.1.122 128.0.0.0 UG 0 0 0 tun0 192.168.1.0 192.168.8.1 255.255.255.0 UG 0 0 0 eth0 192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 root # curl icanhazip.com 119.82.254.66
Бачимо, що трафік тепер іде через VPN-тунель за умовчанням і наша публічна адреса змінилась.
Перевіримо ще раз конфігурацію на хості:
root # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 600 0 0 wlan0 192.168.0.0 0.0.0.0 255.255.255.0 U 600 0 0 wlan0 192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 br-netns root # curl icanhazip.com xxx.xxx.14.162
Чудово, на хості все без змін. Хост «не бачить» VPN.
Отже все має працювати, але є ще один важливий момент. Часто при підключенні до VPN нам необхідно використовувати окремі DNS-сервери для доступу до приватних ресурсів. Додавши їх до нашого /etc/resolv.conf
, ми можемо зламати/погіршити name resolution на хост-машині. Тут на допомогу можуть прийти ті самі namespace. Але тепер mount namespace.
Конфігурація mount namespace
Наша задача ізолювати /etc/resolv.conf
таким чином, щоб зміни, які вносяться в цей файл в нашій «пісочниці», не міняли оригінальний файл, який використовується на хості. Для цього ми створимо свій mount namespace, в якому у вибрану директорію змонтуємо tmpfs, в яку скопіюємо наш resolv.conf
файл. Далі нам потрібно зробити bind-mount цієї копії у відповідне місце в файловій системі. Тут слід зауважити, що зазвичай, в системах, де мережа керується за допомогою NetworkManager, файл /etc/resolv.conf
є символічним посиланням на /run/NetworkManager/resolv.conf
, тому ми можемо ізолювати /run/NetworkManager
директорій і правити resolv.conf
файл там.
Для створення і оперування namespace ми будемо використовувати файл, створений в /run/mountns/<name>
наступним чином:
root # mkdir -p /home/student/.config/sandbox root # mkdir -p /run/mountns root # touch /run/mountns/student root # unshare --mount=/run/mountns/student true root # nsenter --net=/run/netns/student --mount=/run/mountns/student bash root # mount -t tmpfs tmpfs /home/student/.config/sandbox root # mkdir -p /home/student/.config/sandbox/NetworkManager root # cp -r /run/NetworkManager/* /home/student/.config/sandbox/NetworkManager root # mount -o bind /home/student/.config/sandbox/NetworkManager /run/NetworkManager
Перевіримо, що у нас вийшло в «пісочниці»:
root # nsenter --net=/run/netns/student --mount=/run/mountns/student bash root # mount | grep -e student -e NetworkManager nsfs on /run/netns/student type nsfs (rw) tmpfs on /home/student/.config/sandbox type tmpfs (rw,relatime) tmpfs on /run/NetworkManager type tmpfs (rw,relatime)
Тепер спробуємо поредагувати /etc/resolv.conf
в «пісочниці»:
root # cat /etc/resolv.conf # Generated by NetworkManager nameserver 192.168.0.1 root # nsenter --net=/run/netns/student --mount=/run/mountns/student sh -c "echo 'nameserver 8.8.8.8' >> /etc/resolv.conf" root # nsenter --net=/run/netns/student --mount=/run/mountns/student sh -c "cat /etc/resolv.conf" # Generated by NetworkManager nameserver 192.168.0.1 nameserver 8.8.8.8 root # cat /etc/resolv.conf # Generated by NetworkManager nameserver 192.168.0.1
Чудово, як бачимо, зміни в «пісочниці» не впливають на конфігурацію на хості.
Для автоматизації описаної конфігурації можна скористатись скриптами з namespacer репозиторію. Там наявні скрипти для OpenRC. Достатньо їх скопіювати в /etc/local.d/
і зробити символічні посилання, вказуючи в назві посилання імена користувачів, для яких ви хочете створювати namespace при старті системи. Наприклад:
# ln -s /etc/local.d/20-ns{,-student}.start # ln -s /etc/local.d/20-ns{,-student}.stop
Тепер у нас все готово для запуску
На жаль, документованої опції, як саме це зробити в Xfce DE, я не знайшов, тому скористався наступним хаком:
student $ mkdir -p ~/.config/xfce4/ student $ cp /etc/xdg/xfce4/xinitrc ~/.config/xfce4/
І зробити наступні зміни:
146,147c146,155 < # start xfce4-session normally < exec ${XFCE4_SESSION_COMPOSITOR:-xfce4-session} --- > # if network and mount namespaces present run in it > if [ -f /run/netns/$USER -a -f /run/mountns/$USER ]; then > # unset some sensitive env > unset DBUS_SESSION_BUS_ADDRESS > unset XDG_RUNTIME_DIR > exec sudo -E nsenter --net=/run/netns/$USER --mount=/run/mountns/$USER su $USER -c "${XFCE4_SESSION_COMPOSITOR:-xfce4-session}" > else > # start xfce4-session normally > exec ${XFCE4_SESSION_COMPOSITOR:-xfce4-session} > fi
Додатково за необхідності можна додати нашого користувача в sudoers, оскільки nsenter
доступний тільки для root:
# echo "student ALL = (root) NOPASSWD:SETENV: /usr/bin/nsenter --net=/run/netns/student --mount=/run/mountns/student su student -c xfce4-session" >> /etc/sudoers.d/student
Логінимось в student
і перевіряємо результат. Тепер всі процеси, починаючи від xfce4-session
, мають запускатись в наших namespace.
Запустимо термінал і подивимось на конфігурацію мережі:
student $ ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever 2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0.0.0 brd 0.0.0.0 3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether f6:15:60:55:f6:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 192.168.8.2/24 brd 192.168.8.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::f415:60ff:fe55:f677/64 scope link proto kernel_ll valid_lft forever preferred_lft forever
Як бачимо, ми маємо окрему підмережу, відмінну від мережі хоста, і у нас немає ні Wi-Fi, ні Ethernet інтерфейсу хоста. Хоча якщо ми подивимось в налаштування мережі в Xfce, то там є всі інтерфейси хоста. Чому так? Справа в тому, що NetworkManager був запущений як сервіс при старті системи, і тому він працює в default network namespace. Тобто у нас зберігається можливість керувати мережею хоста через графічний інтерфейс або використовуючи команду nmcli
і водночас ми можемо вільно змінювати налаштування мережі «пісочниці», не зачіпаючи загальні налаштування системи. У випадку VPN нам потрібно його запускати не через NetworkManager, а з консолі (як ми робили це вище) або через свого клієнта, запущеного в нашому namespace.
Також слід звернути увагу на те, що якщо нам потрібно працювати з Docker, то dockerd
(а також containerd
) теж потрібно запускати з нашого namespace, а не з default namespace. Зазвичай в Linux не є проблемою запускати кілька інстансів Docker. У вищезгаданому namespacer репозиторії можна знайти додаткові інструкції, як це зробити в Gentoo. Після запуску окремого dockerd
нам необхідно визначити відповідну змінну середовища, яка має вказувати на правильний сокет нашого dockerd
сервісу. Щось типу:
export DOCKER_HOST=unix:///var/run/docker-student.sock
Зрозуміло, що рішення не є ідеальним, і потенційно можуть бути проблеми з тим, що деякі сервіси працюють в default namespace, тоді як вся
50 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів