Bubblewrap 容器入门教程 / 第 4 章 - 命名空间详解
第 4 章:命名空间详解
本章深入讲解 Linux 内核提供的 7 种命名空间在 Bubblewrap 中的用法,包括每种命名空间的工作原理、隔离效果、实际应用场景和组合使用模式。
4.1 命名空间概览
Linux 内核提供 7 种命名空间(Namespace),每种隔离一类系统资源:
| 命名空间 | 标志位 | 隔离内容 | 最低内核版本 | bwrap 参数 |
|---|---|---|---|---|
| Mount | CLONE_NEWNS | 挂载点 | 2.4.19 | 隐含使用 |
| User | CLONE_NEWUSER | 用户/组 ID | 3.8 | --unshare-user |
| PID | CLONE_NEWPID | 进程 ID | 2.6.19 | --unshare-pid |
| Network | CLONE_NEWNET | 网络栈 | 2.6.29 | --unshare-net |
| UTS | CLONE_NEWUTS | 主机名/域名 | 2.6.19 | --unshare-uts |
| Cgroup | CLONE_NEWCGROUP | cgroup 根 | 4.6 | --unshare-cgroup |
| IPC | CLONE_NEWIPC | 进程间通信 | 2.6.19 | --unshare-ipc |
命名空间层次关系
宿主系统
├── Mount NS: 全局挂载视图
│ └── User NS (root)
│ └── 其他所有命名空间
│
├── Bubblewrap 沙箱
│ ├── User NS: UID 1000 → 沙箱内 UID 0 (mapped)
│ ├── Mount NS: 独立的文件系统树
│ ├── PID NS: 进程从 PID 1 开始
│ ├── Network NS: 独立的网络栈(或无网络)
│ ├── UTS NS: 独立的主机名
│ ├── Cgroup NS: 独立的 cgroup 视图
│ └── IPC NS: 独立的 IPC 资源
4.2 User Namespace(用户命名空间)
User Namespace 是 Bubblewrap 能以非特权模式运行的关键。它允许普通用户在沙箱内拥有 “root” 权限,同时在宿主上仍然以普通用户身份运行。
工作原理
宿主系统 沙箱内
┌──────────────────────┐ ┌──────────────────────┐
│ UID 1000 (user) │ → │ UID 0 (root) │
│ GID 1000 (group) │ → │ GID 0 (root) │
│ │ │ │
│ 进程权限: 普通用户 │ │ 进程权限: 沙箱 root │
│ (只能操作自己的文件) │ │ (只能操作沙箱内文件) │
└──────────────────────┘ └──────────────────────┘
基本用法
# 创建带 user namespace 的沙箱
bwrap \
--ro-bind / / \
--unshare-user \
--uid 0 \
--gid 0 \
bash
# 在沙箱内验证
id
# uid=0(root) gid=0(root) groups=0(root)
whoami
# root
# 尝试修改只读文件系统——会被阻止
touch /etc/test
# touch: cannot touch '/etc/test': Read-only file system
# 尝试加载内核模块——user namespace 内禁止
modprobe dummy 2>&1 || true
# modprobe: can't change directory to '/lib/modules': No such file or directory
UID/GID 映射
# 自定义 UID/GID 映射
bwrap \
--ro-bind / / \
--unshare-user \
--uid 1000 \
--gid 1000 \
bash -c 'id; cat /proc/self/uid_map'
# uid=1000 gid=1000
# 0 1000 1
# 使用 --map-root-user 快速映射到 root
bwrap \
--ro-bind / / \
--unshare-user \
--map-root-user \
bash -c 'id; cat /proc/self/uid_map'
# uid=0(root) gid=0(root)
# 0 1000 1
安全特性
User Namespace 内的 “root” 受到以下限制:
| 操作 | 状态 | 原因 |
|---|---|---|
| 修改沙箱内的文件 | ✅ 允许 | 只影响 tmpfs/绑定挂载 |
| 修改宿主的文件 | ❌ 禁止 | 不在沙箱的挂载视图中 |
| 加载内核模块 | ❌ 禁止 | CAP_SYS_MODULE 在 user NS 中无效 |
| 修改系统时间 | ❌ 禁止 | 需要宿主的 CAP_SYS_TIME |
| 挂载文件系统 | 受限 | 仅限标记了 FS_USERNS_MOUNT 的类型 |
| 发送信号到宿主进程 | ❌ 禁止 | PID 命名空间隔离 |
| 配置宿主网络 | ❌ 禁止 | 需要宿主的 CAP_NET_ADMIN |
4.3 Mount Namespace(挂载命名空间)
Mount Namespace 是最常用的命名空间之一。它使沙箱拥有独立的挂载点视图,沙箱内对挂载点的修改不会影响宿主。
工作原理
宿主 Mount NS: 沙箱 Mount NS:
/ /
├── bin ├── bin (ro-bind from /bin)
├── etc (读写) ├── etc (ro-bind from /etc)
├── home ├── home (tmpfs, 空)
│ └── user ├── tmp (tmpfs)
│ └── data ├── dev (最小设备)
├── tmp └── proc (沙箱 PID)
├── usr
└── var
基本用法
# Mount namespace 在 bwrap 中是隐含使用的
# 每次使用 --ro-bind、--bind、--tmpfs 等参数时,都会创建新的 mount namespace
bwrap \
--ro-bind / / \
--tmpfs /tmp \
--bind /home/user/workspace /workspace \
--dev /dev \
--proc /proc \
bash
# 在沙箱内查看挂载信息
mount | head -20
# /dev/sda1 on / type ext4 (ro,relatime)
# tmpfs on /tmp type tmpfs (rw,nodev,size=...)
# /home/user/workspace on /workspace type ext4 (rw,relatime)
# ...
只读重挂载
# 将特定目录重新以只读方式挂载
bwrap \
--bind / / \
--remount-ro /etc \
--remount-ro /usr \
--tmpfs /tmp \
bash
实际应用:构建沙箱
# 编译软件时,源码目录可写,系统目录只读
bwrap \
--ro-bind / / \
--bind ~/projects/myapp /src \
--tmpfs /build \
--dev /dev \
--proc /proc \
--unshare-pid \
bash -c '
cd /src
mkdir -p /build/output
make -j$(nproc) DESTDIR=/build/output install
'
4.4 PID Namespace(进程 ID 命名空间)
PID Namespace 使沙箱内的进程拥有独立的进程 ID 空间。沙箱内的第一个进程获得 PID 1,且无法看到或信号宿主的进程。
基本用法
# 创建 PID 命名空间
bwrap \
--ro-bind / / \
--unshare-pid \
--proc /proc \
--dev /dev \
bash
# 在沙箱内
echo "My PID: $$"
# My PID: 1 (或 bwrap fork 后的 PID)
ps aux
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.0 ... ... ? S ... 0:00 bash
# root 2 0.0 0.0 ... ... ? R+ ... 0:00 ps aux
# 尝试查看宿主进程——看不到
ls /proc/ | grep -E '^[0-9]+$' | sort -n | head -5
# 1
# 2 (即 ps 命令自身)
PID 1 的特殊性
在 PID Namespace 中,PID 1 有一些特殊行为:
bwrap \
--ro-bind / / \
--unshare-pid \
--proc /proc \
--dev /dev \
bash -c '
echo "PID 1 is: $$"
# PID 1 不会收到默认信号处理(除了 SIGKILL)
# 需要显式处理 SIGTERM 等信号
# 查看进程树
pstree -p
'
--fork 参数
PID 命名空间要求进程通过 fork() 创建(clone() 使用 CLONE_NEWPID 标志)。bwrap 的 --fork 参数确保正确创建新进程:
# --fork 通常与 --unshare-pid 一起使用
# bwrap 在 fork 后创建子进程,子进程获得新的 PID namespace
bwrap \
--ro-bind / / \
--unshare-pid \
--fork \
--proc /proc \
--dev /dev \
bash -c 'echo "PID: $$"'
# PID: 1
实际应用:进程隔离
# 隔离一个运行多子进程的服务
bwrap \
--ro-bind / / \
--unshare-pid \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
bash -c '
# 启动后台任务
sleep 100 &
sleep 200 &
# 只能看到沙箱内的进程
ps aux
# USER PID %CPU %MEM ... COMMAND
# root 1 ... bash
# root 3 ... sleep 100
# root 4 ... sleep 200
# root 5 ... ps aux
# 杀死所有后台任务
kill %1 %2 2>/dev/null
'
4.5 Network Namespace(网络命名空间)
Network Namespace 为沙箱提供独立的网络栈,包括网络接口、路由表、iptables 规则等。
无网络模式
# 创建完全无网络的沙箱
bwrap \
--ro-bind / / \
--unshare-net \
--dev /dev \
--proc /proc \
bash
# 在沙箱内查看网络
ip addr
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 测试网络连接——失败
curl -s --max-time 3 https://example.com || echo "Network unreachable"
# curl: (6) Could not resolve host: example.com
保留回环接口
# 创建网络命名空间并启用回环接口
bwrap \
--ro-bind / / \
--unshare-net \
--dev /dev \
--proc /proc \
bash -c '
# 启用 lo 接口
ip link set lo up
ip addr add 127.0.0.1/8 dev lo
# 现在可以连接 localhost
ping -c 1 127.0.0.1
# PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
# 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.020 ms
'
进阶:与宿主共享网络
# 不使用 --unshare-net,沙箱共享宿主的网络
bwrap \
--ro-bind / / \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
bash -c '
curl -s --max-time 5 https://example.com | head -5
# <!doctype html><html>...
'
网络隔离配置对比:
| 配置 | 网络状态 | 用途 |
|---|---|---|
无 --unshare-net | 共享宿主网络 | 需要网络的普通应用 |
--unshare-net | 完全无网络 | 不需要网络的离线任务 |
--unshare-net + ip link set lo up | 仅 localhost | 测试本地服务 |
4.6 UTS Namespace(主机名命名空间)
UTS Namespace 允许沙箱拥有独立的主机名和域名,不影响宿主。
基本用法
# 设置沙箱主机名
bwrap \
--ro-bind / / \
--unshare-uts \
--hostname "sandbox-env" \
bash -c '
hostname
# sandbox-env
cat /proc/sys/kernel/hostname
# sandbox-env
'
# 宿主主机名不受影响
hostname
# your-real-hostname
实际应用
# 运行依赖主机名的服务
bwrap \
--ro-bind / / \
--unshare-uts \
--hostname "test-server" \
--dev /dev \
--proc /proc \
bash -c '
# 服务可以通过主机名识别自己
echo "Server starting on $(hostname)..."
'
4.7 Cgroup Namespace(控制组命名空间)
Cgroup Namespace(内核 >= 4.6)隔离 cgroup 文件系统的视图,使沙箱进程只能看到自己的 cgroup 层级。
基本用法
# 创建 cgroup 命名空间
bwrap \
--ro-bind / / \
--unshare-cgroup \
--dev /dev \
--proc /proc \
bash
# 在沙箱内查看 cgroup
cat /proc/self/cgroup
# 在宿主上可能显示: 0::/user.slice/user-1000.slice/...
# 在沙箱内显示为: 0::/
安全意义
宿主 cgroup 视图: 沙箱 cgroup 视图:
/sys/fs/cgroup/ /sys/fs/cgroup/
├── system.slice/ ├── (只看到沙箱自身的 cgroup)
│ ├── docker.service/ └── (隐藏了宿主的其他 cgroup)
│ └── sshd.service/
├── user.slice/
└── ...
这防止了沙箱进程通过 cgroup 文件系统获取宿主系统上其他进程的信息。
4.8 IPC Namespace(进程间通信命名空间)
IPC Namespace 隔离 System V IPC 对象(信号量、消息队列、共享内存)和 POSIX 消息队列。
基本用法
# 创建 IPC 命名空间
bwrap \
--ro-bind / / \
--unshare-ipc \
--dev /dev \
--proc /proc \
bash
# 在沙箱内查看 IPC 资源
ipcs
# ------ Message Queues --------
# key msqid owner perms used-bytes messages
#
# ------ Shared Memory Segments --------
# key shmid owner perms bytes nattch status
#
# ------ Semaphore Arrays --------
# key semid owner perms nsems
# 沙箱看不到宿主的 IPC 对象
4.9 命名空间组合模式
完全隔离(--unshare-all)
# 分离所有支持的命名空间
bwrap \
--ro-bind / / \
--unshare-all \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
bash
# 验证所有命名空间
cat /proc/self/uid_map
# 0 1000 1 (User NS)
mount | wc -l
# 较少 (Mount NS)
ps aux
# PID 1: bash, PID 2: ps (PID NS)
ip addr
# 仅 lo (Network NS)
hostname
# (默认或空) (UTS NS)
cat /proc/self/cgroup
# 0::/ (Cgroup NS)
ipcs
# 全空 (IPC NS)
最小隔离
# 只使用必要的隔离
bwrap \
--ro-bind / / \
--tmpfs /tmp \
bash
# 仅 Mount NS,共享其他所有命名空间
选择性隔离
# 需要网络但不需进程隔离
bwrap \
--ro-bind / / \
--unshare-user \
--unshare-uts \
--hostname "web-sandbox" \
--dev /dev \
--tmpfs /tmp \
curl -s https://example.com
隔离级别对照表
| 场景 | User | Mount | PID | Net | UTS | Cgroup | IPC |
|---|---|---|---|---|---|---|---|
| 最小沙箱 | ✅ | ✅ | — | — | — | — | — |
| 隔离文件系统 | ✅ | ✅ | — | — | — | — | — |
| 隔离进程 | ✅ | ✅ | ✅ | — | — | — | — |
| 离线沙箱 | ✅ | ✅ | ✅ | ✅ | — | — | — |
| 完全隔离 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Flatpak 应用 | ✅ | ✅ | ✅ | 可选 | ✅ | ✅ | ✅ |
4.10 查看沙箱的命名空间信息
从宿主查看
# 获取 bwrap 进程的 PID
BWRAP_PID=$(pgrep -n bwrap)
# 查看其命名空间
ls -la /proc/$BWRAP_PID/ns/
# lrwxrwxrwx 1 user user 0 ... cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 user user 0 ... ipc -> 'ipc:[4026532510]'
# lrwxrwxrwx 1 user user 0 ... mnt -> 'mnt:[4026532508]'
# lrwxrwxrwx 1 user user 0 ... net -> 'net:[4026532511]'
# lrwxrwxrwx 1 user user 0 ... pid -> 'pid:[4026532509]'
# lrwxrwxrwx 1 user user 0 ... uts -> 'uts:[4026532507]'
# 与宿主的命名空间对比
ls -la /proc/1/ns/
# 不同的编号表示不同的命名空间实例
从沙箱内查看
bwrap \
--ro-bind / / \
--unshare-all \
--proc /proc \
--dev /dev \
bash -c '
echo "=== 沙箱命名空间信息 ==="
echo "User NS: $(readlink /proc/self/ns/user)"
echo "Mount NS: $(readlink /proc/self/ns/mnt)"
echo "PID NS: $(readlink /proc/self/ns/pid)"
echo "Network NS: $(readlink /proc/self/ns/net)"
echo "UTS NS: $(readlink /proc/self/ns/uts)"
echo "Cgroup NS: $(readlink /proc/self/ns/cgroup)"
echo "IPC NS: $(readlink /proc/self/ns/ipc)"
echo ""
echo "UID Map: $(cat /proc/self/uid_map)"
echo "PID: $$"
'
4.11 注意事项
⚠️ 重要提醒
PID 命名空间需要
--fork:PID 命名空间要求第一个进程通过fork()创建。bwrap 通常会自动处理,但在某些情况下可能需要显式指定。--unshare-pid必须配合--proc:否则/proc会显示宿主的进程信息。User Namespace 嵌套限制:内核默认限制 user namespace 的嵌套深度(通常为 32 层)。
网络命名空间需要 root 或 user namespace:创建网络命名空间通常需要 user namespace 支持。
Cgroup Namespace 需要内核 >= 4.6:较旧的内核不支持 cgroup 命名空间。
IPC 隔离不阻止 D-Bus:D-Bus 使用 Unix socket 而非 System V IPC,需要额外处理。
4.12 扩展阅读
- namespaces(7) - Linux Man Page
- user_namespaces(7)
- pid_namespaces(7)
- network_namespaces(7)
- cgroup_namespaces(7)
- LWN: Namespace evolution
- The Linux Kernel: Namespaces
上一章:第 3 章 - 基本用法 | 下一章:第 5 章 - 文件系统隔离