@braedonsaunders/appkit-desk 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -1
- package/backend.d.ts +32 -7
- package/backend.d.ts.map +1 -1
- package/backend.js +107 -524
- package/backend.js.map +1 -1
- package/host.d.ts +27 -1
- package/host.d.ts.map +1 -1
- package/host.js +47 -19
- package/host.js.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/machine.d.ts +154 -0
- package/machine.d.ts.map +1 -0
- package/machine.js +573 -0
- package/machine.js.map +1 -0
- package/package.json +2 -2
- package/plan.d.ts +192 -31
- package/plan.d.ts.map +1 -1
- package/plan.js +298 -12
- package/plan.js.map +1 -1
- package/port-relay.d.ts +13 -0
- package/port-relay.d.ts.map +1 -0
- package/port-relay.js +56 -0
- package/port-relay.js.map +1 -0
- package/virtualization.d.ts +16 -0
- package/virtualization.d.ts.map +1 -0
- package/virtualization.js +156 -0
- package/virtualization.js.map +1 -0
- package/vz/Package.swift +23 -0
- package/vz/Sources/DeskNet/Addresses.swift +106 -0
- package/vz/Sources/DeskNet/DHCP.swift +111 -0
- package/vz/Sources/DeskNet/Fence.swift +57 -0
- package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
- package/vz/Sources/DeskNet/Packets.swift +355 -0
- package/vz/Sources/DeskNet/Plumbing.swift +312 -0
- package/vz/Sources/DeskNet/Sockets.swift +221 -0
- package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
- package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
- package/vz/Sources/appkit-desk-vz/Helper.swift +550 -0
- package/vz/Sources/appkit-desk-vz/main.swift +38 -0
- package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
- package/vz/appkit-desk-vz.entitlements +8 -0
- package/vz/build.sh +23 -0
- package/vz/check/extract-boot.py +221 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Copy a Debian disk image's kernel and initrd out of its ext4 root, read-only.
|
|
3
|
+
|
|
4
|
+
extract-boot.py <disk.raw> <out-dir>
|
|
5
|
+
|
|
6
|
+
Writes <out-dir>/Image (an uncompressed arm64 Image, which is what
|
|
7
|
+
VZLinuxBootLoader boots), <out-dir>/initrd, and <out-dir>/release (the kernel
|
|
8
|
+
version). The kernel and initrd come from the image itself because that is the
|
|
9
|
+
only place Debian publishes the initrd that matches the image's modules.
|
|
10
|
+
|
|
11
|
+
macOS cannot mount ext4, so this reads the filesystem directly: GPT, the
|
|
12
|
+
superblock, group descriptors, inodes, extent trees and directories. It only
|
|
13
|
+
reads, and it supports exactly what a stock Debian cloud image uses.
|
|
14
|
+
"""
|
|
15
|
+
import gzip
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import struct
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import zlib
|
|
22
|
+
|
|
23
|
+
SECTOR = 512
|
|
24
|
+
EXT4_MAGIC = 0xEF53
|
|
25
|
+
EXTENTS_FLAG = 0x80000
|
|
26
|
+
INLINE_DATA_FLAG = 0x10000000
|
|
27
|
+
INCOMPAT_64BIT = 0x80
|
|
28
|
+
S_IFMT = 0o170000
|
|
29
|
+
S_IFDIR = 0o040000
|
|
30
|
+
S_IFLNK = 0o120000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def partitions(image):
|
|
34
|
+
image.seek(SECTOR)
|
|
35
|
+
header = image.read(92)
|
|
36
|
+
if header[:8] != b"EFI PART":
|
|
37
|
+
return [0]
|
|
38
|
+
entries_lba, count, size = struct.unpack_from("<QII", header, 72)
|
|
39
|
+
offsets = []
|
|
40
|
+
for index in range(count):
|
|
41
|
+
image.seek(entries_lba * SECTOR + index * size)
|
|
42
|
+
entry = image.read(size)
|
|
43
|
+
if entry[:16] == b"\0" * 16:
|
|
44
|
+
continue
|
|
45
|
+
first = struct.unpack_from("<Q", entry, 32)[0]
|
|
46
|
+
offsets.append(first * SECTOR)
|
|
47
|
+
return offsets
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Ext4:
|
|
51
|
+
def __init__(self, image, offset):
|
|
52
|
+
self.image = image
|
|
53
|
+
self.offset = offset
|
|
54
|
+
superblock = self.read(1024, 1024)
|
|
55
|
+
if struct.unpack_from("<H", superblock, 0x38)[0] != EXT4_MAGIC:
|
|
56
|
+
raise ValueError("not ext4")
|
|
57
|
+
self.block_size = 1024 << struct.unpack_from("<I", superblock, 0x18)[0]
|
|
58
|
+
self.first_data_block = struct.unpack_from("<I", superblock, 0x14)[0]
|
|
59
|
+
self.inodes_per_group = struct.unpack_from("<I", superblock, 0x28)[0]
|
|
60
|
+
self.inode_size = struct.unpack_from("<H", superblock, 0x58)[0]
|
|
61
|
+
incompat = struct.unpack_from("<I", superblock, 0x60)[0]
|
|
62
|
+
self.is64 = bool(incompat & INCOMPAT_64BIT)
|
|
63
|
+
self.desc_size = struct.unpack_from("<H", superblock, 0xFE)[0] if self.is64 else 32
|
|
64
|
+
self.gdt = (self.first_data_block + 1) * self.block_size
|
|
65
|
+
|
|
66
|
+
def read(self, position, length):
|
|
67
|
+
self.image.seek(self.offset + position)
|
|
68
|
+
data = self.image.read(length)
|
|
69
|
+
if len(data) != length:
|
|
70
|
+
raise ValueError("read past the end of the image")
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def inode(self, number):
|
|
74
|
+
group, index = divmod(number - 1, self.inodes_per_group)
|
|
75
|
+
desc = self.read(self.gdt + group * self.desc_size, self.desc_size)
|
|
76
|
+
table = struct.unpack_from("<I", desc, 0x8)[0]
|
|
77
|
+
if self.is64 and self.desc_size >= 64:
|
|
78
|
+
table |= struct.unpack_from("<I", desc, 0x28)[0] << 32
|
|
79
|
+
return self.read(table * self.block_size + index * self.inode_size, self.inode_size)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def mode(inode):
|
|
83
|
+
return struct.unpack_from("<H", inode, 0)[0]
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def size(inode):
|
|
87
|
+
low, high = struct.unpack_from("<I", inode, 0x4)[0], struct.unpack_from("<I", inode, 0x6C)[0]
|
|
88
|
+
return high << 32 | low
|
|
89
|
+
|
|
90
|
+
def extents(self, node):
|
|
91
|
+
magic, entries, _, depth = struct.unpack_from("<HHHH", node, 0)
|
|
92
|
+
if magic != 0xF30A:
|
|
93
|
+
raise ValueError("bad extent header")
|
|
94
|
+
for index in range(entries):
|
|
95
|
+
entry = node[12 + index * 12: 24 + index * 12]
|
|
96
|
+
if depth == 0:
|
|
97
|
+
logical, length, start_high, start_low = struct.unpack("<IHHI", entry)
|
|
98
|
+
uninitialized = length > 32768
|
|
99
|
+
if uninitialized:
|
|
100
|
+
length -= 32768
|
|
101
|
+
yield logical, length, start_high << 32 | start_low, uninitialized
|
|
102
|
+
else:
|
|
103
|
+
_, leaf_low, leaf_high = struct.unpack_from("<IIH", entry, 0)
|
|
104
|
+
child = self.read((leaf_high << 32 | leaf_low) * self.block_size, self.block_size)
|
|
105
|
+
yield from self.extents(child)
|
|
106
|
+
|
|
107
|
+
def contents(self, inode):
|
|
108
|
+
flags = struct.unpack_from("<I", inode, 0x20)[0]
|
|
109
|
+
size = self.size(inode)
|
|
110
|
+
if flags & INLINE_DATA_FLAG:
|
|
111
|
+
raise ValueError("inline data is not supported")
|
|
112
|
+
if not flags & EXTENTS_FLAG:
|
|
113
|
+
raise ValueError("block-mapped files are not supported")
|
|
114
|
+
data = bytearray(size)
|
|
115
|
+
for logical, length, physical, uninitialized in self.extents(inode[0x28:0x28 + 60]):
|
|
116
|
+
begin = logical * self.block_size
|
|
117
|
+
if begin >= size or uninitialized:
|
|
118
|
+
continue
|
|
119
|
+
count = min(length * self.block_size, size - begin)
|
|
120
|
+
data[begin:begin + count] = self.read(physical * self.block_size, count)
|
|
121
|
+
return bytes(data)
|
|
122
|
+
|
|
123
|
+
def entries(self, inode):
|
|
124
|
+
data = self.contents(inode)
|
|
125
|
+
position = 0
|
|
126
|
+
while position + 8 <= len(data):
|
|
127
|
+
number, record, name_length = struct.unpack_from("<IHB", data, position)
|
|
128
|
+
if record < 8:
|
|
129
|
+
break
|
|
130
|
+
if number:
|
|
131
|
+
yield data[position + 8: position + 8 + name_length].decode("utf-8", "replace"), number
|
|
132
|
+
position += record
|
|
133
|
+
|
|
134
|
+
def lookup(self, path, follow=True, depth=0):
|
|
135
|
+
if depth > 16:
|
|
136
|
+
raise ValueError("too many symbolic links")
|
|
137
|
+
number = 2
|
|
138
|
+
parts = [part for part in path.split("/") if part]
|
|
139
|
+
for index, part in enumerate(parts):
|
|
140
|
+
directory = self.inode(number)
|
|
141
|
+
if self.mode(directory) & S_IFMT != S_IFDIR:
|
|
142
|
+
raise FileNotFoundError(path)
|
|
143
|
+
found = dict(self.entries(directory)).get(part)
|
|
144
|
+
if found is None:
|
|
145
|
+
raise FileNotFoundError(path)
|
|
146
|
+
number = found
|
|
147
|
+
node = self.inode(number)
|
|
148
|
+
last = index == len(parts) - 1
|
|
149
|
+
if self.mode(node) & S_IFMT == S_IFLNK and (follow or not last):
|
|
150
|
+
target = self.symlink(node)
|
|
151
|
+
base = "/" + "/".join(parts[:index])
|
|
152
|
+
rest = "/".join(parts[index + 1:])
|
|
153
|
+
joined = target if target.startswith("/") else os.path.join(base, target)
|
|
154
|
+
return self.lookup(os.path.normpath(joined + ("/" + rest if rest else "")), follow, depth + 1)
|
|
155
|
+
return self.inode(number)
|
|
156
|
+
|
|
157
|
+
def symlink(self, inode):
|
|
158
|
+
size = self.size(inode)
|
|
159
|
+
flags = struct.unpack_from("<I", inode, 0x20)[0]
|
|
160
|
+
if size < 60 and not flags & EXTENTS_FLAG:
|
|
161
|
+
return inode[0x28:0x28 + size].decode()
|
|
162
|
+
return self.contents(inode).decode()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def uncompressed_image(kernel):
|
|
166
|
+
"""Debian's arm64 vmlinuz is a gzip'd Image or an EFI zboot image."""
|
|
167
|
+
if kernel[:2] == b"\x1f\x8b":
|
|
168
|
+
return gzip.decompress(kernel)
|
|
169
|
+
if kernel[:2] == b"MZ" and kernel[4:8] == b"zimg":
|
|
170
|
+
start, length = struct.unpack_from("<II", kernel, 8)
|
|
171
|
+
kind = kernel[24:32].rstrip(b"\0").decode()
|
|
172
|
+
payload = kernel[start:start + length]
|
|
173
|
+
if kind == "gzip":
|
|
174
|
+
return zlib.decompress(payload, 16 + zlib.MAX_WBITS)
|
|
175
|
+
if kind == "zstd":
|
|
176
|
+
zstd = shutil.which("zstd")
|
|
177
|
+
if not zstd:
|
|
178
|
+
raise SystemExit("this kernel is zstd-compressed EFI zboot; install zstd to unpack it")
|
|
179
|
+
return subprocess.run([zstd, "-dc"], input=payload, check=True, capture_output=True).stdout
|
|
180
|
+
raise SystemExit(f"unsupported EFI zboot compression: {kind}")
|
|
181
|
+
if kernel[0x38:0x3C] == b"ARM\x64":
|
|
182
|
+
return kernel
|
|
183
|
+
raise SystemExit("the kernel is neither an arm64 Image, a gzip'd Image, nor EFI zboot")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main():
|
|
187
|
+
if len(sys.argv) != 3:
|
|
188
|
+
raise SystemExit(__doc__)
|
|
189
|
+
disk, out = sys.argv[1], sys.argv[2]
|
|
190
|
+
with open(disk, "rb") as image:
|
|
191
|
+
for offset in partitions(image):
|
|
192
|
+
try:
|
|
193
|
+
fs = Ext4(image, offset)
|
|
194
|
+
boot = dict(fs.entries(fs.lookup("/boot")))
|
|
195
|
+
except (ValueError, FileNotFoundError):
|
|
196
|
+
continue
|
|
197
|
+
kernels = sorted(name for name in boot if name.startswith("vmlinuz-"))
|
|
198
|
+
if not kernels:
|
|
199
|
+
continue
|
|
200
|
+
release = kernels[-1][len("vmlinuz-"):]
|
|
201
|
+
if f"initrd.img-{release}" not in boot:
|
|
202
|
+
raise SystemExit(f"/boot has vmlinuz-{release} but no matching initrd")
|
|
203
|
+
kernel = fs.contents(fs.lookup(f"/boot/vmlinuz-{release}"))
|
|
204
|
+
initrd = fs.contents(fs.lookup(f"/boot/initrd.img-{release}"))
|
|
205
|
+
image_bytes = uncompressed_image(kernel)
|
|
206
|
+
if image_bytes[0x38:0x3C] != b"ARM\x64":
|
|
207
|
+
raise SystemExit("the unpacked kernel is not an arm64 Image")
|
|
208
|
+
os.makedirs(out, exist_ok=True)
|
|
209
|
+
with open(os.path.join(out, "Image"), "wb") as handle:
|
|
210
|
+
handle.write(image_bytes)
|
|
211
|
+
with open(os.path.join(out, "initrd"), "wb") as handle:
|
|
212
|
+
handle.write(initrd)
|
|
213
|
+
with open(os.path.join(out, "release"), "w") as handle:
|
|
214
|
+
handle.write(release + "\n")
|
|
215
|
+
print(release)
|
|
216
|
+
return
|
|
217
|
+
raise SystemExit("no ext4 partition with a kernel in /boot was found")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|