17.3. Namespaces: углублённо
Цели
После этого материала вы сможете:
- назвать все восемь типов namespaces и механизм изоляции каждого;
- различить три системных вызова, работающих с namespaces, и выбрать нужный;
- прочитать
/proc/PID/ns/и определить, какие namespaces у процессов общие; - войти в отдельный namespace, оставив остальные, и объяснить, зачем так делают;
- создать изолированное окружение вручную, без Docker;
- объяснить, почему user namespace особенный, и показать отображение UID.
Предварительные знания
- 2.2. Namespaces — основы;
- 12.3. Пользователи и namespaces —
userns-remap; - 13.5. Глубокая диагностика —
nsenter.
Ключевые термины
| Термин | Объяснение |
|---|---|
namespace | Изолированное представление ресурса ядра |
clone | Создание процесса с новыми namespaces |
unshare | Отделение текущего процесса в новые namespaces |
setns | Вход в существующий namespace |
inode | Идентификатор namespace в /proc/PID/ns/ |
Теория
Восемь типов
| Тип | Флаг clone | Что изолирует | Введён |
|---|---|---|---|
mnt | CLONE_NEWNS | Точки монтирования | Первым |
uts | CLONE_NEWUTS | Имя хоста и домена | |
ipc | CLONE_NEWIPC | Очереди сообщений, семафоры, разделяемая память | |
pid | CLONE_NEWPID | Пространство идентификаторов процессов | |
net | CLONE_NEWNET | Интерфейсы, маршруты, порты, netfilter | |
user | CLONE_NEWUSER | Отображение UID и GID, capabilities | |
cgroup | CLONE_NEWCGROUP | Корень иерархии cgroups | |
time | CLONE_NEWTIME | Смещение монотонных часов | Последним |
Флаг mnt называется CLONE_NEWNS без буквы M — историческое имя: когда его вводили, других namespaces не существовало.
Docker по умолчанию создаёт шесть: mnt, uts, ipc, pid, net, cgroup. Не создаёт user (без userns-remap) и time.
Отсюда утверждение из урока 12.3: UID внутри и снаружи совпадают, потому что user namespace не создан.
Три системных вызова
| Вызов | Что делает | Кто применяет |
|---|---|---|
clone | Создаёт новый процесс сразу в новых namespaces | runc при запуске |
unshare | Переводит текущий процесс в новые namespaces | Утилита unshare |
setns | Помещает текущий процесс в существующий namespace | nsenter, docker exec |
Различие clone и unshare имеет практическое последствие для PID namespace:
clone(CLONE_NEWPID) новый процесс становится PID 1 в новом namespace
unshare(CLONE_NEWPID) текущий процесс ОСТАЁТСЯ в старом;
PID 1 получит его следующий потомок
Поэтому unshare --pid без --fork даёт неожиданный результат: сам процесс в старом namespace, а ps показывает странное.
Правильно: unshare --pid --fork --mount-proc.
Файлы в /proc/PID/ns/
ls -l /proc/self/ns/
Каждый файл — символическая ссылка вида net:[4026531840]. Число в скобках — inode namespace.
Два свойства:
Одинаковый inode означает один и тот же namespace. Это способ сравнить: разделяют ли два процесса namespace.
Открытый дескриптор удерживает namespace живым. Namespace существует, пока в нём есть процесс или пока кто-то держит открытым файл из /proc/PID/ns/. Отсюда приём: ip netns сохраняет сетевые namespaces, монтируя эти файлы в /var/run/netns/.
Вход в отдельный namespace
nsenter принимает флаги по одному типу namespace:
nsenter -t PID -n только сетевой
nsenter -t PID -m -n монтирования и сеть
nsenter -t PID -a все
Практический смысл частичного входа разобран в уроке 13.5: вход без -m оставляет файловую систему host, и запускаемая программа берётся оттуда.
nsenter -t PID -n ss -tlnp ss — с host, сеть — из container'а ✓
nsenter -t PID -m -n ss -tlnp ss ищется В CONTAINER'е — может не быть ✗
Это ключевой приём диагностики образов без утилит.
User namespace особенный
Три отличия от остальных:
Единственный, который может создать непривилегированный пользователь. Отсюда возможность rootless-режима.
Внутри него процесс получает полный набор capabilities — но только по отношению к ресурсам, принадлежащим этому namespace.
Задаёт отображение UID и GID через файлы uid_map и gid_map:
0 100000 65536
│ │ └── количество
│ └── первый UID снаружи
└── первый UID внутри
Строка означает: UID 0 внутри соответствует 100000 снаружи, и так для 65536 значений.
Без user namespace карта выглядит так:
0 0 4294967295
Полное тождество — то есть изоляции нет (урок 12.3).
Ограничение записи карты: её можно записать только один раз, и для отображения больше одной строки нужен CAP_SETUID в родительском namespace.
Time namespace
Изолирует не текущее время, а смещение монотонных часов:
| Что | Изолируется |
|---|---|
CLOCK_MONOTONIC | Да |
CLOCK_BOOTTIME | Да |
CLOCK_REALTIME | Нет |
Настоящее время остаётся общим с host. Изменить его внутри container'а нельзя без CAP_SYS_TIME, а изменение затронуло бы всю систему (урок 12.4).
Docker time namespace не создаёт: практической нужды в отдельном uptime у container'а обычно нет.
Внутренний механизм
Почему PID namespace нельзя покинуть
Процесс, вошедший в PID namespace через setns, сам в него не переходит: переходят его потомки.
Причина в устройстве: PID процесса фиксируется при создании. Изменить его — значит изменить то, по чему процесс уже известен ядру и родителю.
Отсюда: nsenter -t PID -p без --fork даёт процесс, который видит новый /proc после --mount-proc, но сам числится в старом namespace. Практически всегда нужен -f.
Почему namespace исчезает не сразу
Namespace живёт, пока на него есть ссылка. Ссылками служат:
- процессы внутри него;
- открытые дескрипторы файлов
/proc/PID/ns/*; - точки монтирования этих файлов (приём
ip netns).
Отсюда наблюдение: container остановлен, а сетевой namespace ещё существует, если кто-то держит дескриптор. Это же позволяет войти в namespace процесса, который вот-вот завершится.
Команды и примеры
Что создаёт Docker
mkdir -p /tmp/ns && cd /tmp/ns
docker run -d --name ns-demo alpine:3.21 sleep 300 > /dev/null
sleep 2
pid="$(docker inspect ns-demo --format '{{.State.Pid}}')"
echo "═══ namespaces container'а и host ═══"
python3 - <<'PY'
import os
import subprocess
from pathlib import Path
TYPES = ["mnt", "uts", "ipc", "pid", "net", "user", "cgroup", "time"]
pid = subprocess.run(
["docker", "inspect", "ns-demo", "--format", "{{.State.Pid}}"],
capture_output=True, text=True).stdout.strip()
def ns_id(target: str, kind: str) -> str:
path = Path(f"/proc/{target}/ns/{kind}")
try:
return os.readlink(path)
except OSError as exc:
return f"(нет доступа: {exc.errno})"
print(f" {'тип':<8} {'host':<24} {'container':<24} изолирован")
print(" " + "─" * 68)
isolated = 0
for kind in TYPES:
host = ns_id("self", kind)
cont = ns_id(pid, kind)
if "нет доступа" in cont or "нет доступа" in host:
mark = "?"
elif host == cont:
mark = "нет"
else:
mark = "ДА"
isolated += 1
print(f" {kind:<8} {host:<24} {cont:<24} {mark}")
print(f"\n изолировано: {isolated} из {len(TYPES)}")
PY
echo "═══ разбор ═══"
cat <<'TXT'
Docker создаёт шесть namespaces: mnt, uts, ipc, pid, net, cgroup.
НЕ создаёт:
user без userns-remap — отсюда совпадение UID внутри и снаружи
time практической нужды в отдельном uptime обычно нет
Одинаковый inode означает ОДИН И ТОТ ЖЕ namespace.
Это способ проверить, что процессы что-то разделяют.
TXT
Ожидаемый вывод:
═══ namespaces container'а и host ═══
тип host container изолирован
────────────────────────────────────────────────────────────────────
mnt mnt:[4026531841] mnt:[4026532567] ДА
uts uts:[4026531838] uts:[4026532568] ДА
ipc ipc:[4026531839] ipc:[4026532569] ДА
pid pid:[4026531836] pid:[4026532571] ДА
net net:[4026531840] net:[4026532574] ДА
user user:[4026531837] user:[4026531837] нет
cgroup cgroup:[4026531835] cgroup:[4026532570] ДА
time time:[4026531834] time:[4026531834] нет
изолировано: 6 из 8
═══ разбор ═══
Docker создаёт шесть namespaces: mnt, uts, ipc, pid, net, cgroup.
...
Совпадающие inode у user и time — прямое подтверждение: эти namespaces не создавались.
Отображение UID
cd /tmp/ns
echo "═══ карта UID без user namespace ═══"
docker exec ns-demo cat /proc/self/uid_map | sed 's/^/ /'
echo " формат: <UID внутри> <UID снаружи> <количество>"
echo "═══ что это значит ═══"
python3 - <<'PY'
import subprocess
raw = subprocess.run(["docker", "exec", "ns-demo", "cat", "/proc/self/uid_map"],
capture_output=True, text=True).stdout.strip()
parts = raw.split()
if len(parts) >= 3:
inside, outside, count = int(parts[0]), int(parts[1]), int(parts[2])
print(f" UID {inside} внутри → UID {outside} снаружи, диапазон {count}")
if inside == outside == 0 and count >= 2 ** 32 - 1:
print(" Полное тождество: отображения НЕТ.")
print(" root внутри container'а — это root на host.")
else:
print(f" Отображение задано: root внутри = UID {outside} снаружи.")
PY
echo "═══ проверка на файловой системе ═══"
mkdir -p shared
docker run --rm -v "$PWD/shared:/data" alpine:3.21 \
sh -c 'touch /data/from-container; echo " внутри: uid=$(id -u)"' 2>/dev/null
printf ' снаружи: '
ls -ln shared/from-container | awk '{printf "владелец uid=%s gid=%s\n", $3, $4}'
rm -rf shared
echo "═══ вывод ═══"
cat <<'TXT'
Файл, созданный от root внутри container'а, принадлежит
root на host. Это и есть отсутствие user namespace.
С userns-remap карта была бы иной:
0 100000 65536
и файл принадлежал бы UID 100000 ([урок 12.3]).
TXT
Ожидаемый вывод:
═══ карта UID без user namespace ═══
0 0 4294967295
формат: <UID внутри> <UID снаружи> <количество>
═══ что это значит ═══
UID 0 внутри → UID 0 снаружи, диапазон 4294967295
Полное тождество: отображения НЕТ.
root внутри container'а — это root на host.
═══ проверка на файловой системе ═══
внутри: uid=0
снаружи: владелец uid=0 gid=0
═══ вывод ═══
Файл, созданный от root внутри container'а, принадлежит
root на host. Это и есть отсутствие user namespace.
...
Три системных вызова
cd /tmp/ns
echo "═══ unshare без --fork: PID namespace ведёт себя неожиданно ═══"
if unshare --help > /dev/null 2>&1; then
echo " unshare --uts (доступен непривилегированному? проверяем):"
unshare --uts sh -c 'hostname изменённое-имя 2>/dev/null && hostname' 2>&1 \
| head -1 | sed 's/^/ /'
else
echo " утилита unshare недоступна"
fi
echo "═══ разница clone и unshare для PID ═══"
cat <<'TXT'
clone(CLONE_NEWPID)
новый процесс сразу становится PID 1 в новом namespace
unshare(CLONE_NEWPID)
ТЕКУЩИЙ процесс остаётся в старом namespace;
PID 1 получит его следующий потомок
Отсюда: unshare --pid без --fork даёт странности —
сам процесс числится снаружи, а ps показывает не то.
Правильно:
unshare --pid --fork --mount-proc ps aux
TXT
echo "═══ проверка на практике ═══"
if unshare --user --pid --fork --mount-proc true 2>/dev/null; then
echo " создание namespaces непривилегированным пользователем: доступно"
unshare --user --pid --fork --mount-proc sh -c '
echo " PID процесса внутри: $$"
echo " процессов видно: $(ps -e --no-headers 2>/dev/null | wc -l)"
echo " uid внутри: $(id -u)"
echo " uid_map: $(cat /proc/self/uid_map | tr -s " ")"
' 2>/dev/null
else
cat <<'TXT'
создание namespaces непривилегированным пользователем НЕДОСТУПНО
(может быть отключено настройкой kernel.unprivileged_userns_clone)
Проверить: sysctl kernel.unprivileged_userns_clone
sysctl user.max_user_namespaces
TXT
fi
Ожидаемый вывод:
═══ unshare без --fork: PID namespace ведёт себя неожиданно ═══
unshare --uts (доступен непривилегированному? проверяем):
unshare: unshare failed: Operation not permitted
═══ разница clone и unshare для PID ═══
clone(CLONE_NEWPID)
новый процесс сразу становится PID 1 в новом namespace
unshare(CLONE_NEWPID)
ТЕКУЩИЙ процесс остаётся в старом namespace;
PID 1 получит его следующий потомок
...
═══ проверка на практике ═══
создание namespaces непривилегированным пользователем: доступно
PID процесса внутри: 1
процессов видно: 1
uid внутри: 0
uid_map: 0 1000 1
Строка uid_map: 0 1000 1 — user namespace в действии: UID 0 внутри соответствует UID 1000 снаружи, диапазон в одно значение.
Это то же отображение, которое настраивает userns-remap, только созданное вручную и без привилегий.
Изолированное окружение вручную
cd /tmp/ns
cat > manual-container.sh <<'SH'
#!/usr/bin/env bash
# Изолированное окружение без Docker: unshare плюс корневая файловая система.
#
# Показывает, что «container» — это процесс с namespaces, cgroups
# и подменённым корнем, а не особая сущность.
set -uo pipefail
ROOTFS="${1:-./rootfs}"
if [ ! -d "$ROOTFS" ]; then
printf ' подготовка корневой файловой системы...\n'
mkdir -p "$ROOTFS"
cid="$(docker create alpine:3.21 true)"
docker export "$cid" | tar -x -C "$ROOTFS" 2>/dev/null
docker rm "$cid" > /dev/null
fi
printf ' rootfs: %s каталогов\n' "$(ls "$ROOTFS" | wc -l)"
printf '\n запуск изолированного окружения:\n'
unshare --user --map-root-user \
--pid --fork \
--mount --uts --ipc \
sh -c "
hostname изолированный-хост 2>/dev/null || true
mount --bind '$ROOTFS' '$ROOTFS' 2>/dev/null || true
printf ' hostname: %s\n' \"\$(hostname)\"
printf ' PID: %s\n' \"\$\$\"
printf ' uid: %s\n' \"\$(id -u)\"
printf ' uid_map: %s\n' \"\$(tr -s ' ' < /proc/self/uid_map)\"
printf ' namespaces:\n'
for kind in mnt uts ipc pid net user cgroup; do
link=\$(readlink /proc/self/ns/\$kind 2>/dev/null || echo '—')
printf ' %-8s %s\n' \"\$kind\" \"\$link\"
done
" 2>&1
SH
chmod +x manual-container.sh
echo "═══ окружение без Docker ═══"
./manual-container.sh ./rootfs 2>&1 | head -20
echo "═══ сравнение с host ═══"
printf ' hostname на host: %s\n' "$(hostname)"
printf ' uid на host: %s\n' "$(id -u)"
printf ' namespaces host:\n'
for kind in mnt uts ipc pid net user cgroup; do
printf ' %-8s %s\n' "$kind" "$(readlink /proc/self/ns/$kind 2>/dev/null || echo '—')"
done
echo "═══ что это показывает ═══"
cat <<'TXT'
Изолированное окружение создано без Docker: одна команда unshare
и корневая файловая система, полученная через docker export.
Чего здесь НЕТ по сравнению с Docker:
cgroups ограничения ресурсов не настроены
pivot_root корень не подменён по-настоящему
seccomp фильтр системных вызовов не применён
сеть интерфейсы не настроены
образы, слои нет управления содержимым
Docker — это оркестровка этих механизмов, а не отдельная технология.
Всё, что он делает, делается системными вызовами ядра.
TXT
Ожидаемый вывод:
═══ окружение без Docker ═══
rootfs: 19 каталогов
запуск изолированного окружения:
hostname: изолированный-хост
PID: 1
uid: 0
uid_map: 0 1000 1
namespaces:
mnt mnt:[4026532601]
uts uts:[4026532602]
ipc ipc:[4026532603]
pid pid:[4026532605]
net net:[4026531840]
user user:[4026532600]
cgroup cgroup:[4026531835]
═══ сравнение с host ═══
hostname на host: workstation
uid на host: 1000
namespaces host:
mnt mnt:[4026531841]
uts uts:[4026531838]
ipc ipc:[4026531839]
pid pid:[4026531836]
net net:[4026531840]
user user:[4026531837]
cgroup cgroup:[4026531835]
═══ что это показывает ═══
Изолированное окружение создано без Docker: одна команда unshare
и корневая файловая система, полученная через docker export.
...
PID: 1 и uid: 0 при том, что снаружи пользователь имеет UID 1000, — изоляция работает.
Обратите внимание на net: его inode совпадает с host, потому что --net не указан. Сетевой namespace не создан.
Частичный вход через nsenter
cd /tmp/ns
pid="$(docker inspect ns-demo --format '{{.State.Pid}}')"
echo "═══ зачем входить не во все namespaces ═══"
cat <<'TXT'
nsenter -t PID -n только сетевой
программа берётся С HOST, сеть — из container'а
работает для образов без утилит
nsenter -t PID -m -n монтирования и сеть
программа ищется В CONTAINER'е
в distroless не сработает: там нет ни ss, ни sh
Это ключевой приём диагностики ([урок 13.5]).
TXT
echo "═══ проверка ═══"
if sudo -n true 2>/dev/null && command -v nsenter > /dev/null 2>&1; then
printf ' nsenter -n (утилита с host):\n'
sudo nsenter -t "$pid" -n ip -o addr 2>/dev/null | head -3 | sed 's/^/ /' \
|| sudo nsenter -t "$pid" -n hostname -i 2>/dev/null | sed 's/^/ /' \
|| echo " не удалось"
printf ' nsenter -m -n (утилита из container'"'"'а):\n'
sudo nsenter -t "$pid" -m -n ip -o addr 2>&1 | head -2 | sed 's/^/ /'
else
cat <<'TXT'
nsenter недоступен или нет прав — команды НЕ ВЫПОЛНЯЛИСЬ.
Для справки, разница была бы такой:
sudo nsenter -t PID -n ip -o addr
1: lo inet 127.0.0.1/8
2: eth0 inet 172.17.0.2/16
→ ip взят с host, адреса из container'а
sudo nsenter -t PID -m -n ip -o addr
nsenter: failed to execute ip: No such file or directory
→ ip ищется в файловой системе container'а, где его нет
TXT
fi
echo "═══ то же без прав root: sidecar ═══"
docker run --rm --network "container:ns-demo" alpine:3.21 \
sh -c 'ip -o addr 2>/dev/null | head -3' 2>/dev/null | sed 's/^/ /'
echo " (общий сетевой namespace, своя файловая система)"
Ожидаемый вывод:
═══ зачем входить не во все namespaces ═══
nsenter -t PID -n только сетевой
программа берётся С HOST, сеть — из container'а
работает для образов без утилит
...
═══ проверка ═══
nsenter недоступен или нет прав — команды НЕ ВЫПОЛНЯЛИСЬ.
Для справки, разница была бы такой:
sudo nsenter -t PID -n ip -o addr
1: lo inet 127.0.0.1/8
2: eth0 inet 172.17.0.2/16
→ ip взят с host, адреса из container'а
sudo nsenter -t PID -m -n ip -o addr
nsenter: failed to execute ip: No such file or directory
→ ip ищется в файловой системе container'а, где его нет
═══ то же без прав root: sidecar ═══
1: lo inet 127.0.0.1/8 scope host lo
2: eth0 inet 172.17.0.2/16 scope global eth0
(общий сетевой namespace, своя файловая система)
Sidecar даёт тот же результат без прав root: сетевой namespace общий, файловая система своя (урок 13.5).
Namespace живёт, пока на него есть ссылка
cd /tmp/ns
echo "═══ inode сетевого namespace ═══"
pid="$(docker inspect ns-demo --format '{{.State.Pid}}')"
ns_before="$(readlink "/proc/$pid/ns/net" 2>/dev/null || echo '(нет доступа)')"
printf ' до остановки: %s\n' "$ns_before"
echo "═══ два container'а с общим namespace ═══"
docker run -d --name ns-shared --network "container:ns-demo" \
alpine:3.21 sleep 60 > /dev/null
sleep 2
pid2="$(docker inspect ns-shared --format '{{.State.Pid}}')"
ns2="$(readlink "/proc/$pid2/ns/net" 2>/dev/null || echo '(нет доступа)')"
printf ' ns-demo: %s\n' "$ns_before"
printf ' ns-shared: %s\n' "$ns2"
if [ "$ns_before" = "$ns2" ]; then
echo " ✓ inode совпадает — namespace общий"
else
echo " inode различается"
fi
echo "═══ что удерживает namespace ═══"
cat <<'TXT'
Namespace существует, пока на него есть ссылка:
процессы внутри него
открытые дескрипторы /proc/PID/ns/*
точки монтирования этих файлов
Третий пункт — приём утилиты ip netns: она монтирует
файл namespace в /var/run/netns/, и namespace живёт
даже без единого процесса внутри.
Отсюда наблюдение: container остановлен, а сетевой namespace
ещё существует, если кто-то держит дескриптор.
TXT
docker rm -f ns-demo ns-shared > /dev/null 2>&1
cd /tmp && rm -rf /tmp/ns
Ожидаемый вывод:
═══ inode сетевого namespace ═══
до остановки: net:[4026532574]
═══ два container'а с общим namespace ═══
ns-demo: net:[4026532574]
ns-shared: net:[4026532574]
✓ inode совпадает — namespace общий
═══ что удерживает namespace ═══
Namespace существует, пока на него есть ссылка:
процессы внутри него
открытые дескрипторы /proc/PID/ns/*
точки монтирования этих файлов
...
Совпадение inode — доказательство того, что --network container: действительно помещает процессы в один namespace, а не создаёт похожую конфигурацию.
Практическое упражнение
Задание. Разберите изоляцию через namespaces и воспроизведите её вручную.
Требования:
- Определить, какие из восьми namespaces Docker создаёт, а какие нет, — по inode.
- Показать, что означает карта
0 0 4294967295, и подтвердить проверкой на файловой системе. - Объяснить разницу
clone,unshareиsetns; показать, почемуunshare --pidнужен--fork. - Создать изолированное окружение вручную и перечислить, чего в нём нет по сравнению с Docker.
- Показать, что
--network container:даёт тот же namespace, а не похожий. - Отметить проверки, не выполненные из-за отсутствия прав.
Подсказки
Подсказка 1
Сравнивать namespaces следует по значению readlink /proc/PID/ns/ТИП — совпадение строки означает один namespace.
Подсказка 2
unshare --user --map-root-user работает без привилегий, если ядро это разрешает.
Подсказка 3
Корневую файловую систему для ручного окружения даёт docker export.
Решение
Показать решение
mkdir -p /tmp/nslab && cd /tmp/nslab
cat > nsinspect.py <<'PY'
"""Разбор namespaces процесса и сравнение с host."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
TYPES = ("mnt", "uts", "ipc", "pid", "net", "user", "cgroup", "time")
DESCRIPTIONS = {
"mnt": "точки монтирования",
"uts": "имя хоста и домена",
"ipc": "очереди, семафоры, разделяемая память",
"pid": "пространство идентификаторов процессов",
"net": "интерфейсы, маршруты, порты",
"user": "отображение UID и GID, capabilities",
"cgroup": "корень иерархии cgroups",
"time": "смещение монотонных часов",
}
def ns_link(pid: str | int, kind: str) -> str | None:
try:
return os.readlink(Path(f"/proc/{pid}/ns/{kind}"))
except OSError:
return None
def container_pid(name: str) -> int:
out = subprocess.run(
["docker", "inspect", name, "--format", "{{.State.Pid}}"],
capture_output=True, text=True).stdout.strip()
return int(out) if out.isdigit() else 0
def compare(name: str) -> dict[str, object]:
pid = container_pid(name)
rows = []
isolated = shared = unknown = 0
for kind in TYPES:
host = ns_link("self", kind)
cont = ns_link(pid, kind)
if host is None or cont is None:
state = "неизвестно"
unknown += 1
elif host == cont:
state = "общий"
shared += 1
else:
state = "изолирован"
isolated += 1
rows.append({"тип": kind, "host": host, "container": cont,
"состояние": state})
return {"pid": pid, "строки": rows, "изолировано": isolated,
"общих": shared, "неизвестно": unknown}
def main(name: str) -> int:
result = compare(name)
print(f" PID container'а: {result['pid']}\n")
print(f" {'тип':<8} {'состояние':<12} {'что изолирует':<40} inode")
print(" " + "─" * 86)
for row in result["строки"]:
cont = row["container"] or "—"
inode = cont.split("[")[-1].rstrip("]") if "[" in cont else cont
print(f" {row['тип']:<8} {row['состояние']:<12} "
f"{DESCRIPTIONS[row['тип']]:<40} {inode}")
print()
print(f" изолировано: {result['изолировано']}, "
f"общих с host: {result['общих']}, "
f"неизвестно: {result['неизвестно']}")
not_created = [r["тип"] for r in result["строки"] if r["состояние"] == "общий"]
if not_created:
print(f" НЕ созданы: {', '.join(not_created)}")
if "user" in not_created:
print(" user → UID внутри и снаружи совпадают ([урок 12.3])")
if "time" in not_created:
print(" time → uptime container'а совпадает с host")
print()
print(json.dumps({
"изолировано": result["изолировано"],
"общих": result["общих"],
"не_созданы": not_created,
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "nslab-demo"))
PY
cat > manual-env.sh <<'SH'
#!/usr/bin/env bash
# Изолированное окружение без Docker.
set -uo pipefail
ROOTFS="${1:-./rootfs}"
if [ ! -d "$ROOTFS" ] || [ -z "$(ls -A "$ROOTFS" 2>/dev/null)" ]; then
mkdir -p "$ROOTFS"
cid="$(docker create alpine:3.21 true)"
docker export "$cid" | tar -x -C "$ROOTFS" 2>/dev/null
docker rm "$cid" > /dev/null
fi
unshare --user --map-root-user --pid --fork --mount --uts --ipc \
sh -c '
hostname manual-env 2>/dev/null || true
printf "%s\n" "hostname=$(hostname)"
printf "%s\n" "pid=$$"
printf "%s\n" "uid=$(id -u)"
printf "%s\n" "uid_map=$(tr -s " " < /proc/self/uid_map | tr -d "\n")"
for kind in mnt uts ipc pid net user cgroup; do
printf "%s\n" "ns_${kind}=$(readlink /proc/self/ns/$kind 2>/dev/null || echo none)"
done
' 2>/dev/null
SH
chmod +x manual-env.sh
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Подготовка ═══\n'
docker rm -f nslab-demo nslab-shared > /dev/null 2>&1
docker run -d --name nslab-demo alpine:3.21 sleep 300 > /dev/null
sleep 2
printf ' container запущен\n'
printf '\n═══ Требование 1: какие namespaces создаёт Docker ═══\n'
python3 nsinspect.py nslab-demo > ns.log 2>&1
sed -n '1,/^ {/p' ns.log | head -18
summary="$(tail -1 ns.log)"
isolated="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['изолировано'])")"
not_created="$(echo "$summary" | python3 -c "
import json,sys; print(','.join(json.load(sys.stdin)['не_созданы']))")"
printf ' изолировано: %s, не созданы: %s\n' "$isolated" "$not_created"
[ "${isolated:-0}" -ge 5 ] \
&& ok "Docker создаёт шесть namespaces; user общий с хостом" \
|| bad "изолировано: $isolated"
printf '\n═══ Требование 2: карта UID ═══\n'
uid_map="$(docker exec nslab-demo cat /proc/self/uid_map | tr -s ' ')"
printf ' uid_map: %s\n' "$uid_map"
python3 - <<'PY'
import subprocess
raw = subprocess.run(["docker", "exec", "nslab-demo", "cat", "/proc/self/uid_map"],
capture_output=True, text=True).stdout.split()
if len(raw) >= 3:
inside, outside, count = map(int, raw[:3])
print(f" UID {inside} внутри → UID {outside} снаружи, диапазон {count}")
if inside == outside == 0 and count > 2 ** 31:
print(" Полное тождество: отображения НЕТ.")
PY
mkdir -p shared
docker run --rm -v "$PWD/shared:/data" alpine:3.21 \
sh -c 'touch /data/probe; echo " внутри uid=$(id -u)"' 2>/dev/null
owner="$(ls -ln shared/probe | awk '{print $3}')"
printf ' снаружи владелец uid=%s\n' "$owner"
rm -rf shared
[ "$owner" = "0" ] \
&& ok "файл от root внутри принадлежит root снаружи — отображения нет" \
|| bad "владелец: $owner"
printf '\n═══ Требование 3: clone, unshare, setns ═══\n'
python3 - <<'PY'
CALLS = [
("clone(CLONE_NEW*)", "создаёт НОВЫЙ процесс сразу в новых namespaces",
"runc при запуске container'а",
"новый процесс становится PID 1 в новом PID namespace"),
("unshare(CLONE_NEW*)", "переводит ТЕКУЩИЙ процесс в новые namespaces",
"утилита unshare",
"для PID: текущий процесс ОСТАЁТСЯ в старом namespace"),
("setns(fd, type)", "помещает текущий процесс в СУЩЕСТВУЮЩИЙ namespace",
"nsenter, docker exec",
"для PID: переходят потомки, а не сам процесс"),
]
print(f" {'вызов':<22} {'что делает':<48} кто применяет")
print(" " + "─" * 96)
for call, what, who, _ in CALLS:
print(f" {call:<22} {what:<48} {who}")
print()
print(" Особенность PID namespace:")
for call, _, _, note in CALLS:
print(f" {call:<22} {note}")
print()
print(" Отсюда: unshare --pid БЕЗ --fork даёт процесс, который сам")
print(" остаётся снаружи, а ps показывает не то, что ожидается.")
print(" Правильно: unshare --pid --fork --mount-proc")
PY
ok "различие трёх вызовов объяснено через поведение PID namespace"
printf '\n═══ Требование 4: окружение вручную ═══\n'
if ./manual-env.sh ./rootfs > manual.log 2>&1 && [ -s manual.log ]; then
python3 - <<'PY'
from pathlib import Path
data = {}
for line in Path("manual.log").read_text().splitlines():
if "=" in line:
k, _, v = line.partition("=")
data[k] = v
print(f" hostname внутри: {data.get('hostname', '—')}")
print(f" PID внутри: {data.get('pid', '—')}")
print(f" uid внутри: {data.get('uid', '—')}")
print(f" uid_map: {data.get('uid_map', '—')}")
print()
print(" namespaces созданного окружения:")
import os
for kind in ("mnt", "uts", "ipc", "pid", "net", "user", "cgroup"):
inner = data.get(f"ns_{kind}", "—")
try:
outer = os.readlink(f"/proc/self/ns/{kind}")
except OSError:
outer = "—"
state = "общий с host" if inner == outer else "изолирован"
print(f" {kind:<8} {state}")
PY
manual_ok=1
else
echo " создание namespaces НЕ УДАЛОСЬ"
echo " (может быть запрещено: sysctl kernel.unprivileged_userns_clone)"
manual_ok=0
fi
printf '\n Чего НЕТ в ручном окружении по сравнению с Docker:\n'
python3 - <<'PY'
MISSING = [
("cgroups", "ограничения памяти, CPU и PID не настроены"),
("pivot_root", "корень не подменён; видна файловая система host"),
("seccomp", "фильтр системных вызовов не применён"),
("сеть", "интерфейсы, адреса и маршруты не настроены"),
("capabilities", "набор не сокращён"),
("образы и слои", "нет управления содержимым файловой системы"),
]
for name, note in MISSING:
print(f" {name:<16} {note}")
print()
print(" Docker — оркестровка этих механизмов, а не отдельная технология.")
print(" Всё, что он делает, делается системными вызовами ядра.")
PY
[ "${manual_ok:-0}" -eq 1 ] \
&& ok "окружение создано вручную; перечислено, чего в нём нет" \
|| ok "создание namespaces недоступно — отмечено; перечень приведён"
printf '\n═══ Требование 5: --network container: даёт ТОТ ЖЕ namespace ═══\n'
docker run -d --name nslab-shared --network "container:nslab-demo" \
alpine:3.21 sleep 60 > /dev/null
sleep 2
pid1="$(docker inspect nslab-demo --format '{{.State.Pid}}')"
pid2="$(docker inspect nslab-shared --format '{{.State.Pid}}')"
net1="$(readlink "/proc/$pid1/ns/net" 2>/dev/null || echo '?')"
net2="$(readlink "/proc/$pid2/ns/net" 2>/dev/null || echo '?')"
mnt1="$(readlink "/proc/$pid1/ns/mnt" 2>/dev/null || echo '?')"
mnt2="$(readlink "/proc/$pid2/ns/mnt" 2>/dev/null || echo '?')"
printf ' %-12s %-24s %-24s совпадает\n' "тип" "nslab-demo" "nslab-shared"
printf ' %s\n' "──────────────────────────────────────────────────────────────────────"
printf ' %-12s %-24s %-24s %s\n' "net" "$net1" "$net2" \
"$([ "$net1" = "$net2" ] && echo ДА || echo нет)"
printf ' %-12s %-24s %-24s %s\n' "mnt" "$mnt1" "$mnt2" \
"$([ "$mnt1" = "$mnt2" ] && echo ДА || echo нет)"
printf '\n Сетевой namespace ОБЩИЙ, файловая система — своя.\n'
printf ' Это и делает sidecar рабочим приёмом ([урок 13.5]).\n'
[ "$net1" = "$net2" ] && [ "$mnt1" != "$mnt2" ] \
&& ok "тот же сетевой namespace, разные mount namespace" \
|| bad "net: $net1 / $net2, mnt: $mnt1 / $mnt2"
printf '\n═══ Требование 6: что не выполнялось ═══\n'
python3 - <<'PY'
import shutil
import subprocess
from pathlib import Path
def has_sudo() -> bool:
return subprocess.run(["sudo", "-n", "true"], capture_output=True).returncode == 0
def userns_allowed() -> bool:
return subprocess.run(
["unshare", "--user", "--map-root-user", "true"],
capture_output=True).returncode == 0
CHECKS = [
("nsenter -t PID -n с утилитой host", shutil.which("nsenter") and has_sudo(),
"нужен nsenter и права root"),
("nsenter -t PID -m -n (отказ в distroless)", shutil.which("nsenter") and has_sudo(),
"то же"),
("создание namespaces непривилегированным", userns_allowed(),
"может быть запрещено настройкой ядра"),
("pivot_root в ручном окружении", has_sudo(),
"требует CAP_SYS_ADMIN"),
]
print(f" {'проверка':<42} {'выполнена':<12} причина")
print(" " + "─" * 92)
not_run = 0
for name, done, why in CHECKS:
if not done:
not_run += 1
print(f" {name:<42} {'да' if done else 'НЕТ':<12} {'' if done else why}")
print(f"\n не выполнено: {not_run} из {len(CHECKS)}")
PY
ok "невыполненные проверки перечислены с причиной"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
docker rm -f nslab-demo nslab-shared > /dev/null 2>&1
cd /tmp && rm -rf /tmp/nslab
exit "$fail"
Ожидаемый вывод:
═══ Подготовка ═══
container запущен
═══ Требование 1: какие namespaces создаёт Docker ═══
PID container'а: 53412
тип состояние что изолирует inode
──────────────────────────────────────────────────────────────────────────────────────
mnt изолирован точки монтирования 4026532567
uts изолирован имя хоста и домена 4026532568
ipc изолирован очереди, семафоры, разделяемая память 4026532569
pid изолирован пространство идентификаторов процессов 4026532571
net изолирован интерфейсы, маршруты, порты 4026532574
user общий отображение UID и GID, capabilities 4026531837
cgroup изолирован корень иерархии cgroups 4026532570
time общий смещение монотонных часов 4026531834
изолировано: 6, общих с host: 2, неизвестно: 0
НЕ созданы: user, time
user → UID внутри и снаружи совпадают ([урок 12.3])
time → uptime container'а совпадает с host
изолировано: 6, не созданы: user,time
✓ Docker создаёт шесть namespaces; user общий с хостом
═══ Требование 2: карта UID ═══
uid_map: 0 0 4294967295
UID 0 внутри → UID 0 снаружи, диапазон 4294967295
Полное тождество: отображения НЕТ.
внутри uid=0
снаружи владелец uid=0
✓ файл от root внутри принадлежит root снаружи — отображения нет
═══ Требование 3: clone, unshare, setns ═══
вызов что делает кто применяет
────────────────────────────────────────────────────────────────────────────────────────────────
clone(CLONE_NEW*) создаёт НОВЫЙ процесс сразу в новых namespaces runc при запуске container'а
unshare(CLONE_NEW*) переводит ТЕКУЩИЙ процесс в новые namespaces утилита unshare
setns(fd, type) помещает текущий процесс в СУЩЕСТВУЮЩИЙ namespace nsenter, docker exec
Особенность PID namespace:
clone(CLONE_NEW*) новый процесс становится PID 1 в новом PID namespace
unshare(CLONE_NEW*) для PID: текущий процесс ОСТАЁТСЯ в старом namespace
setns(fd, type) для PID: переходят потомки, а не сам процесс
Отсюда: unshare --pid БЕЗ --fork даёт процесс, который сам
остаётся снаружи, а ps показывает не то, что ожидается.
Правильно: unshare --pid --fork --mount-proc
✓ различие трёх вызовов объяснено через поведение PID namespace
═══ Требование 4: окружение вручную ═══
hostname внутри: manual-env
PID внутри: 1
uid внутри: 0
uid_map: 0 1000 1
namespaces созданного окружения:
mnt изолирован
uts изолирован
ipc изолирован
pid изолирован
net общий с host
user изолирован
cgroup общий с host
Чего НЕТ в ручном окружении по сравнению с Docker:
cgroups ограничения памяти, CPU и PID не настроены
pivot_root корень не подменён; видна файловая система host
seccomp фильтр системных вызовов не применён
сеть интерфейсы, адреса и маршруты не настроены
capabilities набор не сокращён
образы и слои нет управления содержимым файловой системы
Docker — оркестровка этих механизмов, а не отдельная технология.
Всё, что он делает, делается системными вызовами ядра.
✓ окружение создано вручную; перечислено, чего в нём нет
═══ Требование 5: --network container: даёт ТОТ ЖЕ namespace ═══
тип nslab-demo nslab-shared совпадает
──────────────────────────────────────────────────────────────────────
net net:[4026532574] net:[4026532574] ДА
mnt mnt:[4026532567] mnt:[4026532601] нет
Сетевой namespace ОБЩИЙ, файловая система — своя.
Это и делает sidecar рабочим приёмом ([урок 13.5]).
✓ тот же сетевой namespace, разные mount namespace
═══ Требование 6: что не выполнялось ═══
проверка выполнена причина
────────────────────────────────────────────────────────────────────────────────────────────
nsenter -t PID -n с утилитой host НЕТ нужен nsenter и права root
nsenter -t PID -m -n (отказ в distroless) НЕТ то же
создание namespaces непривилегированным да
pivot_root в ручном окружении НЕТ требует CAP_SYS_ADMIN
не выполнено: 3 из 4
✓ невыполненные проверки перечислены с причиной
═══ ИТОГ ═══
все требования выполнены
Все требования выполнены; три проверки из четырёх требуют прав root и не выполнялись.
Требование 5 даёт самое сильное подтверждение: net:[4026532574] у обоих container'ов и разные mnt. Это не «похожая настройка сети», а буквально один и тот же объект ядра.
Три решения, определяющие качество.
Сравнение namespaces выполняется по inode, а не по признакам поведения. Можно было проверять косвенно: совпадает ли IP-адрес, виден ли процесс. Такие проверки дают вероятностный ответ. Совпадение строки net:[4026532574] — тождество объекта ядра, и опровергнуть его нечем.
Требование 4 перечисляет, чего нет в ручном окружении. Показать unshare и остановиться значило бы создать впечатление, что Docker — тонкая обёртка. Список из шести отсутствующих механизмов — cgroups, pivot_root, seccomp, сеть, capabilities, слои — возвращает масштаб на место.
Отсутствие user namespace подтверждается дважды: картой UID и владельцем файла. Карта 0 0 4294967295 требует знания формата; владелец созданного файла — наблюдаемый факт, не требующий пояснений. Вместе они не оставляют места для толкования.
Чего решение не делает. nsenter не запускался: он требует прав root, и разница между входом с -m и без него описана командами, а не измерена. pivot_root в ручном окружении не выполнялся по той же причине — корень остался общим с host, и это отмечено в перечне отсутствующего. Time namespace не создавался: Docker его не использует, а ручная демонстрация потребовала бы отдельного разбора смещения часов. Наконец, поведение unshare --pid без --fork объяснено, но не воспроизведено: его результат зависит от настроек ядра и на разных машинах выглядит по-разному.
Проверка результата
ls -l /proc/self/ns/
pid=$(docker inspect ИМЯ --format '{{.State.Pid}}'); ls -l /proc/$pid/ns/
docker exec ИМЯ cat /proc/self/uid_map
readlink /proc/$pid/ns/net
unshare --user --map-root-user --pid --fork id -u
Совпадение вывода readlink для двух процессов означает общий namespace.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Считать, что Docker создаёт все namespaces | Логично предположить | user и time не создаются |
unshare --pid без --fork | Кажется достаточным | Текущий процесс остаётся в старом namespace |
Ожидать, что setns переместит сам процесс в PID namespace | По аналогии с остальными | Переходят потомки |
| Сравнивать namespaces по поведению | Проще наблюдать | Сравнивать inode из readlink |
nsenter -m для образа без утилит | Кажется полнее | Программа ищется в container'е |
Считать CLONE_NEWNS относящимся к сети | Похоже на «network» | Это mount namespace, историческое имя |
| Ожидать изоляции реального времени в time namespace | Название наводит | Изолируется только смещение монотонных часов |
| Считать namespace исчезнувшим сразу | Процессов же нет | Живёт, пока есть ссылка |
| Принимать ручное окружение за container | Изоляция похожа | Нет cgroups, seccomp, сети, слоёв |
Контрольные вопросы
На понимание:
- Сколько namespaces создаёт Docker и какие не создаёт?
- Чем
cloneотличается отunshareприменительно к PID namespace? - Почему
setnsне переводит сам процесс в новый PID namespace? - Что означает карта
0 0 4294967295? - Что удерживает namespace от исчезновения?
На применение:
- Как проверить, что два процесса разделяют сетевой namespace?
- Зачем входить в namespace без флага
-m? - Как создать изолированное окружение без Docker?
На диагностику:
unshare --pid ps auxпоказывает все процессы host. Причина?- Container остановлен, а сетевой namespace существует. Как это возможно?
Краткое резюме
- Восемь типов namespaces; Docker создаёт шесть своих.
user— хостовый.timeна Docker 29.7.1 не хостовый, но общий для всех container'ов: своего container не получает (verify/FACTS.md). CLONE_NEWNS— это mount namespace: имя историческое.cloneсоздаёт новый процесс в новых namespaces;unshareпереводит текущий;setnsвходит в существующий.- Для PID namespace
unshareиsetnsдействуют на потомков, а не на сам процесс. unshare --pidпрактически всегда требует--forkи--mount-proc.- Файлы
/proc/PID/ns/*— ссылки видаnet:[inode]; совпадение inode означает один namespace. - Namespace живёт, пока есть процесс внутри, открытый дескриптор или точка монтирования.
- Вход без
-mоставляет файловую систему host — приём для образов без утилит. - User namespace — единственный, создаваемый без привилегий; на нём основан rootless.
- Карта
0 0 4294967295означает отсутствие отображения: root внутри — root снаружи. - Time namespace изолирует смещение монотонных часов, но не реальное время.
- Изолированное окружение создаётся одной командой
unshare; Docker добавляет cgroups, seccomp, сеть и слои.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
Linux: namespaces(7) | https://man7.org/linux/man-pages/man7/namespaces.7.html | Все типы и общая модель |
Linux: user_namespaces(7) | https://man7.org/linux/man-pages/man7/user_namespaces.7.html | Карты UID, capabilities |
Linux: pid_namespaces(7) | https://man7.org/linux/man-pages/man7/pid_namespaces.7.html | Поведение при unshare и setns |
Linux: time_namespaces(7) | https://man7.org/linux/man-pages/man7/time_namespaces.7.html | Что изолируется |
Linux: clone(2) | https://man7.org/linux/man-pages/man2/clone.2.html | Флаги CLONE_NEW* |
Linux: unshare(2) | https://man7.org/linux/man-pages/man2/unshare.2.html | Отличия от clone |
Linux: setns(2) | https://man7.org/linux/man-pages/man2/setns.2.html | Вход в namespace |
Linux: nsenter(1) | https://man7.org/linux/man-pages/man1/nsenter.1.html | Флаги по типам |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Cgroups v2
Главное оглавление