-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtreesandbox.py
More file actions
executable file
·3477 lines (3006 loc) · 161 KB
/
Copy pathtreesandbox.py
File metadata and controls
executable file
·3477 lines (3006 loc) · 161 KB
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env -S python3 -IBS
# Tree Sandbox for Linux
# Licensed under GPL. https://github.com/garywill/treesandbox
# This project comes with no warranty. Use on your own risk.
import os, sys, shutil, subprocess, pwd, grp, time, pty, ctypes, ctypes.util, atexit, json, copy, tempfile, struct, re, socket, signal, asyncio, datetime , types, select, fcntl, traceback, random , errno, shlex, enum, argparse, hashlib, io, resource, string, platform
from pathlib import Path
from glob import glob
shutil.rmtree = None
# === USER_CONFIG BEGIN === NOTE: Don't change this line ===
# You can use our default userconfig() code as example / template / tutorial.
# Config your sandbox by enabling / modifying / commenting out these options.
# Notice TreeSandbox is in early stage. We try to keep userconfig options stably, but no promise.
def userconfig(si):
uc = d() # dict-like object
uc.sandbox_name='TryTreeSandbox' # NOTE You should give a name to your sandbox
# ---- Reuse Or Not ----
# uc.reuseful=True # Reuse running same-name sandbox instance if there is one alive. (Enabling this makes your sandbox single-instance, otherwise multi-instance)
uc.idleKeepSbxTime = 2 if uc.reuseful else 0 # Keep sandbox alive for a time (second), even if idle (no user app alive)
# ---- ---- ----
uc.apps = [
# The first item is default app, which can omit appname
d(cmdvec=['bash', '--norc'], appname='bash'), # Recommend to keep this item, so host can get sandbox shell easily if needed
d(cmdvec=['sleep', 'infinity'], appname='sleep'),
]
# cmdvec is array, elements are shell args ( shell command string splitted )
# When starting sandbox, you can use cli '--app <appname>'. If not, default app is chosen
# ---- User Mounts -----
# Linux basic system dirs (/bin, /lib, ...) are auto mounted.
# uc.user_mnts are what you want to add.
uc.user_mnts = [
# The term "CWD" here is the path where you put this sandbox start script.
# `si` is dict-like object, means "sandbox info".
# 'SDS' means "src and dest have same value".
# For persistant storage, use 'fakehome' dir as sandbox's HOME dir. Otherwise, tmpfs is used as HOME
# d(op='bind', src=f'{si.CWD}/fakehome', dest=si.HOME),
# d(op='robind', src=f'{si.HOME}/.bashrc', SDS=1),
# d(op='robind', src=f'{si.HOME}/bin', SDS=1),
# d(op='robind', src=f'{si.HOME}/.local/bin', SDS=1),
# d(op='robind', src=f'{si.HOME}/.local/lib', SDS=1),
# d(op='robind', src='/home/linuxbrew', SDS=1),
# d(op='robind', src=f'{si.HOME}/.npmrc', SDS=1),
# d(op='robind', src=f'{si.HOME}/.vimrc', SDS=1),
# d(op='robind', src=f'{si.HOME}/.config/pip/pip.conf', SDS=1),
# d(many_op='appimage', name='xxxx', src=f'{si.CWD}/xxxx.AppImage'),
# AppImage mounting example. Will do :
# - AppImage mounted at /sbxdir/apps/xxxx/ in sandbox
# - Script /sbxdir/apps/run_xxxx is created
]
# --- GUI ----
# Without uc.gui, no DISPLAY in sandbox
# When uc.gui has value and is not "realX", a sandbox-managed new DISPLAY is used inside.
# uc.gui="realX" # Use host's real X11
# uc.gui="weston-xwayland" # A Xwayland in Weson is used
# uc.gui="xephyr"
# uc.gui='xpra'
# uc.newXId='50' # When internel DISPLAY used , the DISPLAY id. String. Otherwise random
uc.windowed_size = (800, 600) # Take effects when gui uses weston/xephyr
uc.sync_clipbd_from_sandbox = True # Auto sync clipboard from sandbox to host (take effect if internel DISPLAY used)
uc.gpus = True if uc.gui else False # Sandbox can see /dev/dri and needed GPU's PCI paths in /sys .
uc.see_userfonts = True if uc.gui else False # Sandbox can see ~/.fonts and so on.
# --- ---- ----
# uc.see_real_hw=True # Sandbox see host's real /dev and /sys
# --- DBus ----
# User (session) DBUS (things like IME needs DBUS)
if uc.gui: uc.dbus_session="filter"
# uc.dbus_session="allow" # Allow all DBUS communication
# uc.dbus_session="filter" # DBUS communication filtered by xdg-dbus-proxy. Default rule is allowing IME and notifications (you can add more to uc.dbusproxy_extra also)
# uc.dbusproxy_extra = ['--see=org.gnome.Shell'] # xdg-dbus-proxy (by Flatpak) extra args
# --- ---- ----
# Create a path in host as share dir. Dir will be accessable (r/w) by sandbox too.
# In sandbox, both same path and a '/tmp/share' is to this dir (r/w).
# This is a prefix. Sandbox name will be added to the dir name.
uc.sharedir_prefix='/tmp/tsbx-share_'
# uc.pulseaudio=True,
# uc.cups=True, # CUPS print
uc.ask_xdg_open=True # Replace 'xdg-open' by an asking script.
uc.forbid_browsers=True # Ban system's firefox/chromium/... in sandbox. (Experimental)
# uc.mask_osrelease=True # Ban /etc/os-release
# uc.machineid='zero' # Write zeros to /etc/machine-id (in rare cases may break some app). Otherwise keep real.
uc.set_envs = d( # Env vars seen by main apps in sandbox. Values must be string
# ENV_VAR_NAME1 = 'ENV_VAR_VAL1',
# ENV_VAR_NAME2 = 'ENV_VAR_VAL2',
)
# --- Network ----
uc.net_iface='real' # Use host's real net ifaces. Won't unshare net ns
# uc.net_iface='tuntap-pasta' # Use pasta to create new net ns and manage net iface
# uc.net_iface='none' # Omitting net_iface means 'none' also
# uc.dns_custom=['127.0.0.1'] # Custom /etc/resolv.conf . If not custom and net_iface=real, host's real resolv.conf will be used
uc.pasta_custom_args = [ # NOTE Takes effect when uc.net_iface=tuntap-pasta .
# NOTE (no '-T' or no '-U' will allow all local ports seen by sandbox)
'-T', 'none', '-U', 'none', # Forbid to access any port of host localhost
'--config-net', '--host-lo-to-ns-lo',
# '--no-map-gw', # If sandbox ip not configured, its internal ip will be looked same as host. In this case you should consider enabling this --no-map-gw
'-a', '172.16.1.2', '-n', '30', '-g', '172.16.1.1', '-a', 'fd00::2', '-g', 'fd00::1',
# '--ns-mac-addr', '00:00:00:00:00:04', # No this = random MAC
# '--debug', '--trace',
] if uc.net_iface=='tuntap-pasta' else None
# NOTE Only when uc.net_iface=tuntap-pasta , set_nftables can be enabled
# uc.set_nftables = True # Enable this, then nftables rules below will be applied to sandbox
if uc.set_nftables == True : uc.nftables_rule = '''
define DYNAMIC_BANIP_V4 = { 224.0.0.0/4 }
# optional blacklisting 224.0.0.0/4, (multicast)
# optional blacklisting 127.0.0.0/8, (loopback)
define DYNAMIC_BANIP_V6 = { ff00::/8 }
# optional blacklisting ff00::/8, (multicast)
# optional blacklisting ::1, (loopback)
table inet myfiltertable {
set banip_v4 { type ipv4_addr; flags interval
elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 255.255.255.255, $DYNAMIC_BANIP_V4 }
}
set banip_v6 { type ipv6_addr; flags interval
elements = { ::/128, ::ffff:0:0/96, ::ffff:0:0:0/96, fc00::/7, fe80::/10, $DYNAMIC_BANIP_V6 }
}
chain myoutputchain { type filter hook output priority 0; policy accept;
ct state established,related accept
meta l4proto ipv6-icmp ip6 daddr { ff02::1, ff02::2, ff02::1:ff00:0/104 } accept
meta l4proto { tcp, udp } th dport { 53 } accept
ip daddr @banip_v4 reject with icmp type admin-prohibited
ip6 daddr @banip_v6 reject with icmpv6 type admin-prohibited
}
}
'''.strip()
return uc
# === USER_CONFIG END === NOTE: Don't change this line ===
def gen_dynamic_cfg(si, uc): # 这个只在顶层解析一次
cmds_to_mask = [] # 内部,不传递
paths_to_mask = [] # 传递
mnts_dns = []
mnts_gui = []
xephyr_extra_args = []
weston_extra_args = []
xpra_extra_args = [] ; xpra_server_extra_args = [] ; xpra_client_extra_args = []
xwayland_extra_args = []
bridges = []
#-------------------------
icewm = True if uc.gui in ['xephyr','weston-xwayland'] else False
if uc.see_userfonts: mnts_gui += [
d(op='robind', src=f'{si.HOME}/.fonts', SDS=1) if os.path.lexists(f'{si.HOME}/.fonts') else None,
d(op='robind', src=f'{si.HOME}/.fonts.conf', SDS=1) if os.path.lexists(f'{si.HOME}/.fonts.conf') else None,
d(op='robind', src=f'{si.HOME}/.cache/fontconfig', SDS=1) if os.path.lexists(f'{si.HOME}/.cache/fontconfig') else None,
]
if uc.gpus: # /sys/module/i915 这类一般不用也可以
sys_devices_pciX_X = [ padir(p) for p in glob('/sys/devices/*/*/drm') ]
mnts_gui += [
d(op='rosame', src='/dev/dri', SDS=1),
d(op='rosame', src='/sys/class/drm', SDS=1),
*[ d(op='rosame', src=p, SDS=1) for p in glob('/sys/dev/char/226:*') ],
*[ d(op='rosame', src=p, SDS=1) for p in sys_devices_pciX_X ],
*[ d(op='rosame', src=rslvy(f'{p}/driver'), SDS=1) for p in sys_devices_pciX_X ],
]
for link in glob('/sys/bus/pci/devices/*'):
if rslvy(link) in sys_devices_pciX_X:
mnts_gui += [ d(op='rosame', src=link, SDS=1) ]
if uc.gui and uc.gui != 'realX': # 使用GUI但不是真实X, 说明是某种隔离的X,需要新的X编号
if uc.newXId:
newXId = uc.newXId
else:
while (newXId := str(random.randrange(230, 980)) ) :
if is_XId_available(newXId): break
if uc.gui == 'xpra':
mnts_gui += [
d(op='tmpfs', dest=f'{si.HOME}/.xpra'),
d(op='tmpfs', dest=f'{si.HOME}/.config/xpra'),
# d(op='rofile', dest=f'{si.HOME}/.fakexinerama', content=''), # 不注释这两个则可以阻止这两个文件有内容,但好像不重要
# d(op='rofile', dest=f'{si.HOME}/.{newXId}-fakexinerama', content=''),
d(op='rofile',dest='/etc/X11/Xwrapper.config', content='allowed_users=anybody') if os.path.lexists('/etc/X11/Xwrapper.config') else None,
]
xpra_extra_args += [
'--daemon=no',
# '--bind=unix',
# '--auth=allow',
'--start-new-commands=no',
'--pulseaudio=no',
'--dbus-launch=no', # NOTE 禁止dbus为什么无效?
'--dbus-proxy=no',
'--dbus-control=no',
'--webcam=no',
'--html=off',
'--systemd-run=no',
'--exit-with-windows=no',
'--exit-with-client=no',
'--mdns=no',
'--file-transfer=no',
'--forward-xdg-open=off',
# --headerbar=auto|no|force
'--printing=no',
'--keyboard-sync=no',
# --keyboard-raw=yes|no
'--opengl=yes:native', # --opengl=(yes|no|auto)[:backend]
'--encoding=rgb',
'-z0', # 无压缩
'--video-encoders=vaapi',
#--speaker=on|off|disabled and --microphone=on|off|disabled|on:DEVICE|off:DEVICE
'--speaker=disabled',
'--microphone=disabled',
#--title=VALUE
#--border=yellow,10.
'--clipboard-direction=disabled', # 用xpra的好像对ASK_OPEN不灵
'--use-display=yes'
]
# Xorg的参数-ac让不需要XAUTHORITY。 如果不自定义xpra的--xvfb的值的话,无法让Xserver免认证。xpra的--auth可能控制的是xpra的客户端与服务端之间的认证,不是x server与client的认证
if uc.windowed_size:
if uc.gui == 'xephyr':
xephyr_extra_args = ['-screen', f'{uc.windowed_size[0]}x{uc.windowed_size[1]}']
elif uc.gui == 'weston-xwayland' :
weston_extra_args = [f'--width={uc.windowed_size[0]}', f'--height={uc.windowed_size[1]}' ]
xwayland_extra_args = ['-geometry', f'{uc.windowed_size[0]}x{uc.windowed_size[1]}']
if uc.dbus_session == 'filter':
dbusproxy_argv = [
getenv('DBUS_SESSION_BUS_ADDRESS'), '/tmp/dbusproxy.socket', '--filter',
'--talk=org.freedesktop.Notifications',
'--talk=org.fcitx.*',
'--talk=org.freedesktop.IBus.*',
'--talk=org.freedesktop.portal.IBus',
'--talk=org.freedesktop.portal.Fcitx',
*(uc.dbusproxy_extra or [])]
# '--talk=org.kde.StatusNotifierWatcher', org.kde.StatusNotifierItem # TODO 这两个与系统托盘图标有关, realX时可以考虑允许
# 处理 /etc/resolv.conf
CHK( Path('/var/run').is_symlink() and rslvn('/var/run') == '/run', "/var/run is not linked to /run on your host, which is different to most Linux distros. We can't handle this for now")
RSLVCF_is_link = True if Path('/etc/resolv.conf').is_symlink() else False
RSLVCF_is_file = is_file('/etc/resolv.conf')
CHK(RSLVCF_is_link or RSLVCF_is_file, f"/etc/resolv.conf not symlink or file. We can't handle this")
dns_use_custom = isinstance(uc.dns_custom, list)
if dns_use_custom: RSLVCF_content = ''.join([f'nameserver {ip}\n' for ip in uc.dns_custom])
have_iface = uc.net_iface in ['real', 'tuntap-pasta']
if not have_iface: uc.net_iface = 'none'
if uc.set_nftables: CHK(uc.net_iface=='tuntap-pasta', 'Only when uc.net_iface=tuntap-pasta, set_nftables can be enabled')
# link/file | custom/notcustom | ifacereal 共8种情况
# TODO nscd if use real
if RSLVCF_is_file : # /etc/resolv.conf是文件,非链接
if dns_use_custom:
mnts_dns = [d(op='rofile', content=RSLVCF_content, dest='/etc/resolv.conf')]
else:
if have_iface: mnts_dns = [] # 原本的/etc/resolv.conf文件保持
else : mnts_dns = [d(op='empty-if-exist', dest='/etc/resolv.conf')] # 清空
else: # /etc/resolv.conf是链接
RSLVCF_target_dir = padir(rslvn('/etc/resolv.conf'))
CHK(RSLVCF_target_dir.startswith('/run/'), f"/etc/resolv.conf target is {rslvn('/etc/resolv.conf')}, which not in /run/xxx/ , we can't handle this. (Most distros /etc/resolv.conf -> /var/run/xxxx/ -> /run/xxxxx)")
if dns_use_custom:
mnts_dns = [d(op='rofile', content=RSLVCF_content, dest=rslvn('/etc/resolv.conf'))]
else:
if have_iface: mnts_dns = [d(op='robind', src=RSLVCF_target_dir, SDS=1)]
else : pass # 让/run/xxxxx/resolv.conf继续不存在
browser_cmds = [
"firefox", "firefox-esr", "seamonkey", "icecat",
"librewolf", "waterfox", "palemoon", "basilisk", "floop", "zen-browser",
"chromium", "chromium-browser",
"google-chrome", "google-chrome-stable", "ungoogled-chromium",
"microsoft-edge", "microsoft-edge-stable",
"vivaldi", "brave-browser", "opera",
"torbrowser-launcher", "torbrowser",
"konqueror", "falkon", "epiphany",
"lynx", "w3m", "links", "elinks", "browsh",
"dillo", "qutebrowser", "midori", "otter-browser", "xombrero", "luakit", "dooble", "netsurf", "nyxt", "iridium", "surf"
]
if uc.forbid_browsers:
cmds_to_mask += browser_cmds
paths_to_mask += [ path for cmd in cmds_to_mask if (path := which_and_resolve_exist(cmd)) is not None ]
if uc.machineid == 'zero':
machineid = '00000000000000000000000000000000'
# bridge seefrom是从哪层可看见这个桥进程 seeto是通过这个桥进程看到哪个层的fs
if uc.gui in ['weston-xwayland','xpra']:
bItem = d(seefrom='semitruCmpannLyr', seeto='mainLyr')
bItem.create_links = []
bItem.create_links += [f'/tmp/.X11-unix/X{newXId}']
if uc.gui=='xpra':
bItem.create_links += [ # NOTE 不能链目录,要防止xpra客户端的socket被放进目录里被server看见
f'/run/xpra/{si.hostname}-{newXId}',
f'/run/user/{si.uid}/xpra/{si.hostname}-{newXId}',
f'/run/user/{si.uid}/xpra/Xauthority-{newXId}',
f'/run/user/{si.uid}/xpra/{newXId}/socket',
f'/run/user/{si.uid}/xpra/{newXId}/xauthority',
f'/run/user/{si.uid}/xpra/{newXId}/config',
f'/run/user/{si.uid}/xpra/{newXId}/cmdline',
f'/run/user/{si.uid}/xpra/{newXId}/server.env',
f'{si.HOME}/.xpra/{si.hostname}-{newXId}',
f'{si.HOME}/.config/xpra/xpra.conf',
]
bridges.append(bItem)
dyncfg = d({k: v for k, v in locals().items() if k in [
'paths_to_mask', 'machineid', 'sharedir_onhost', 'dbusproxy_argv' , 'mnts_dns', 'bridges',
'newXId', 'mnts_gui', 'xephyr_extra_args', 'weston_extra_args', 'xwayland_extra_args', 'xpra_extra_args', 'xpra_server_extra_args', 'xpra_client_extra_args', 'icewm',
]})
return dyncfg
# layer1 产生。 所有的layer_cfg都在 layer1 下
def gen_layer1(si, uc, dyncfg): # 这个只在顶层解析一次
# 第1层不跑任何程序,只用于PID隔离,和退出时的清理工作
return d(
layer_name='layer1', # 默认模板的 layer_name 不要修改
unshare_pid=True, # 第1层必须
unshare_mnt=True, # 第1层尝试有unshare mnt但不newrootfs
# uid 变 0
unshare_user=True, uid_map_as_root=True,
# 准备开始第2层。这第1层的 sublayers 数组应该只有一个元素,即,第2层只有一个容器
sublayers = [gen_layer2(si, uc, dyncfg)],
)
def gen_layer2(si, uc, dyncfg):
return d(
layer_name='layer2', # 默认模板的 layer_name 不要修改
unshare_mnt=True,
newrootfs=True, # 第2层必须 # 有newrootfs则必须有fs
fs=[ # fs全称 fs_operations_for_new_rootfs 。
# 第2层是首次 unshare mnt 。先复制一次真实host的rootfs环境
d(many_op='container-rootfs'),
d(many_op='basic-dev'),
d(op='rosame', src='/dev/net/tun', SDS=1) if uc.net_iface=='tuntap-pasta' else None,
d(many_op='mask-privacy', destbase='/'),
d(many_op='sbxdir-in-newrootfs', dest='/sbxdir'),
*dyncfg.mnts_gui,
d(op='robind', src=f'/tmp/.X11-unix/X{getenv("DISPLAY").lstrip(":")}', SDS=1),
d(op='robind', src=f'{getenv("XAUTHORITY")}', SDS=1),
d(op='bind', src=getenv('DBUS_SESSION_BUS_ADDRESS').removeprefix('unix:path='), SDS=1 ),
d(many_op='dup-rootfs', destbase='/zrootfs'), # 排除/proc。不加ro。
d(many_op='mask-privacy', destbase='/zrootfs'),
d(op='empty-if-exist', dest=f'/zrootfs/{si.PTMP}'),
],
envs_unset=[
"SYSTEMD_EXEC_PID", "MANAGERPID", "SSH_AGENT_PID", "SSH_AUTH_SOCK", "WINDOWMANAGER", "SHELL_SESSION_ID", "INVOCATION_ID", "GPG_TTY", "XDG_SESSION_ID", "KONSOLE_DBUS_SERVICE", "GPG_AGENT_INFO", "OLDPWD", "WINDOWID", "SESSION_MANAGER", "JOURNAL_STREAM", "XDG_CACHE_HOME",
"XDG_SESSION_TYPE", "WAYLAND_DISPLAY", "QT_WAYLAND_RECONNECT", # 这几个是因为现在暂时不支持主机wayland所以放这里先
],
envset_grps=[
d(NO_AT_BRIDGE='1'),
d(XDG_RUNTIME_DIR=si.sbx_XDG_R_D),
],
create_userns_unpri=True,
unshare_net=True if uc.net_iface == 'none' else False,
pasta_args = uc.pasta_custom_args if uc.net_iface=='tuntap-pasta' else None, # 运行pasta, 并把自身加入其新netns
nftables_rule = uc.nftables_rule if uc.set_nftables else None,
sublayers = [
gen_layer2c(si, uc, dyncfg),
gen_layer2z(si, uc, dyncfg),
],
)
def gen_layer2c(si, uc, dyncfg):
# layer2c实际上深度为3, 这层是为了运行可信程序如 xpra client , dbus proxy 等
return d(
layer_name='layer2c', unshare_pid=True, unshare_mnt=True,
unshare_net=True, # 如果有些subp会监听抽象套接字,为了不被其他沙箱偷看,要隔离. 也可以考虑用 unshare -n -r -c 来启动它们
newrootfs=True,
fs=[
d(many_op='dup-rootfs', destbase='/'),
d(many_op='sbxdir-in-newrootfs', dest='/sbxdir'),
],
is_semitruCmpannLyr=True, # 设layer2c(而非2)为semitruCmpannLyr,因为2c才有unshare_pid
subprocs=[
d( subp_name='xephyr', cmdvec=["Xephyr", f":{si.newXId}", '-nolisten', 'local', "-resizeable", "-ac", '-title', si.sandbox_name, *dyncfg.xephyr_extra_args]
) if uc.gui=='xephyr' else None,
d( subp_name='weston', cmdvec=["weston", f"--socket=wayland-{si.newXId}" , f"--shell=kiosk", *dyncfg.weston_extra_args]
) if uc.gui=='weston-xwayland' else None,
d( subp_name='xpraclient', cmdvec=['xpra', *dyncfg.xpra_extra_args, *dyncfg.xpra_client_extra_args, 'attach',f':{si.newXId}'],
start_after = [
d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') ,
d(waittype='socket-listened', path=f'/run/xpra/{si.hostname}-{si.newXId}')
] ) if uc.gui=='xpra' else None,
d( subp_name='dbusproxy', cmdvec=['xdg-dbus-proxy', *dyncfg.dbusproxy_argv]
) if uc.dbus_session=='filter' else None,
],
daemon_tasks = [
d(task='sync_clipbd') if uc.gui in ['weston-xwayland', 'xephyr', 'xpra'] else None,
],
)
def gen_layer2z(si, uc, dyncfg):
return d( # layer2z 作为 layer2和3之间,把layer2的/zrootfs变回真/,准备让layer3接
layer_name='layer2z', unshare_mnt=True,
start_after=[
d(waittype='socket-listened', path='/tmp/dbusproxy.socket') if uc.dbus_session=='filter' else None,
d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') if uc.gui=='xephyr' else None,
d(waittype='socket-listened', path=f'{si.sbx_XDG_R_D}/wayland-{si.newXId}') if uc.gui=='weston-xwayland' else None,
],
newrootfs=True,
fs=[
d(many_op='dup-rootfs', srcbase='/zrootfs'),
d(many_op='sbxdir-in-newrootfs', dest='/sbxdir'),
d(op='robind', src=f'/tmp/.X11-unix/X{si.newXId}', dest=f'/sbxdir/temp/X{si.newXId}') if uc.gui=='xephyr' else None,
d(op='robind', src=f'{si.sbx_XDG_R_D}/wayland-{si.newXId}', dest=f'/sbxdir/temp/wayland-{si.newXId}') if uc.gui=='weston-xwayland' else None,
d(op='robind', src='/tmp/dbusproxy.socket', dest='/sbxdir/temp/dbusproxy.socket') if uc.dbus_session=='filter' else None,
],
sublayers=[ gen_layer3(si, uc, dyncfg) ],
)
def gen_layer3(si, uc, dyncfg):
return d(
layer_name='layer3', # 默认模板的 layer_name 不要修改
unshare_mnt=True,
unshare_cgroup=True,
unshare_ipc=True,
unshare_time=True,
unshare_uts=True,
newrootfs=True, # 有newrootfs则必须有fs
fs=[ # fs全称fs_operations_for_new_rootfs 。
d(many_op='container-rootfs'), # 不包括 dev 。不包括 proc
d(many_op='sbxdir-in-newrootfs', dest='/sbxdir'),
d(op='empty-if-exist', dest=rslvn(si.startscript_on_host)),
# ---- 以上是不变条目 ----
d(many_op='basic-dev') if not uc.see_real_hw else None, # 创建新的容器最小的/dev
d(op='robind', src=f'/run/user/{si.uid}/pulse/native', SDS=1) if uc.pulseaudio else None,
d(op='robind', src=rslvy('/var/run/cups/cups.sock'), SDS=1) if uc.cups else None,
*([
d(op='robind', src='/dev', SDS=1),
d(op='tmpfs',dest='/dev/shm'),
d(op='robind', src='/sys/class', SDS=1),
d(op='robind', src='/sys/bus', SDS=1),
d(op='robind', src='/sys/devices', SDS=1),
] if uc.see_real_hw else [] ),
# TODO 1. 改用dyncfg 2. layer2里也加
*([
d(op='robind', dest=f'/tmp/.X11-unix/X{getenv("DISPLAY").lstrip(":")}', SDS=1),
d(op='robind', dest='/tmp/xauthfile', src=f'{getenv("XAUTHORITY")}'),
] if uc.gui=='realX' else [] ),
d(op='robind', src=f'/sbxdir/temp/X{si.newXId}', dest=f'/tmp/.X11-unix/X{si.newXId}') if uc.gui=='xephyr' else None,
d(op='robind', src=f'/sbxdir/temp/wayland-{si.newXId}', dest=f'{si.sbx_XDG_R_D}/wayland-{si.newXId}', ) if uc.gui=='weston-xwayland' else None,
*dyncfg.mnts_gui,
d(op='rofile', dest=shutil.which("xdg-open"), destmode='555', content=ASK_OPEN ) if uc.ask_xdg_open else None,
*[d(op='empty-if-exist', dest=path) for path in dyncfg.paths_to_mask],
d(op='robind', dest='/tmp/dbus-session.socket', src=getenv('DBUS_SESSION_BUS_ADDRESS').removeprefix('unix:path=')) if uc.dbus_session == 'allow' else None,
d(op='robind', dest='/tmp/dbus-session.socket', src='/sbxdir/temp/dbusproxy.socket') if uc.dbus_session=='filter' else None,
d(op='empty-if-exist', dest='/etc/fstab'),
d(op='empty-if-exist', dest=rslvn('/etc/os-release')) if uc.mask_osrelease else None,
d(op='rofile', dest='/etc/machine-id', content=dyncfg.machineid) if dyncfg.machineid else None,
*dyncfg.mnts_dns,
*([
d(op='tmpfs', dest=f'{si.HOME}/.icewm'),
d(op='rofile', dest=f'{si.HOME}/.icewm/preferences', content=ICEWM_PREF),
d(op='rofile', dest=f'{si.HOME}/.icewm/prefoverride', content=ICEWM_PREF),
# d(op='rofile', dest=f'{si.HOME}/.icewm/winoptions', content=ICEWM_WINOPTIONS),# 让app无法决定新窗口位置
d(op='rofile', dest=f'{si.HOME}/.icewm/menu', content=''),
d(op='rofile', dest=f'{si.HOME}/.icewm/toolbar', content=''),
d(op='rofile', dest=f'{si.HOME}/.icewm/programs', content=''),
] if dyncfg.icewm else [] ),
*([
d(op='bind', src=si.sharedir_onhost, dest='/tmp/share'),
d(op='bind', src=si.sharedir_onhost, SDS=1),
] if si.sharedir_onhost else []),
# NOTE 用户挂载要放最后
*(uc.user_mnts if uc.user_mnts else []), # NOTE 用户挂载要放最后
d(op='final-rmt-ro', dest='/sbxdir/apps', flag=mntflag_apps)
],
envs_unset=[
"ICEAUTHORITY", "XAUTHORITY", "DISPLAY", "WAYLAND_DISPLAY", "XAUTHLOCALHOSTNAME", "IBUS_ADDRESS", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SYSTEM_BUS_ADDRESS",
"XDG_SESSION_DESKTOP", "XDG_CURRENT_DESKTOP", "KDE_FULL_SESSION", "KDE_APPLICATIONS_AS_SCOPE", "KDE_SESSION_UID", "KDE_SESSION_VERSION", # TODO 如果用户主机不是KDE是其他, 会有其他变量需要去除
],
envset_grps=[
d( DISPLAY=getenv("DISPLAY"), XAUTHORITY='/tmp/xauthfile', ) if uc.gui=='realX' else None,
d(DISPLAY=f':{si.newXId}') if uc.gui in ['xephyr','weston-xwayland','xpra'] else None,
# d(WAYLAND_DISPLAY=f'wayland-{si.newXId}') if uc.gui=='weston-xwayland' else None, # 先不要 WAYLAND_DISPLAY 这个环境变量,让应用都使用 Xwayland 先
d(DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/dbus-session.socket') if uc.dbus_session else None,
],
sublayers=[
gen_layer4c(si, uc, dyncfg),
gen_layer4(si, uc, dyncfg),
],
)
def gen_layer4c(si, uc, dyncfg):
return d(
layer_name='layer4c', # 默认模板的 layer_name 不要修改
unshare_pid=True, unshare_mnt=True,
unshare_net=True, # NOTE 内部xpra所带出来的dbus可能监听抽象套接字。最好unshare_net, 否则因为我们不要求认证,其他沙箱不隔离网络就可能偷看这个, 也可以考虑用unshare -n -r -c 来启动Xorg
subprocs=[
*([
d( subp_name='icewm', cmdvec=['env', 'LC_ALL=en_US.UTF8', 'env', 'LANG=en_US.UTF8', 'env', 'LANGUAGE=en_US.UTF8', 'icewm-session', '--nobg'] , start_after = [ d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') ] ) ,
# d( subp_name='icewmtray', cmdvec=["icewmtray"] , start_after = [ d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') ] ) ,
] if dyncfg.icewm else [] ) ,
d( subp_name='xwayland', cmdvec=['env', f'WAYLAND_DISPLAY=wayland-{si.newXId}', 'Xwayland', f':{si.newXId}', '-nolisten', 'local', *dyncfg.xwayland_extra_args ]
) if uc.gui=='weston-xwayland' else None,
d( subp_name='xorg', cmdvec=['Xorg', '-ac', '-noreset', '-novtswitch', '-nolisten', 'tcp', '-nolisten', 'local', '+extension', 'GLX', '+extension', 'RANDR', '+extension', 'RENDER', '-config', '/etc/xpra/xorg.conf', '-depth', '24', f':{si.newXId}'] ) if uc.gui=='xpra' else None,
d( subp_name='xpraserver' , cmdvec=['env', 'XPRA_PRIVATE_XAUTH=1', 'xpra', 'start', *dyncfg.xpra_extra_args, *dyncfg.xpra_server_extra_args, f':{si.newXId}'], start_after = [ d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') ]
) if uc.gui=='xpra' else None,
],
)
def gen_layer4(si, uc, dyncfg):
return d( # 主 用户app 在这里跑
layer_name='layer4', # 默认模板的 layer_name 不要修改
is_mainlyr=True, # 我是主app所在层
unshare_pid=True, unshare_mnt=True,
envset_grps = [
d(PATH=getenv("PATH").rstrip(':')+':/sbxdir/apps' ),
uc.set_envs if uc.set_envs else {},
],
start_after = [
d(waittype='socket-listened', path=f'/tmp/.X11-unix/X{si.newXId}') if uc.gui in ['xephyr', 'weston-xwayland','xpra'] else None,
# TODO 等待icewm, 如果需要
],
# user_shell=True, # 调试用
# dev_shell=True, # 调试用
)
class NameMng:
random_chars = "abcdefghkmnpqrsuvwxyz"
@classmethod
def chk_str_valid_sandbox_name(cls, string):
CHK( re.match(r'^[a-zA-Z0-9_-]+$', string), f"Sandbox name can only contain letters, numbers, '-', '_' . This name is invalid: {string}" )
CHK( not '--' in string, f" '--' is not allowed in sandbox name. This name is invalid: {string}")
CHK( not string.startswith('-') and not string.endswith('-'), f"Sandbox name can not starts or ends with '-'. This name is invalid: {string}")
@classmethod
def gen_instance_name_mkdir(cls): # 只在 最外层启动时 并且 确定要创建新实例时 调用
now = datetime.datetime.now()
time_str = now.strftime("%m%d-%H%M%S")
ds = now.microsecond // 100_000
n = 0
while True:
if n>100: raise_exit('Have tried too many times generating instance name')
random_str = ''.join(random.choices(cls.random_chars, k=3))
instance_name = f'{si.sandbox_name}--{time_str}-{ds}{random_str}'
outest_sbxdir = f'{si.PTMP}/{instance_name}'
CG_SBX = f'{si.CG_TSBXS}/{instance_name}'
if os.path.lexists(outest_sbxdir) or os.path.lexists(CG_SBX):
n+=1 ; continue
try: os.makedirs(outest_sbxdir, exist_ok=False)
except FileExistsError:
n+=1 ; continue
except: raise
mkdirp(si.CG_TSBXS)
try: os.makedirs(CG_SBX, exist_ok=False)
except FileExistsError:
n+=1 ; continue
except: raise
Path(f'{CG_SBX}/cgroup.procs').write_text(str(os.getpid()))
break
return instance_name, outest_sbxdir, CG_SBX
@classmethod
def is_pattern_instance_name(cls, string):
return re.match(rf'^{si.sandbox_name}--\d{{4}}-\d{{6}}-\d[{cls.random_chars}]{{3}}$', string)
resv_name_prefix = ['bridge_', 'layer', 'shareshell_', 'mainApp']
resv_words = ['host', 'sbx', 'sbxs', 'tsbx', 'tsbxs', 'tsbxes', 'sandbox', 'sandboxs', 'sandboxes', 'layer', 'layers', 'new', 'py', 'json', 'name', 'dirs', 'log', 'logs', 'socket', 'nc', 'tmpfs', 'tmp', 'temp', 'overlay', 'events', 'lyr_cfg', 'pid', 'userconfig', 'rootfs', 'outest', 'mainLyr', 'semitruCmpannLyr', 'userns_unpri', 'netns_tun', 'bridge', 'shareshell', 'mainApp']
def init_sbxinfo(): # 仅顶层运行,子容器层不运行。返回的数据一路传下各个子层
# 获得调用py脚本的文件位置信息,一般仅用于顶层得多,子容器内用得少
scriptfilepath = rslvy(os.path.abspath(__file__))
scriptdirpath = os.path.dirname(scriptfilepath) # 获取脚本所在目录
scriptdirname = os.path.basename(scriptdirpath) # 获取脚本所在目录名
scriptname = os.path.basename(scriptfilepath) # 获取脚本文件名(含扩展名)
scriptnamenoext = os.path.splitext(scriptname)[0] # 获取脚本文件名(不含扩展名)
si = d()
for i in [0,1,2]:
try: fcntl.fcntl(i, fcntl.F_GETFD)
except OSError as err:
if err.errno != errno.EBADF: raise
else:
devnull = os.open('/dev/null', os.O_RDWR)
os.dup2(devnull, i)
if devnull != i: os.close(devnull)
fdnull = os.open("/dev/null", os.O_PATH)
CHK(fdnull>=3, 'fdnull must >=3')
set_fd_keep_on_exec(fdnull, False)
si.fdnull = fdnull
# 从外部(linux host)启动沙箱的原本用户信息
uid = os.getuid()
gid = os.getgid()
username = pwd.getpwuid(uid).pw_name # 获取当前用户名
groupname = grp.getgrgid(gid).gr_name
HOME = f'/home/{username}' if uid>0 else '/root'
hostname = open("/etc/hostname").read().strip()
outest_pid = os.getpid()
host_XDG_R_D = getenv("XDG_RUNTIME_DIR")
sbx_XDG_R_D = f'/run/user/{uid}'
startscript_on_host = scriptfilepath
CWD = scriptdirpath
PTMP = f'/tmp/tsbxs-{uid}'
hash_bootsbx_py = hash_blake2b(open(scriptfilepath, 'rb').read())
CHK(uid != 0 and gid != 0, f'Currently our sandbox tool does not support running as root')
mkdirp(PTMP) # 创建不同沙箱实例共用的 主临时目录,不清理这个
os.chmod(PTMP, 0o700)
si.update( { k: v for k, v in locals().items() if k in
['hostname', 'PTMP', 'uid', 'gid', 'username', 'groupname', 'HOME', 'outest_pid',
'startscript_on_host', 'CWD', 'hash_bootsbx_py', 'host_XDG_R_D', 'sbx_XDG_R_D']
} )
uc = userconfig(si) # NOTE
# 沙箱名。不是子容器层名
if uc.sandbox_name: NameMng.chk_str_valid_sandbox_name(uc.sandbox_name)
sandbox_name = uc.sandbox_name or f'{scriptdirname}_{scriptname}' # 沙箱名
sandbox_name = re.sub(r'[^a-zA-Z0-9_\-]', lambda m: f"_{ord(m.group(0)):x}", sandbox_name)
CHK( sandbox_name not in resv_words, f"Sandbox name {sandbox_name} conflicts with reserved word {resv_words}")
CHK( len(sandbox_name) < 500, f'Sandbox name too long: {sandbox_name}')
apps = uc.apps
if uc.reuseful: reuseful = uc.reuseful
if uc.idleKeepSbxTime: idleKeepSbxTime = uc.idleKeepSbxTime
if (sharedir_prefix := uc.sharedir_prefix):
CHK( sharedir_prefix.startswith('/tmp/') or sharedir_prefix.startswith('/dev/shm/'), "uc.sharedir_prefix must start with '/tmp/' or '/dev/shm/'")
sharedir_onhost = f'{sharedir_prefix}{sandbox_name}'
si.sharedir_onhost = sharedir_onhost
else:
sharedir_onhost = None
sync_clipbd_from_sandbox = True if uc.sync_clipbd_from_sandbox else False
si.update( { k: v for k, v in locals().items() if k in
[ 'sandbox_name', 'reuseful', 'idleKeepSbxTime', 'apps', 'sync_clipbd_from_sandbox', ]
} )
CG_HOSTUSER = f'/sys/fs/cgroup/user.slice/user-{uid}.slice/user@{uid}.service'
CG_TSBXS = f'{CG_HOSTUSER}/tsbxs.slice'
CHK( os.access(CG_HOSTUSER, os.W_OK), f"The directory {CG_HOSTUSER} does not exist or is not writable")
BND_MAX = int(Path('/proc/sys/kernel/cap_last_cap').read_text())
pythonbin = sys.executable
dyncfg = gen_dynamic_cfg(si, uc) # NOTE
if 'newXId' in dict.keys(dyncfg): newXId = dyncfg.newXId
si.update( { k: v for k, v in locals().items() if k in
['newXId', 'CG_HOSTUSER', 'CG_TSBXS', 'BND_MAX', 'pythonbin', ]
} )
layer1_cfg = gen_layer1(si, uc, dyncfg)
start_lyrs_recursive_jobs(si, layer1_cfg)
if uc.net_iface == 'tuntap-pasta': si.expected_alive_procs += [ 'netns_tun'] # 'pasta_runner'因为无法获取ns所以不放其中
bridges = []
for bItem in (dyncfg.bridges or []):
def get_real_layername(name_in):
if name_in.startswith('layer'): return name_in
else:
if si.specialLyrs[name_in]: return si.specialLyrs[name_in]
real_seefrom = get_real_layername(bItem.seefrom)
real_seeto = get_real_layername(bItem.seeto)
if not (real_seefrom and real_seeto):
log_warn(f'The layer(s) indicated by this bridge item {bItem} not found, ignoring bridge item.')
continue
bridge_name = f'bridge_<{real_seefrom.removeprefix('layer')}>_<{real_seeto.removeprefix('layer')}>'
dcp_bItem = copy.deepcopy(bItem)
dcp_bItem.update( d(real_seefrom=real_seefrom , real_seeto=real_seeto, bridge_name=bridge_name) )
bridges.append(dcp_bItem)
si.expected_alive_procs.append(bridge_name)
si.bridges = bridges
OG = d(dyncfg=dyncfg, uc=uc)
return si, layer1_cfg, OG
def start_lyrs_recursive_jobs(si, layer1_cfg): # 这是给最外层启动时把layer1_cfg作为cfg传入的
recursive_lyrs_jobs(si, layer1_cfg, None, [])
recr_rm_empty_lyr(si, layer1_cfg)
recursive_valid_lyrs(si, layer1_cfg)
def recursive_lyrs_jobs(si, cfg, parent_cfg, used_layer_names): # cfg:要处理的层, parent_cfg : 其父层
# 计算本层深度
cfg.depth = parent_cfg.depth + 1 if parent_cfg is not None else 1
CHK( cfg.layer_name, "Some layer has no layer_name")
CHK( re.match(r'^[a-zA-Z0-9_-]+$', cfg.layer_name), f"layer_name can only contain letters, numbers, '-', '_' . This name is invalid: {cfg.layer_name}" )
CHK( cfg.layer_name not in resv_words, f"Layer name {cfg.layer_name} conflicts with reserved word {resv_words}")
CHK( cfg.layer_name.startswith('layer'), f"Layer name {cfg.layer_name} does not start with 'layer'")
CHK( cfg.layer_name not in used_layer_names, f"Layer name '{cfg.layer_name}' is duplicated")
used_layer_names.append(cfg.layer_name)
CHK( len(cfg.layer_name.encode()) <= 15 , f"Layer name {cfg.layer_name} exceeds 15 bytes")
# 配置中的数组类型去除None成员
if cfg.fs:
cfg.fs = [fsItem for fsItem in cfg.fs if fsItem is not None]
if cfg.sublayers :
cfg.sublayers = [sublyr for sublyr in cfg.sublayers if sublyr is not None]
if cfg.subprocs :
cfg.subprocs = [cmd for cmd in cfg.subprocs if cmd is not None]
CHK( cfg.unshare_pid and cfg.unshare_mnt, f"Layer {cfg.layer_name} has subprocs but unshare_pid + unshare_mnt not enabled")
for subpItem in cfg.subprocs:
if subpItem.start_after:
subpItem.start_after = [item for item in subpItem.start_after if item is not None]
if cfg.subprocs and cfg.sublayers:
raise_exit(f"Layer {cfg.layer_name} has both subprocs and sublayers. Not valid config")
if cfg.envs_unset:
cfg.envs_unset = [item for item in cfg.envs_unset if item is not None]
if cfg.envset_grps:
cfg.envset_grps = [item for item in cfg.envset_grps if item is not None]
if cfg.start_after:
cfg.start_after = [item for item in cfg.start_after if item is not None]
if cfg.uid_map_as_root :
CHK( cfg.unshare_user, f"Layer {cfg.layer_name} has uid_map_as_* but unshare_user not enabled")
if cfg.unshare_pid and not cfg.unshare_mnt:
raise_exit(f"Layer {cfg.layer_name} has unshare_pid enabled, but unshare_mnt not enabled")
if (cfg.newrootfs or cfg.fs) and not cfg.unshare_mnt:
raise_exit(f"Layer {cfg.layer_name} sets newrootfs or fs, but unshare_mnt not enabled")
if bool(cfg.fs) != bool(cfg.newrootfs):
raise_exit(f"Layer {cfg.layer_name}: fs and newrootfs must both be present or both absent")
if cfg.is_mainlyr :
CHK( cfg.unshare_pid , f'Main layer {cfg.layer_name} requires unshare_pid=True')
if cfg.is_semitruCmpannLyr :
CHK( cfg.unshare_pid , f'Semi-trusted companion process layer {cfg.layer_name} requires unshare_pid=True')
# 检查fs条目
for fsItem in (cfg.fs or []):
if fsItem.dest: fsItem.dest = napath(fsItem.dest)
if fsItem.src: fsItem.src = napath(fsItem.src)
if fsItem.destbase: fsItem.destbase = napath(fsItem.destbase)
if len(cfg.sublayers or []) > 0 and cfg.newrootfs:
if not any( opItem.many_op == 'sbxdir-in-newrootfs' for opItem in cfg.fs):
raise_exit(f"Layer {cfg.layer_name} sets newrootfs and wants to create sublayers, but its fs has no entry with many_op = 'sbxdir-in-newrootfs' (required in this case)")
# 对第1层检查
if cfg.depth == 1:
CHK( cfg.uid_map_as_root,"First layer should enable uid_map_as_root")
CHK( cfg.unshare_pid, "First layer should enable unshare_pid")
CHK( len(cfg.sublayers) == 1, "First layer's sublayers array should but does not contain exactly 1 element")
CHK( not cfg.newrootfs, "First layer should not enable newrootfs")
if cfg.depth > 1:
CHK(not cfg.unshare_user, f"Layer {cfg.layer_name} has unshare_user enabled, but layers after the first layer do not need this. We have userns_unpri")
# 对第2层检查
if cfg.depth == 2:
CHK( cfg.unshare_mnt, "Second layer should enable unshare_mnt")
CHK( cfg.newrootfs, "Second layer should enable newrootfs")
CHK( cfg.fs, "Second layer should have fs")
if not any( opItem.many_op == 'dup-rootfs' for opItem in cfg.fs):
raise_exit("Second layer's fs has no entry with many_op='dup-rootfs'")
if not any( opItem.many_op == 'mask-privacy' for opItem in cfg.fs):
raise_exit("Second layer's fs has no entry with many_op='mask-privacy'")
if cfg.layer_name == 'layer3': # 对第3层检查
if cfg.fs and any( opItem.many_op == 'dup-rootfs' for opItem in cfg.fs) :
raise_exit(f"Layer {cfg.layer_name} should not use many_op='dup-rootfs' in fs, because its parent layer is the last layer allowed to see host files")
if not (cfg.unshare_mnt and cfg.unshare_cgroup and cfg.unshare_ipc and cfg.unshare_time and cfg.unshare_uts and cfg.newrootfs and cfg.fs) :
raise_exit(f"Layer {cfg.layer_name} did not enable all of [unshare_mnt, unshare_cgroup, unshare_ipc, unshare_time, unshare_uts, newrootfs, fs] (all required)")
if not any( opItem.many_op == 'container-rootfs' for opItem in cfg.fs):
raise_exit(f"Layer {cfg.layer_name}'s fs has no entry with many_op='container-rootfs'")
if cfg.layer_name in ['layer2c', 'layer4c', 'layer4']:
CHK( cfg.unshare_pid, f"{cfg.layer_name} did not enable unshare_pid=True (required)")
if parent_cfg is None:
pa_tree = []
pa_pidns_depth = 0
pa_pidns_tree = []
else:
pa_tree = parent_cfg.tree
pa_pidns_depth = parent_cfg.pidns_depth
pa_pidns_tree = parent_cfg.pidns_tree
cfg.tree = pa_tree + [cfg.layer_name]
cfg.pidns_depth = pa_pidns_depth + (0 if not cfg.unshare_pid else 1)
cfg.pidns_tree = pa_pidns_tree + ([] if not cfg.unshare_pid else [cfg.layer_name])
if cfg.user_shell or cfg.dev_shell:
if cfg.sublayers:
log_warn(f"{cfg.layer_name} is set to start dev_shell or user_shell, its sublayers will be ignored")
cfg.sublayers = []
# if cfg.subprocs and [x for x in cfg.subprocs if x.subp_name == 'mainApp']: # 现在mainApp是由最外层发来的了
for sublyr_cfg in (cfg.sublayers or []):
recursive_lyrs_jobs(si, sublyr_cfg, cfg, used_layer_names)
def recursive_valid_lyrs(si, layer1_cfg):
used_proc_names = []
si.all_layers = []
si.specialLyrs = d()
def _recr(cfg):
nonlocal used_proc_names
CHK( cfg.layer_name not in used_proc_names, f"Name {cfg.layer_name} is duplicated")
si.all_layers.append(cfg.layer_name)
if cfg.unshare_pid:
used_proc_names.append(cfg.layer_name)
if cfg.is_mainlyr:
CHK(not si.specialLyrs.mainLyr, 'Duplicate mainLyr found')
si.specialLyrs.mainLyr = cfg.layer_name
if cfg.is_semitruCmpannLyr:
CHK(not si.specialLyrs.semitruCmpannLyr, 'Duplicate semitruCmpannLyr found')
si.specialLyrs.semitruCmpannLyr = cfg.layer_name
for subpItem in (cfg.subprocs or [] ):
CHK( subpItem.subp_name, f"Subprocess has no subp_name set : {subpItem}")
CHK( re.match(r'^[a-zA-Z0-9_-]+$', subpItem.subp_name), f"subp_name can only contain letters, numbers, '-', '_' . This name is invalid: {subpItem.subp_name}" )
CHK( len(subpItem.subp_name)<=30, f"subp_name too long, exceeds 30 characters: {subpItem}")
CHK( subpItem.subp_name not in used_proc_names, f"Name {subpItem.subp_name} is duplicated")
for x in resv_name_prefix:
CHK( not subpItem.subp_name.startswith(x), f"Subprocess name {subpItem.subp_name} starting with '{x}' is invalid {subpItem}")
used_proc_names.append(subpItem.subp_name)
if cfg.user_shell: used_proc_names.append('user_shell')
if cfg.dev_shell: used_proc_names.append('dev_shell')
for sublyr_cfg in (cfg.sublayers or [] ):
_recr(sublyr_cfg)
_recr(layer1_cfg)
wdg_target_procs = [x for x in used_proc_names if x != 'mainApp'] # 不看主app, 只看它所属层
si.expected_alive_procs = wdg_target_procs + ['userns_unpri']
si.expected_alive_layers = list(set(si.expected_alive_procs) & set(si.all_layers))
CHK(si.specialLyrs.mainLyr, 'mainLyr not found')
def recr_rm_empty_lyr(si, cfg):
def _recr(si, cfg):
# print(cfg.layer_name)
have_rmed = False
cnt_cmds_0 = len(cfg.subprocs or [] )
cnt_sl_0 = len(cfg.sublayers or [] )
cnt_task_0 = len(cfg.daemon_tasks or [])
if cfg.subprocs : cfg.subprocs = [cmd for cmd in cfg.subprocs if cmd is not None]
if cfg.sublayers : cfg.sublayers = [sublyr for sublyr in cfg.sublayers if sublyr and not sublyr.disabled]
if cfg.daemon_tasks : cfg.daemon_tasks = [task for task in cfg.daemon_tasks if task]
cnt_cmds_1 = len(cfg.subprocs or [] )
cnt_sl_1 = len(cfg.sublayers or [] )
cnt_task_1 = len(cfg.daemon_tasks or [])
if cnt_cmds_0 != cnt_cmds_1 or cnt_sl_0 != cnt_sl_1 or cnt_task_0 != cnt_task_1:
have_rmed = True
for sublyr_cfg in (cfg.sublayers or [] ):
if _recr(si, sublyr_cfg):
have_rmed = True
if not (cfg.sublayers or cfg.subprocs or cfg.daemon_tasks or cfg.user_shell or cfg.dev_shell or cfg.is_mainlyr):
# print('setting' , cfg.layer_name, 'to disable')
cfg.disabled = True
have_rmed = True
# print(have_rmed)
return have_rmed
while _recr(si, cfg): pass
def make_mnt_fill_sbxdir(si, lyrcfg, call_at_begin=None, call_at_buildfs=None, OG=None): # 创建本层的sbxdir, 可能是刚启动时新创建,也可能是准备变根前为变根后的环境内创建(可能复制启动时已有的)
# sbxdir_path/ :
# dirmaker.xxx.name
# dirmaker.name -> dirmaker.xxx.name
# sbxinfo.json
# bootsbx.py
# sbx.xxx.name
# sbx.name -> sbx.xxx.name
# events.layers.log
# lyr_cfg.xxx.json (多) 包括本层和所有递归子层
# new.xxx.rootfs (多)所有有 newrootfs 的本层和递归子层
# temp/ 挂载为rw tmpfs
# apps/ 挂为 tmpfs rw
# overlays.xxx.dirs/ 挂载为tmpfs 可能rw (暂未实现)
if call_at_begin: # 刚启动脚本
si.instance_name , si.outest_sbxdir, si.CG_SBX = NameMng.gen_instance_name_mkdir()
target_sbxdir_path = napath(si.outest_sbxdir)
old_sbxdir_path = None
elif call_at_buildfs: # 为本层接下来的新文件系统准备的 (可能 变根=新旧路径不同 ,也可能 不变根=新旧路径同)
target_sbxdir_path = napath(f'{lyrcfg.newrootfs_path}/{lyrcfg.sbxdir_path1}')
old_sbxdir_path = napath(lyrcfg.sbxdir_path0)
if target_sbxdir_path == old_sbxdir_path:
return
# 能往下执行,说明是要从空白创建
# else:
# creating_new_sbxdir=True
def make_file_get_fd(filename, open_flag, filemode):
fd = os.open(f'{target_sbxdir_path}/{filename}', open_flag, filemode)
set_fd_keep_on_exec(fd, False)
return fd
def create_socket_file_fd(socket_file_name):
skt = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
skt.setblocking(False)
skt.bind(f'{target_sbxdir_path}/{socket_file_name}')
fd = skt.detach() ; set_fd_keep_on_exec(fd, False)
return fd
def create_socketpair_fds():
skt_chd, skt_pa = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
fd_chd = skt_chd.detach() ; set_fd_keep_on_exec(fd_chd, False)
fd_pa = skt_pa.detach() ; set_fd_keep_on_exec(fd_pa, False) # 为了不让fd号码乱,pa也保留
return d(pa=fd_pa, chd=fd_chd)
# sbxdir 本身目录创建
mkdirp(target_sbxdir_path)
new_tmpfs_for_sbxdir = True if call_at_buildfs else False
if new_tmpfs_for_sbxdir:
mount('tmpfs', target_sbxdir_path, 'tmpfs', mntflag_newsbxdir, 'mode=700')
# dirmaker.layerX.name
if not os.path.lexists(f'{target_sbxdir_path}/dirmaker.layer.name'):
with open(f'{target_sbxdir_path}/dirmaker.layer.{lyrcfg.layer_name}.name', 'w') as f:
f.write(lyrcfg.layer_name)
os.chmod(f.name, 0o444)
symlink(f'dirmaker.layer.{lyrcfg.layer_name}.name', f'{target_sbxdir_path}/dirmaker.layer.name')
if call_at_begin:
# sbx.xxxx.pid
with open(f'{si.outest_sbxdir}/sbx.{si.outest_pid}.pid', 'w') as f:
f.write(str(si.outest_pid))
os.chmod(f.name, 0o444)