#!/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('= 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()