#!/usr/bin/env python3 """ NBD server for PERC H710 RAID recovery. Applies three transformations on every read: 1. Chunk translation — skips every 5th 64KB chunk (PERC internal metadata) 2. Superblock patch — clears metadata_csum and has_journal feature bits so the kernel stops validating checksums we can't fix 3. GDT reconstruction — synthesizes correct group descriptor entries for regions that fall inside metadata chunks (zeros) Confirmed filesystem parameters (from session forensics): bpg = 32768 blocks per group ipg = 8192 inodes per group inode_size = 256 bytes GDT entry = 64 bytes (64bit feature) num_groups = 35728 Group N: bb = 1038+N, ib = 1054+N, it = 1070 + N*512 Usage: python3 nbd_server_v2.py & nbd-client -g 127.0.0.1 10809 /dev/nbd0 mount -o ro,norecovery -t ext4 /dev/nbd0 /mnt/root """ import socket import struct import threading import os import sys # ── Physical layout ─────────────────────────────────────────────────────────── 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 block count # ── ext4 filesystem parameters ──────────────────────────────────────────────── BSIZE = 4096 # block size in bytes BPG = 32768 # blocks per group IPG = 8192 # inodes per group INODE_SIZE = 256 # inode size in bytes GDT_ENTRY = 64 # group descriptor size (64-bit mode) NUM_GROUPS = 35728 # total number of block groups # GDT starts at block 1 = byte 4096 from LV start GDT_START_VIRT = BSIZE # virtual byte offset of GDT GDT_END_VIRT = BSIZE + NUM_GROUPS * GDT_ENTRY # ~9.1 MB # Superblock is at virtual byte 1024 SB_VIRT_OFFSET = 1024 SB_SIZE = 1024 # Feature flag offsets within superblock SB_FEAT_COMPAT_OFF = 92 # s_feature_compat SB_FEAT_INCOMPAT_OFF = 96 # s_feature_incompat SB_FEAT_RO_COMPAT_OFF = 100 # s_feature_ro_compat SB_CHECKSUM_OFF = 1020 # s_checksum (last 4 bytes of sb) # Bits to clear INCOMPAT_HAS_JOURNAL = 0x00000004 RO_COMPAT_METADATA_CSUM = 0x00000400 RO_COMPAT_GDT_CSUM = 0x00000010 # Cached patched superblock (built once on first read) _patched_sb = None _sb_lock = threading.Lock() # ── NBD protocol constants ──────────────────────────────────────────────────── NBD_MAGIC = 0x4e42444d41474943 NBD_CLISERV_MAGIC = 0x00420281861253 NBD_REQUEST_MAGIC = 0x25609513 NBD_REPLY_MAGIC = 0x67446698 NBD_CMD_READ = 0 NBD_CMD_WRITE = 1 NBD_CMD_DISC = 2 NBD_FLAG_READ_ONLY = 0x0002 NBD_FLAG_HAS_FLAGS = 0x0001 # ── Chunk translation ───────────────────────────────────────────────────────── def virt_to_phys_segments(virt_offset, length): """ Yield (phys_offset_or_None, seg_length) pairs covering the request. None means the region falls in a PERC metadata chunk → return zeros. """ remaining = length pos = virt_offset 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 chunk_remain = CHUNK_BYTES - intra seg_len = min(chunk_remain, remaining) if chunk_idx == 4: yield None, seg_len else: phys = (LV_PHYS_START + group * 4 * CHUNK_BYTES + chunk_idx * CHUNK_BYTES + intra) yield phys, seg_len pos += seg_len remaining -= seg_len def raw_read(virt_offset, length): """Read virtual bytes without any patching.""" result = bytearray(length) written = 0 with open(DEV, 'rb') as f: for phys, seg_len in virt_to_phys_segments(virt_offset, length): if phys is not None: f.seek(phys) data = f.read(seg_len) result[written:written + len(data)] = data written += seg_len else: written += seg_len # leave as zeros return bytes(result) # ── Group descriptor synthesis ──────────────────────────────────────────────── def make_gdt_entry(group_num): """ Build a 64-byte group descriptor for group N. Pattern confirmed from session forensics: block_bitmap_lo = 1038 + N inode_bitmap_lo = 1054 + N inode_table_lo = 1070 + N * 512 We set free counts to 0 and used_dirs to 0 — the kernel will recompute them if needed. Checksum is left zero (we cleared metadata_csum). """ gd = bytearray(GDT_ENTRY) bb = 1038 + group_num ib = 1054 + group_num it = 1070 + group_num * 512 struct.pack_into('= GDT_END_VIRT: continue gdt_rel = abs_byte - GDT_START_VIRT grp = gdt_rel // GDT_ENTRY byte_in = gdt_rel % GDT_ENTRY if grp < NUM_GROUPS: entry = make_gdt_entry(grp) result[i] = entry[byte_in] return bytes(result) def overlaps(a_start, a_len, b_start, b_len): return a_start < b_start + b_len and b_start < a_start + a_len # ── Superblock patching ─────────────────────────────────────────────────────── def get_patched_superblock(): """ Read the real superblock once, clear problematic feature bits, cache it. Clears: - has_journal (skip journal replay) - metadata_csum (stop checksum validation of bitmaps/GDT) - gdt_csum (older checksum variant) """ global _patched_sb with _sb_lock: if _patched_sb is not None: return _patched_sb sb = bytearray(raw_read(SB_VIRT_OFFSET, SB_SIZE)) incompat = struct.unpack_from(' 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) if chunk_idx == 4: # This is a metadata chunk — was read as zeros # If it overlaps the GDT, synthesize seg_end = pos + seg_len if overlaps(pos, seg_len, GDT_START_VIRT, GDT_END_VIRT - GDT_START_VIRT): synth = synthesize_gdt_region(pos, seg_len) dst_off = pos - virt_offset data[dst_off:dst_off + seg_len] = synth pos += seg_len remaining -= seg_len return bytes(data) # ── NBD protocol (old-style handshake) ─────────────────────────────────────── def handle_client(conn, addr): print(f'[nbd] client connected from {addr}') try: # Old-style handshake: magic + cliserv_magic + size + flags + 124 pad conn.sendall(b'NBDMAGIC') conn.sendall(struct.pack('>Q', NBD_CLISERV_MAGIC)) conn.sendall(struct.pack('>Q', VIRT_SIZE)) conn.sendall(struct.pack('>H', NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY)) conn.sendall(b'\x00' * 124) print(f'[nbd] handshake done, serving {VIRT_SIZE // (1024**3):.1f} GB') while True: hdr = b'' while len(hdr) < 28: chunk = conn.recv(28 - len(hdr)) if not chunk: return hdr += chunk magic, flags, cmd, handle, offset, length = \ struct.unpack('>IHHQQI', hdr) if magic != NBD_REQUEST_MAGIC: print(f'[nbd] bad magic 0x{magic:08x}, closing') return if cmd == NBD_CMD_READ: try: payload = read_virtual(offset, length) except Exception as e: print(f'[nbd] read error offset={offset} len={length}: {e}') payload = b'\x00' * length reply = struct.pack('>IIQ', NBD_REPLY_MAGIC, 0, handle) conn.sendall(reply + payload) elif cmd == NBD_CMD_DISC: print(f'[nbd] client disconnected cleanly') return else: # Return error for writes and other commands reply = struct.pack('>IIQ', NBD_REPLY_MAGIC, 1, handle) conn.sendall(reply) except (ConnectionResetError, BrokenPipeError): print(f'[nbd] client {addr} dropped connection') except Exception as e: print(f'[nbd] error: {e}') finally: conn.close() def main(): print(f'[nbd] PERC H710 recovery NBD server v2') print(f'[nbd] device: {DEV}') print(f'[nbd] lv_start: byte {LV_PHYS_START} (sector {LV_PHYS_START//512})') print(f'[nbd] virt_size: {VIRT_SIZE // (1024**3):.1f} GB') print(f'[nbd] features: chunk-skip + sb-patch + gdt-synth') print() server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 10809)) server.listen(5) print(f'[nbd] listening on 127.0.0.1:10809') print(f'[nbd] connect with: nbd-client -g 127.0.0.1 10809 /dev/nbd0') print(f'[nbd] then mount: mount -o ro,norecovery -t ext4 /dev/nbd0 /mnt/root') print() while True: conn, addr = server.accept() t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True) t.start() if __name__ == '__main__': main()