Главная/Основы Containerization/Урок

2.3. Linux namespaces

Цели

После этого материала вы сможете:

  • объяснить, что такое namespace и какую задачу он решает;
  • перечислить все типы namespaces и сказать, что изолирует каждый;
  • посмотреть namespaces любого процесса, включая процессы containers;
  • определить, находятся ли два процесса в одном namespace;
  • войти в namespace работающего container с host;
  • создать изолированное окружение без Docker, средствами unshare;
  • объяснить, какие namespaces Docker создаёт по умолчанию, а какие — нет.

Предварительные знания

  • 2.1. Процессы и containers;
  • понимание файловой системы /proc;
  • базовое представление о сетевых интерфейсах.

Ключевые термины

ТерминОбъяснение
namespaceМеханизм ядра, изолирующий представление процесса об одном виде системных ресурсов
unshareСистемный вызов и одноимённая утилита: отделяет процесс от текущего namespace, создавая новый
setnsСистемный вызов для входа в существующий namespace
nsenterУтилита, использующая setns для запуска команды в namespaces другого процесса
inodeНомер файлового объекта. Namespaces идентифицируются номерами inode
mount propagationПравила, по которым монтирования распространяются между mount namespaces

Теория

Задача, которую решают namespaces

Обычно все процессы Linux видят одну систему: один список процессов, одну файловую систему, один набор сетевых интерфейсов, одно имя хоста. Namespace разрывает эту единственность: процессы в разных namespaces видят разные системы, хотя выполняются на одном ядре.

Формально namespace — это обёртка вокруг глобального ресурса ядра, из-за которой процессам внутри namespace кажется, что они обладают собственным экземпляром этого ресурса.

Каждый процесс принадлежит ровно одному namespace каждого типа. По умолчанию это initial namespace — тот же, что у init.

Типы namespaces

ТипФлаг clone()Что изолируетПоявился
MountCLONE_NEWNSТочки монтирования и дерево файловой системыLinux 2.4.19
UTSCLONE_NEWUTSИмя хоста и доменное имя NISLinux 2.6.19
IPCCLONE_NEWIPCОчереди сообщений, семафоры, разделяемая память System V; POSIX message queuesLinux 2.6.19
PIDCLONE_NEWPIDПространство идентификаторов процессовLinux 2.6.24
NetworkCLONE_NEWNETСетевые интерфейсы, таблицы маршрутизации, правила фильтрации, порты, /proc/netLinux 2.6.29
UserCLONE_NEWUSERИдентификаторы пользователей и групп, capabilitiesLinux 3.8
CgroupCLONE_NEWCGROUPКорень иерархии cgroup, видимый процессуLinux 4.6
TimeCLONE_NEWTIMEСистемные часы CLOCK_MONOTONIC и CLOCK_BOOTTIMELinux 5.6

Аббревиатура UTS происходит от структуры utsname («UNIX Time-sharing System»), которую возвращает системный вызов uname().

Что делает каждый тип

Mount namespace. Собственное дерево точек монтирования. Именно он даёт container отдельную файловую систему: сначала создаётся mount namespace, затем внутри него выполняется pivot_root() на распакованные слои образа. Монтирование внутри container не влияет на host, и наоборот — если не настроено обратное через mount propagation.

UTS namespace. Собственное имя хоста. Причина, по которой hostname внутри container возвращает ID container, а не имя вашей машины.

IPC namespace. Собственные механизмы межпроцессного взаимодействия System V и POSIX message queues. Два container не могут случайно использовать один сегмент разделяемой памяти. Флаг --ipc=host отключает эту изоляцию — иногда требуется, например, для PyTorch DataLoader с большим shared_memory.

PID namespace. Собственное пространство PID. Единственный namespace с иерархией: процессы дочернего namespace видны из родительского, но не наоборот. Первый процесс в новом PID namespace получает PID 1 и особый статус — см. раздел 04.

Network namespace. Собственный сетевой стек целиком: интерфейсы, адреса, маршруты, правила iptables/nftables, номера портов, содержимое /proc/net. Порт 8000, занятый в одном namespace, свободен в другом. Именно поэтому три container могут одновременно слушать порт 80. Подробно — в разделе 08.

User namespace. Собственное отображение UID и GID. Позволяет процессу быть root внутри, оставаясь непривилегированным снаружи. Основа rootless mode. Docker по умолчанию не создаёт user namespace для containers — это важно и объясняется ниже.

Cgroup namespace. Скрывает от процесса реальный путь его cgroup в иерархии, показывая его как корень. Предотвращает утечку информации о структуре host.

Time namespace. Позволяет сместить CLOCK_MONOTONIC и CLOCK_BOOTTIME. Нужен в основном для миграции containers (checkpoint/restore). Docker его по умолчанию не использует; системное время (CLOCK_REALTIME) он не изолирует вовсе — оно всегда общее с host.

Что Docker создаёт по умолчанию

При обычном docker run создаются: mount, UTS, IPC, PID, network, cgroup.

Не создаётся: user namespace. Это означает, что root внутри container — это UID 0 и на host. Ограничения накладываются другими механизмами: отбором capabilities (урок 2.5), профилем seccomp и AppArmor. Но UID тот же.

Практическое следствие, которое проявляется в разделе 07: файл, созданный процессом container от root в bind mount, будет принадлежать root на host.

Включить user namespace можно двумя способами: userns-remap на уровне daemon или rootless mode. Оба разбираются в разделе 12.

Time namespace также не используется.

Как namespaces идентифицируются

Каждый namespace представлен файлом в /proc/<pid>/ns/:

text
/proc/1234/ns/
├── cgroup -> 'cgroup:[4026531835]'
├── ipc    -> 'ipc:[4026531839]'
├── mnt    -> 'mnt:[4026531841]'
├── net    -> 'net:[4026531840]'
├── pid    -> 'pid:[4026531836]'
├── user   -> 'user:[4026531837]'
└── uts    -> 'uts:[4026531838]'

Число в скобках — номер inode. Он служит идентификатором namespace. Два процесса находятся в одном namespace тогда и только тогда, когда номера inode совпадают. Это даёт простой способ проверки принадлежности.

Namespace существует, пока на него есть ссылка: работающий процесс, открытый файловый дескриптор или bind mount файла из /proc/<pid>/ns/. Когда последняя ссылка исчезает, ядро уничтожает namespace.


Внутренний механизм

Три способа работы с namespaces

Системный вызовДействие
clone() с флагами CLONE_NEW*Создать новый процесс сразу в новых namespaces
unshare() с теми же флагамиОтделить текущий процесс в новые namespaces
setns()Переместить текущий процесс в существующий namespace

Container runtime использует первый: runc вызывает clone() с нужным набором флагов, порождая процесс уже изолированным.

Утилита nsenter использует третий: открывает файл /proc/<pid>/ns/<type>, передаёт дескриптор в setns() и затем запускает команду.

Особенность PID namespace

PID namespaces образуют дерево. Процесс имеет PID в своём namespace и во всех родительских, вплоть до initial.

Из этого следует асимметрия видимости: родительский namespace видит процессы дочернего, дочерний родительский — нет. Поэтому с host вы видите процессы всех containers, а изнутри container — только свои.

Вторая особенность: смена PID namespace для уже работающего процесса невозможна. setns() с PID namespace влияет только на дочерние процессы, которые будут созданы после вызова. Причина в том, что PID процесса не может измениться на лету — это сломало бы все ссылки на него.

Практическое следствие для nsenter: вход в PID namespace container требует запуска новой команды, а не «перемещения» текущей оболочки.


Команды и примеры

Просмотр namespaces процесса

bash
ls -l /proc/self/ns/
text
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 net -> 'net:[4026531840]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 time -> 'time:[4026531834]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 user -> 'user:[4026531837]'
lrwxrwxrwx 1 evg evg 0 Jul 29 14:22 uts -> 'uts:[4026531838]'

/proc/self — ссылка на каталог текущего процесса, удобно для интерактивной работы.

Сравнение namespaces host и container

bash
docker run -d --name ns-demo alpine sleep 3600
CPID="$(docker inspect ns-demo --format '{{.State.Pid}}')"

echo "=== host ==="
sudo readlink /proc/1/ns/{pid,net,mnt,uts,ipc,user,cgroup}

echo "=== container ==="
sudo readlink /proc/"$CPID"/ns/{pid,net,mnt,uts,ipc,user,cgroup}
text
=== host ===
pid:[4026531836]
net:[4026531840]
mnt:[4026531841]
uts:[4026531838]
ipc:[4026531839]
user:[4026531837]
cgroup:[4026531835]
=== container ===
pid:[4026532412]
net:[4026532415]
mnt:[4026532410]
uts:[4026532411]
ipc:[4026532413]
user:[4026531837]
cgroup:[4026532473]

Разберём результат:

ТипСовпадает?Вывод
pid, net, mnt, uts, ipc, cgroupНетDocker создал новые namespaces
userДаUser namespace не созданroot в container это root на host

Последняя строка — самое важное наблюдение урока. Она объясняет и проблемы с правами доступа в разделе 07, и модель угроз в разделе 12.

Утилита lsns

bash
sudo lsns -p "$CPID"
text
        NS TYPE   NPROCS   PID USER COMMAND
4026531834 time      412     1 root /sbin/init
4026531837 user      412     1 root /sbin/init
4026532410 mnt         1 188104 root sleep 3600
4026532411 uts         1 188104 root sleep 3600
4026532412 pid         1 188104 root sleep 3600
4026532413 ipc         1 188104 root sleep 3600
4026532415 net         1 188104 root sleep 3600
4026532473 cgroup      1 188104 root sleep 3600

Колонка NPROCS показывает число процессов в namespace. Для time и user — 412 (весь host), для остальных — 1 (только наш container). Наглядное подтверждение того, какие namespaces созданы, а какие разделяются.

Все namespaces системы:

bash
sudo lsns

Только сетевые:

bash
sudo lsns -t net

Вход в namespace container

bash
sudo nsenter -t "$CPID" -n ip addr show

Разбор флагов:

ФлагЗначение
-t PIDЦелевой процесс, в namespaces которого входим
-nNetwork namespace
-mMount namespace
-pPID namespace
-uUTS namespace
-iIPC namespace
-aВсе namespaces сразу
text
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    inet 127.0.0.1/8 scope host lo
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP 
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0

Мы увидели сетевые интерфейсы container, не заходя в него. Это принципиально важно для диагностики: работает даже тогда, когда в образе нет ни оболочки, ни ip, ни ping — используются утилиты host.

Приём подробно применяется в разделе 13.

Проверка сокетов container утилитами host:

bash
sudo nsenter -t "$CPID" -n ss -tlnp

Вход в mount namespace — увидеть файловую систему container:

bash
sudo nsenter -t "$CPID" -m ls /
text
bin    dev    etc    home   lib    media  mnt    opt    proc   root
run    sbin   srv    sys    tmp    usr    var

Создание namespace без Docker

Namespaces — механизм ядра, Docker к нему лишь обращается. Убедимся напрямую.

UTS namespace:

bash
sudo unshare --uts sh -c 'hostname isolated-host && hostname'
hostname
text
isolated-host
my-laptop

Внутри нового UTS namespace имя хоста изменено; на host оно прежнее.

PID namespace:

bash
sudo unshare --pid --fork --mount-proc sh -c 'echo "PID: $$"; ps -e'
text
PID: 1
    PID TTY          TIME CMD
      1 pts/2    00:00:00 sh
      3 pts/2    00:00:00 ps

Оболочка получила PID 1 и видит только свои процессы. Разбор флагов:

  • --pid — создать PID namespace;
  • --fork — обязательно: сам unshare не может сменить свой PID, поэтому порождает дочерний процесс, который и становится PID 1;
  • --mount-proc — перемонтировать /proc в новом mount namespace, иначе ps будет читать /proc host и показывать все процессы.

Забытый --mount-proc — классическая ошибка: PID namespace создан, но ps показывает процессы host, и кажется, что изоляция не работает.

Network namespace:

bash
sudo unshare --net ip addr show
text
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Новый сетевой namespace содержит только lo, и тот выключен. Именно с такого состояния Docker начинает настройку сети container — добавляет veth-интерфейс и адрес. Разбирается в разделе 08.

Комбинация:

bash
sudo unshare --uts --pid --fork --mount-proc --net \
    sh -c 'hostname mini-container; echo "host: $(hostname), PID: $$"; ip -o link show'

Это уже очень близко к тому, что делает container runtime, — не хватает только смены корня и cgroup.

Разделение namespaces между containers

bash
docker run -d --name first alpine sleep 3600
docker run -d --name second --network=container:first alpine sleep 3600

P1="$(docker inspect first --format '{{.State.Pid}}')"
P2="$(docker inspect second --format '{{.State.Pid}}')"

sudo readlink /proc/"$P1"/ns/net
sudo readlink /proc/"$P2"/ns/net
text
net:[4026532415]
net:[4026532415]

Номера совпали: два container разделяют один network namespace. Это тот же механизм, на котором построены pod в Kubernetes — см. раздел 18.

Проверим:

bash
docker exec second ip addr show eth0 | grep inet
docker exec first ip addr show eth0 | grep inet

Одинаковые адреса — сетевой стек общий.

Уборка

bash
docker rm -f ns-demo first second

Практическое упражнение

Задание. Напишите скрипт ns-report.sh <container>, который для указанного container выводит таблицу всех namespaces с указанием: тип, номер inode, совпадает ли с host, число процессов в этом namespace.

Затем на основании таблицы письменно ответьте: какие namespaces Docker создал, а какие разделяются с host, и почему это важно.

Подсказки

Подсказка 1

Номер namespace: sudo readlink /proc/<pid>/ns/<type>. Для host используйте /proc/1/ns/<type>.

Подсказка 2

Число процессов удобно взять из lsns:

bash
sudo lsns -t net -o NS,NPROCS --noheadings

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# ns-report.sh — отчёт о namespaces container.
set -euo pipefail

NAME="${1:?Использование: $0 <container>}"
CPID="$(docker inspect "$NAME" --format '{{.State.Pid}}')"

if [ "$CPID" = "0" ]; then
    echo "Container $NAME не запущен." >&2
    exit 1
fi

printf '%-8s %-14s %-14s %-10s %s\n' ТИП HOST CONTAINER СОВПАДАЕТ ПРОЦЕССОВ
printf '%s\n' "----------------------------------------------------------------"

for t in mnt uts ipc pid net user cgroup time; do
    host_ns="$(sudo readlink "/proc/1/ns/$t" 2>/dev/null || echo '-')"
    cont_ns="$(sudo readlink "/proc/$CPID/ns/$t" 2>/dev/null || echo '-')"
    [ "$host_ns" = "-" ] && continue

    if [ "$host_ns" = "$cont_ns" ]; then
        same="ДА"
    else
        same="нет"
    fi

    inode="${cont_ns//[^0-9]/}"
    nprocs="$(sudo lsns -t "$t" -o NS,NPROCS --noheadings 2>/dev/null \
              | awk -v n="$inode" '$1==n {print $2}')"
    : "${nprocs:=?}"

    printf '%-8s %-14s %-14s %-10s %s\n' \
        "$t" "${host_ns//[^0-9]/}" "$inode" "$same" "$nprocs"
done

echo
echo "СОВПАДАЕТ=ДА означает, что namespace разделяется с host —"
echo "изоляции по этому виду ресурсов нет."

Запуск:

bash
docker run -d --name nstest alpine sleep 300
chmod +x ns-report.sh
./ns-report.sh nstest
docker rm -f nstest

Ожидаемый вывод:

text
ТИП      HOST           CONTAINER      СОВПАДАЕТ  ПРОЦЕССОВ
----------------------------------------------------------------
mnt      4026531841     4026532410     нет        1
uts      4026531838     4026532411     нет        1
ipc      4026531839     4026532413     нет        1
pid      4026531836     4026532412     нет        1
net      4026531840     4026532415     нет        1
user     4026531837     4026531837     ДА         412
cgroup   4026531835     4026532473     нет        1
time     4026531834     4026531834     ДА         412

СОВПАДАЕТ=ДА означает, что namespace разделяется с host —
изоляции по этому виду ресурсов нет.

Ответ на вопрос задания.

Docker создал шесть namespaces: mount, UTS, IPC, PID, network, cgroup. Два разделяются с host: user и time.

Разделение user namespace — практически значимо. Оно означает, что UID 0 внутри container это UID 0 на host. Отсюда следуют два наблюдаемых эффекта:

  1. Файлы, созданные процессом container в bind mount, принадлежат root на host, и обычный пользователь не может их удалить (раздел 07).
  2. Если процесс выйдет за пределы изоляции, он окажется на host под UID 0. Ограничения задаются не namespace, а capabilities, seccomp и AppArmor (раздел 12).

Разделение time namespace означает, что часы у container и host общие. На практике это обычно желательно.

Проверка результата

bash
docker run -d --name check alpine sleep 60
CPID=$(docker inspect check --format '{{.State.Pid}}')
sudo readlink /proc/$CPID/ns/user
sudo readlink /proc/1/ns/user
docker rm -f check

Оба значения должны совпасть — это подтверждает, что user namespace не создан.

Типичные ошибки

ОшибкаПричинаИсправление
unshare --pid без --forkСам unshare не может сменить свой PIDДобавить --fork
unshare --pid --fork без --mount-proc/proc остался от host, ps показывает его процессыДобавить --mount-proc
Ожидание, что root в container не является root на hostUser namespace по умолчанию не создаётсяИспользовать userns-remap или rootless mode
Попытка войти в PID namespace без запуска новой командыPID работающего процесса нельзя изменитьnsenter запускает новую команду в целевом namespace
Диагностика сети container путём установки ip/ping в образРаздувает образ, нарушает минимальностьnsenter -t <pid> -n с host, инструменты host
Вывод об отсутствии изоляции по «одинаковому» psЗабыт --mount-proc при ручном экспериментеПроверить, что /proc перемонтирован
Ожидание, что --ipc=host безопасенОтключает изоляцию System V IPC между container и hostПрименять только при явной необходимости и с пониманием риска

Контрольные вопросы

На понимание:

  1. Как по номеру inode определить, находятся ли два процесса в одном namespace?
  2. Почему PID namespace — единственный с иерархией?
  3. Почему Docker по умолчанию не создаёт user namespace, и какие два следствия это даёт?
  4. Что произойдёт с namespace, когда завершится последний процесс в нём?
  5. Почему три container могут одновременно слушать порт 80?

На применение:

  1. Как посмотреть сетевые интерфейсы container, если в его образе нет утилиты ip?
  2. Как проверить, что два container разделяют один network namespace?
  3. Как создать процесс с изолированным списком процессов, не используя Docker?

На диагностику:

  1. Файл, созданный приложением в container, нельзя удалить с host обычным пользователем. Какой namespace за это отвечает и почему?
  2. В экспериментальном PID namespace команда ps показывает все процессы host. Что забыто?

Краткое резюме

  1. Namespace изолирует представление процесса об одном виде системных ресурсов.
  2. Восемь типов: mount, UTS, IPC, PID, network, user, cgroup, time.
  3. Docker по умолчанию создаёт шесть; user и time разделяются с host.
  4. Отсутствие user namespace означает, что root в container — это root на host.
  5. Namespaces идентифицируются номерами inode в /proc/<pid>/ns/.
  6. Совпадение номеров означает принадлежность одному namespace.
  7. PID namespaces образуют дерево: родитель видит процессы потомка, но не наоборот.
  8. nsenter позволяет войти в namespace container с host, используя инструменты host.
  9. unshare создаёт namespaces без Docker — механизм принадлежит ядру, а не Docker.
  10. Разделение namespace между containers (--network=container:name) — механизм, лежащий в основе pod в Kubernetes.

Официальные источники

ИсточникСсылкаЧто подтверждает
namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/namespaces.7.htmlПолный список типов namespaces, флаги CLONE_NEW*, файлы в /proc/<pid>/ns/
pid_namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/pid_namespaces.7.htmlИерархия PID namespaces, особый статус PID 1
user_namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/user_namespaces.7.htmlОтображение UID и GID, capabilities внутри namespace
network_namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/network_namespaces.7.htmlЧто входит в сетевой namespace
mount_namespaces(7) man pagehttps://man7.org/linux/man-pages/man7/mount_namespaces.7.htmlДерево монтирований и mount propagation
unshare(1) man pagehttps://man7.org/linux/man-pages/man1/unshare.1.htmlФлаги --fork, --mount-proc и их назначение
nsenter(1) man pagehttps://man7.org/linux/man-pages/man1/nsenter.1.htmlВход в namespaces другого процесса
lsns(8) man pagehttps://man7.org/linux/man-pages/man8/lsns.8.htmlПросмотр namespaces системы
Isolate containers with a user namespacehttps://docs.docker.com/engine/security/userns-remap/Включение user namespace в Docker

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Cgroups
Главное оглавление

Markdown на GitHub ↗