Files
ext4recovery/test/nbd_server_v6.py
2026-04-30 11:04:05 +00:00

272 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
NBD server v6 — chunk translation + backup GDT overlay.
The primary GDT has invalid checksums (written through PERC which stored
its own checksums). The backup GDT at block group 1 has valid checksums.
We serve the backup GDT bytes at the primary GDT location so the kernel
can validate and mount the filesystem.
Reads the backup GDT once at startup and caches it.
All other reads: pure chunk translation, no modification.
Usage:
python3 nbd_server_v6.py &
nbd-client 127.0.0.1 10809 /dev/nbd0 -N ""
mount -o ro,norecovery -t ext4 /dev/nbd0 /mnt/root
"""
import socket
import struct
import threading
import sys
DEV = '/dev/md0'
CHUNK_BYTES = 128 * 512 # 64 KB
LV_PHYS_START = 5120000 * 512 # byte 2,621,440,000
VIRT_SIZE = 9365766144 * 512 # from superblock
BSIZE = 4096
GDT_ENTRY = 64
BPG = 32768
NUM_GROUPS = 35728
# Primary GDT: virtual bytes 4096 to 4096+NUM_GROUPS*64
PRIMARY_GDT_START = BSIZE
PRIMARY_GDT_SIZE = NUM_GROUPS * GDT_ENTRY
PRIMARY_GDT_END = PRIMARY_GDT_START + PRIMARY_GDT_SIZE
# Backup GDT: at block group 1, block 1 = (BPG+1)*BSIZE
BACKUP_GDT_START = (BPG + 1) * BSIZE
# Cached backup GDT (loaded at startup)
_backup_gdt = None
# NBD protocol
NBDMAGIC = 0x4e42444d41474943
IHAVEOPT = 0x49484156454F5054
REPLYMAGIC = 0x3e889045565a9
NBD_OPT_EXPORT_NAME = 1
NBD_OPT_ABORT = 2
NBD_OPT_LIST = 3
NBD_OPT_GO = 7
NBD_REP_ACK = 1
NBD_REP_SERVER = 2
NBD_REP_INFO = 3
NBD_REP_ERR_UNSUP = (1 << 31) | 1
NBD_INFO_EXPORT = 0
NBD_FLAG_HAS_FLAGS = 1 << 0
NBD_FLAG_READ_ONLY = 1 << 1
NBD_FLAG_SEND_FLUSH = 1 << 2
NBD_REQUEST_MAGIC = 0x25609513
NBD_REPLY_MAGIC = 0x67446698
NBD_CMD_READ = 0
NBD_CMD_DISC = 2
NBD_CMD_FLUSH = 3
TX_FLAGS = NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY | NBD_FLAG_SEND_FLUSH
def raw_read(virt_offset, length):
"""Pure chunk translation — no modifications."""
result = bytearray(length)
pos = virt_offset
remaining = length
with open(DEV, 'rb') as f:
while remaining > 0:
group = pos // (5 * CHUNK_BYTES)
in_group = pos % (5 * CHUNK_BYTES)
chunk_idx = in_group // CHUNK_BYTES
intra = in_group % CHUNK_BYTES
seg_len = min(CHUNK_BYTES - intra, remaining)
dst_off = pos - virt_offset
if chunk_idx != 4:
phys = (LV_PHYS_START
+ group * 4 * CHUNK_BYTES
+ chunk_idx * CHUNK_BYTES
+ intra)
f.seek(phys)
chunk = f.read(seg_len)
result[dst_off:dst_off + len(chunk)] = chunk
pos += seg_len
remaining -= seg_len
return bytes(result)
def load_backup_gdt():
"""Read and cache the backup GDT from group 1."""
global _backup_gdt
print(f'[gdt] loading backup GDT from virtual byte {BACKUP_GDT_START}...')
_backup_gdt = raw_read(BACKUP_GDT_START, PRIMARY_GDT_SIZE)
# Verify first few entries look sane
ok = True
for i in range(min(5, NUM_GROUPS)):
e = _backup_gdt[i*GDT_ENTRY:(i+1)*GDT_ENTRY]
bb = struct.unpack_from('<I', e, 0)[0]
ib = struct.unpack_from('<I', e, 4)[0]
it = struct.unpack_from('<I', e, 8)[0]
cs = struct.unpack_from('<H', e, 30)[0]
print(f'[gdt] group {i}: bb={bb} ib={ib} it={it} csum=0x{cs:04x}')
if bb == 0 and ib == 0 and it == 0:
ok = False
if not ok:
print('[gdt] WARNING: backup GDT looks empty, check parameters')
else:
print(f'[gdt] backup GDT loaded OK ({PRIMARY_GDT_SIZE//1024}KB)')
return ok
def read_virtual(virt_offset, length):
"""
Read with backup GDT overlay.
Primary GDT region (virtual bytes 4096..4096+NUM_GROUPS*64) is
served from the cached backup GDT instead of the primary location.
Everything else is pure chunk translation.
"""
req_end = virt_offset + length
# Fast path: no overlap with primary GDT
if req_end <= PRIMARY_GDT_START or virt_offset >= PRIMARY_GDT_END:
return raw_read(virt_offset, length)
# Build result from possibly multiple segments
data = bytearray(raw_read(virt_offset, length))
# Overlay backup GDT where request overlaps primary GDT
ol_start = max(virt_offset, PRIMARY_GDT_START)
ol_end = min(req_end, PRIMARY_GDT_END)
if ol_start < ol_end and _backup_gdt is not None:
src_off = ol_start - PRIMARY_GDT_START
dst_off = ol_start - virt_offset
n = ol_end - ol_start
data[dst_off:dst_off + n] = _backup_gdt[src_off:src_off + n]
return bytes(data)
def recv_all(conn, n):
buf = b''
while len(buf) < n:
d = conn.recv(n - len(buf))
if not d:
raise ConnectionError('disconnected')
buf += d
return buf
def send_reply(conn, opt, rtype, data=b''):
conn.sendall(struct.pack('>Q', REPLYMAGIC))
conn.sendall(struct.pack('>I', opt))
conn.sendall(struct.pack('>I', rtype))
conn.sendall(struct.pack('>I', len(data)))
if data:
conn.sendall(data)
def handle_client(conn, addr):
print(f'[nbd] {addr} connected')
try:
conn.sendall(struct.pack('>Q', NBDMAGIC))
conn.sendall(struct.pack('>Q', IHAVEOPT))
conn.sendall(struct.pack('>H', 0x0003))
recv_all(conn, 4)
while True:
hdr = recv_all(conn, 16)
_, opt, opt_len = struct.unpack('>QII', hdr)
opt_data = recv_all(conn, opt_len) if opt_len else b''
if opt == NBD_OPT_EXPORT_NAME:
conn.sendall(struct.pack('>Q', VIRT_SIZE))
conn.sendall(struct.pack('>H', TX_FLAGS))
break
elif opt == NBD_OPT_GO:
info = struct.pack('>HQH', NBD_INFO_EXPORT, VIRT_SIZE, TX_FLAGS)
send_reply(conn, opt, NBD_REP_INFO, info)
send_reply(conn, opt, NBD_REP_ACK)
break
elif opt == NBD_OPT_LIST:
send_reply(conn, opt, NBD_REP_SERVER,
struct.pack('>I', 0))
send_reply(conn, opt, NBD_REP_ACK)
elif opt == NBD_OPT_ABORT:
send_reply(conn, opt, NBD_REP_ACK)
return
else:
send_reply(conn, opt, NBD_REP_ERR_UNSUP)
print(f'[nbd] {addr} transmission ({VIRT_SIZE//1024//1024//1024}GB)')
while True:
hdr = recv_all(conn, 28)
magic, flags, cmd, handle, offset, length = \
struct.unpack('>IHHQQI', hdr)
if magic != NBD_REQUEST_MAGIC:
return
if cmd == NBD_CMD_READ:
try:
payload = read_virtual(offset, length)
except Exception as e:
print(f'[nbd] read error @ {offset}+{length}: {e}')
payload = b'\x00' * length
conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 0, handle))
conn.sendall(payload)
elif cmd == NBD_CMD_FLUSH:
conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 0, handle))
elif cmd == NBD_CMD_DISC:
return
else:
conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 1, handle))
except (ConnectionError, BrokenPipeError, ConnectionResetError):
pass
except Exception as e:
print(f'[nbd] {addr} error: {e}')
import traceback; traceback.print_exc()
finally:
conn.close()
def main():
print('PERC H710 recovery NBD server v6')
print(f' device : {DEV}')
print(f' lv_start : byte {LV_PHYS_START}')
print(f' virt_size : {VIRT_SIZE//1024//1024//1024} GB')
print(f' primary GDT: virtual bytes {PRIMARY_GDT_START}-{PRIMARY_GDT_END}')
print(f' backup GDT : virtual byte {BACKUP_GDT_START}')
print()
if not load_backup_gdt():
print('ERROR: backup GDT load failed, check BACKUP_GDT_START')
sys.exit(1)
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('127.0.0.1', 10809))
srv.listen(5)
print()
print('Listening on 127.0.0.1:10809')
print(' nbd-client 127.0.0.1 10809 /dev/nbd0 -N ""')
print(' mount -o ro,norecovery -t ext4 /dev/nbd0 /mnt/root')
print()
while True:
conn, addr = srv.accept()
threading.Thread(target=handle_client, args=(conn, addr),
daemon=True).start()
if __name__ == '__main__':
main()