1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
|
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2026 by Richard Qin
dc_init() {
local tarball=
local size=$DC_SIZE
local profile=
while (($# > 0)); do
case "$1" in
--tarball)
(($# >= 2)) || dc_die '--tarball 需要一个文件路径。'
tarball=$2
shift 2
;;
--size)
(($# >= 2)) || dc_die '--size 需要一个大小。'
size=$2
shift 2
;;
--profile)
(($# >= 2)) || dc_die '--profile 需要一个文件路径。'
profile=$2
shift 2
;;
--help|-h)
dc_usage
return 0
;;
*)
dc_die "未知的 init 选项:$1"
;;
esac
done
dc_require_commands debootstrap unshare mkfs.ext4 mount chroot setpriv flock
if [[ -n "$tarball" ]]; then
tarball=$(readlink -f -- "$tarball")
[[ -f "$tarball" ]] || dc_die "找不到 tarball:$tarball"
fi
if [[ -n "$profile" ]]; then
profile=$(readlink -f -- "$profile")
[[ -f "$profile" ]] || dc_die "找不到 profile:$profile"
fi
if [[ -e "$DC_INSTANCE_DIR" ]]; then
dc_die "实例已存在:$DC_INSTANCE。请使用 enter 进入,避免覆盖容器数据。"
fi
dc_create_image "$size"
if [[ -n "$tarball" ]]; then
unshare --mount --propagation private -- bash "$DC_SCRIPT" _init "$DC_INSTANCE" "$tarball"
else
unshare --mount --propagation private -- bash "$DC_SCRIPT" _init "$DC_INSTANCE"
fi
DC_PROFILE_FILE="$DC_INSTANCE_DIR/config"
if [[ -n "$profile" ]]; then
cp -- "$profile" "$DC_PROFILE_FILE"
else
dc_profile_write_default "$DC_NETWORK_MODE"
fi
echo "初始化完成。执行 sudo bash $DC_SCRIPT enter $DC_INSTANCE 进入容器。"
}
dc_resize() {
local size=
while (($# > 0)); do
case "$1" in
--size)
(($# >= 2)) || dc_die '--size 需要一个大小。'
size=$2
shift 2
;;
--help|-h)
dc_usage
return 0
;;
*)
dc_die "未知的 resize 选项:$1"
;;
esac
done
[[ -n "$size" ]] || dc_die 'resize 必须指定 --size。'
[[ -f "$DC_IMAGE" ]] || dc_die '没有找到镜像,请先运行 init。'
dc_resize_image "$size"
echo "实例已扩容:$DC_INSTANCE,新的大小为 $size"
}
dc_init_rootfs() {
local tarball=${1:-}
local -a include_packages=()
dc_mount_root
trap dc_cleanup_mounts EXIT
if [[ -n "$tarball" ]]; then
debootstrap \
--variant=minbase \
--unpack-tarball="$tarball" \
"$DC_SUITE" "$DC_ROOT" "$DC_MIRROR"
else
debootstrap \
--variant=minbase \
"$DC_SUITE" "$DC_ROOT" "$DC_MIRROR"
fi
rm -f "$DC_ROOT/etc/resolv.conf"
cp -L /etc/resolv.conf "$DC_ROOT/etc/resolv.conf"
dc_mount_virtual_filesystems
if [[ -z "$tarball" ]]; then
IFS=, read -r -a include_packages <<< "$DC_INCLUDE"
chroot "$DC_ROOT" /usr/bin/env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update
chroot "$DC_ROOT" /usr/bin/env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install -y "${include_packages[@]}"
fi
echo debian-container > "$DC_ROOT/etc/hostname"
}
dc_base_create() {
local output=$DC_TARBALL
local suite=$DC_SUITE
local arch=$DC_ARCH
local mirror=$DC_MIRROR
local include=$DC_INCLUDE
local exclude=$DC_EXCLUDE
local manifest=
local temporary_root
local -a debootstrap_options=(--variant=minbase)
while (($# > 0)); do
case "$1" in
--output)
(($# >= 2)) || dc_die '--output 需要一个文件路径。'
output=$2
shift 2
;;
--manifest)
(($# >= 2)) || dc_die '--manifest 需要一个文件路径。'
manifest=$2
shift 2
;;
--suite)
(($# >= 2)) || dc_die '--suite 需要一个版本名称。'
suite=$2
shift 2
;;
--arch)
(($# >= 2)) || dc_die '--arch 需要一个架构名称。'
arch=$2
shift 2
;;
--mirror)
(($# >= 2)) || dc_die '--mirror 需要一个镜像地址。'
mirror=$2
shift 2
;;
--include)
(($# >= 2)) || dc_die '--include 需要软件包列表。'
include=$2
shift 2
;;
--exclude)
(($# >= 2)) || dc_die '--exclude 需要软件包列表。'
exclude=$2
shift 2
;;
--help|-h)
dc_usage
return 0
;;
*)
dc_die "未知的 base-create 选项:$1"
;;
esac
done
output=$(readlink -m -- "$output")
if [[ -z "$manifest" ]]; then
manifest="$output.manifest"
else
manifest=$(readlink -m -- "$manifest")
fi
[[ "$manifest" != "$output" ]] || dc_die 'manifest 不能覆盖 tarball。'
[[ ! -e "$output" ]] || dc_die "tarball 已存在:$output"
[[ ! -e "$manifest" ]] || dc_die "manifest 已存在:$manifest"
dc_require_commands awk date debootstrap mktemp readlink sha256sum
mkdir -p "$(dirname -- "$output")"
mkdir -p "$(dirname -- "$manifest")"
temporary_root=$(mktemp -d)
[[ -n "$arch" ]] && debootstrap_options+=("--arch=$arch")
[[ -n "$include" ]] && debootstrap_options+=("--include=$include")
[[ -n "$exclude" ]] && debootstrap_options+=("--exclude=$exclude")
(
trap 'rm -rf -- "$temporary_root"' EXIT
debootstrap \
"${debootstrap_options[@]}" \
--make-tarball="$output" \
"$suite" "$temporary_root" "$mirror"
)
dc_manifest_write "$manifest" "$output" "$suite" "${arch:-native}" "$mirror" "$include" "$exclude"
echo "基础 tarball 已创建:$output"
echo "manifest 已创建:$manifest"
}
dc_enter() {
local network_mode=$DC_NETWORK_MODE
local -a bind_args=()
local spec
dc_profile_load
network_mode=$DC_PROFILE_NETWORK
while (($# > 0)); do
case "$1" in
--network)
(($# >= 2)) || dc_die '--network 需要一个模式。'
network_mode=$2
shift 2
;;
--mount)
(($# >= 2)) || dc_die '--mount 需要一个挂载参数。'
dc_bind_add "$2"
shift 2
;;
--help|-h)
dc_usage
return 0
;;
*)
dc_die "未知的 enter 选项:$1"
;;
esac
done
[[ -f "$DC_IMAGE" ]] || dc_die '没有找到镜像,请先运行 init。'
dc_network_prepare "$network_mode"
for spec in "${DC_BIND_SPECS[@]}"; do
bind_args+=(--mount "$spec")
done
if [[ "$network_mode" == nat ]]; then
dc_network_nat_run _enter "${bind_args[@]}"
return
fi
unshare --mount --uts --ipc --pid --fork --propagation private \
"${DC_NETWORK_UNSHARE_ARGS[@]}" -- bash "$DC_SCRIPT" _enter "$DC_INSTANCE" "${bind_args[@]}"
}
dc_enter_rootfs() {
dc_bind_load_options "$@"
dc_mount_root
trap dc_cleanup_mounts EXIT
dc_network_configure_rootfs
dc_mount_virtual_filesystems
dc_bind_mount_all
hostname debian-container
echo '已进入独立的 Debian 实验环境;输入 exit 可退出容器。'
# 完成挂载后撤销一些宿主级危险权限,保留 CAP_LINUX_IMMUTABLE。
setpriv --bounding-set=-sys_admin,-net_admin,-net_raw,-sys_module,-sys_boot,-sys_time,-sys_rawio,-sys_ptrace,-syslog -- \
chroot "$DC_ROOT" /bin/bash -l
}
dc_exec() {
local network_mode=$DC_NETWORK_MODE
local -a bind_args=()
local spec
dc_profile_load
network_mode=$DC_PROFILE_NETWORK
while (($# > 0)); do
case "$1" in
--network)
(($# >= 2)) || dc_die '--network 需要一个模式。'
network_mode=$2
shift 2
;;
--mount)
(($# >= 2)) || dc_die '--mount 需要一个挂载参数。'
dc_bind_add "$2"
shift 2
;;
--)
shift
break
;;
--help|-h)
dc_usage
return 0
;;
*)
dc_die '用法:exec NAME [--network MODE] -- 命令 [参数...]'
;;
esac
done
(($# > 0)) || dc_die 'exec 需要一个命令。'
[[ -f "$DC_IMAGE" ]] || dc_die '没有找到镜像,请先运行 init。'
dc_network_prepare "$network_mode"
for spec in "${DC_BIND_SPECS[@]}"; do
bind_args+=(--mount "$spec")
done
if [[ "$network_mode" == nat ]]; then
dc_network_nat_run _exec "${bind_args[@]}" -- "$@"
return
fi
unshare --mount --uts --ipc --pid --fork --propagation private \
"${DC_NETWORK_UNSHARE_ARGS[@]}" -- \
bash "$DC_SCRIPT" _exec "$DC_INSTANCE" "${bind_args[@]}" -- "$@"
}
dc_exec_rootfs() {
local -a command_args=()
while (($# > 0)); do
case "$1" in
--mount)
(($# >= 2)) || dc_die '--mount 需要一个挂载参数。'
dc_bind_add "$2"
shift 2
;;
--)
shift
command_args=("$@")
break
;;
*)
dc_die 'exec 子进程参数格式无效。'
;;
esac
done
(($# > 0)) || dc_die 'exec 需要一个命令。'
dc_mount_root
trap dc_cleanup_mounts EXIT
dc_network_configure_rootfs
dc_mount_virtual_filesystems
dc_bind_mount_all
hostname debian-container
setpriv --bounding-set=-sys_admin,-net_admin,-net_raw,-sys_module,-sys_boot,-sys_time,-sys_rawio,-sys_ptrace,-syslog -- \
chroot "$DC_ROOT" "${command_args[@]}"
}
dc_status() {
local spec
dc_profile_load
echo "实例:$DC_INSTANCE"
echo "目录:$DC_INSTANCE_DIR"
echo "网络:$DC_PROFILE_NETWORK"
for spec in "${DC_BIND_SPECS[@]}"; do
echo "挂载:$spec"
done
if [[ -f "$DC_IMAGE" ]]; then
echo '镜像:存在'
fi
dc_status_image
}
dc_delete() {
[[ -d "$DC_INSTANCE_DIR" ]] || { echo 'Debian 实例已不存在。'; return 0; }
dc_require_commands findmnt
if findmnt --mountpoint "$DC_ROOT" >/dev/null 2>&1; then
dc_die "实例仍处于挂载状态:$DC_INSTANCE"
fi
rm -rf -- "$DC_INSTANCE_DIR"
echo "Debian 实例已删除:$DC_INSTANCE"
}
|