| 1 | #!/usr/bin/env python3 |
| 2 | """Compare SMB enumeration with a disposable Linux VM's filesystem and interrupt pagination.""" |
| 3 | import argparse |
| 4 | import json |
| 5 | import os |
| 6 | from pathlib import Path |
| 7 | import socket |
| 8 | import subprocess |
| 9 | import sys |
| 10 | import time |
| 11 | import uuid |
| 12 | |
| 13 | sys.path.insert(0, str(Path(__file__).resolve().parent / 'w7')) |
| 14 | from linux_vm import load_instance, ssh_argv |
| 15 | |
| 16 | |
| 17 | def main(): |
| 18 | parser = argparse.ArgumentParser(description=__doc__) |
| 19 | parser.add_argument('vm', help='An already running, caller-owned Linux lab VM') |
| 20 | parser.add_argument('output', type=Path) |
| 21 | args = parser.parse_args() |
| 22 | output = args.output.resolve() |
| 23 | output.mkdir(parents=True, exist_ok=False) |
| 24 | config = load_instance(args.vm) |
| 25 | root = 'directory-' + uuid.uuid4().hex[:12] |
| 26 | script = f'''from pathlib import Path |
| 27 | import json |
| 28 | root = Path('/srv/agent') / {root!r} |
| 29 | root.mkdir() |
| 30 | (root / 'empty').mkdir() |
| 31 | (root / 'nested š¦').mkdir() |
| 32 | (root / 'denied').mkdir() |
| 33 | (root / 'file.one').write_bytes(b'fixture') |
| 34 | for i in range(4000): |
| 35 | name = f'Section {{i:04d}} š¦ eĢ ' + 'x' * 80 + '.one' |
| 36 | with (root / name).open('wb') as file: file.truncate(i * 12345) |
| 37 | entries = [dict(name=p.name, directory=p.is_dir(), size=p.stat().st_size) for p in root.iterdir()] |
| 38 | (root / 'denied').chmod(0) |
| 39 | print(json.dumps(dict(path=root.name, entries=entries), ensure_ascii=False)) |
| 40 | ''' |
| 41 | fixture = subprocess.run(ssh_argv(args.vm, 'python3 -'), input=script, text=True, |
| 42 | capture_output=True, check=True, timeout=60) |
| 43 | (output / 'oracle.json').write_text(fixture.stdout) |
| 44 | cases = [('complete', {})] |
| 45 | cases += [(f'{direction}-{occurrence}', dict(cut=14, direction=direction, occurrence=occurrence)) |
| 46 | for direction in ('request', 'response') for occurrence in (1, 2, 10)] |
| 47 | cases.append(('close', dict(cut=6, direction='response'))) |
| 48 | for name, control in cases: |
| 49 | with socket.socket() as reservation: |
| 50 | reservation.bind(('127.0.0.1', 0)) |
| 51 | port = reservation.getsockname()[1] |
| 52 | control_path = output / f'{name}-control.json' |
| 53 | control_path.write_text(json.dumps(control)) |
| 54 | trace = output / f'{name}-trace.jsonl' |
| 55 | with trace.open('w') as log: |
| 56 | proxy = subprocess.Popen([sys.executable, str(Path(__file__).with_name('smb-proxy.py')), |
| 57 | str(control_path), '--port', str(port), '--server', '127.0.0.1', |
| 58 | '--server-port', str(config['samba_port'])], stdout=log, stderr=log) |
| 59 | try: |
| 60 | deadline = time.monotonic() + 5 |
| 61 | while True: |
| 62 | records = [json.loads(line) for line in trace.read_text().splitlines()] |
| 63 | if any('listening' in row for row in records) and any('control' in row for row in records): |
| 64 | break |
| 65 | if proxy.poll() is not None or time.monotonic() > deadline: |
| 66 | raise RuntimeError('Directory test proxy did not start') |
| 67 | time.sleep(.05) |
| 68 | env = dict(os.environ, ONESTORE_SMB_LAB=f'127.0.0.1:{port}', |
| 69 | ONESTORE_SMB_DIRECTORY=root, ONESTORE_SMB_DIRECTORY_ORACLE=str(output / 'oracle.json')) |
| 70 | test = 'tests::live_directory_interruption' if control else 'smb::tests::live_directory' |
| 71 | with (output / f'{name}.log').open('w') as result: |
| 72 | subprocess.run(['cargo', 'test', '-p', 'notebook', '--features', 'smb', test, '--', '--ignored', '--exact'], |
| 73 | env=env, stdout=result, stderr=result, check=True, timeout=120) |
| 74 | finally: |
| 75 | proxy.terminate() |
| 76 | proxy.wait(timeout=5) |
| 77 | records = [json.loads(line) for line in trace.read_text().splitlines()] |
| 78 | if control: |
| 79 | assert sum('cut' in row for row in records) == 1 |
| 80 | else: |
| 81 | assert sum(row.get('command') == 14 and row.get('status') == '0x0' for row in records) > 10 |
| 82 | assert any(row.get('command') == 14 and row.get('status') == '0x80000006' for row in records) |
| 83 | print(f'{name}: passed', flush=True) |
| 84 | env = dict(os.environ, ONESTORE_SMB_LAB=f'127.0.0.1:{config["samba_port"]}', |
| 85 | ONESTORE_SMB_DIRECTORY_ORACLE=str(output / 'oracle.json')) |
| 86 | with (output / 'reconnected.log').open('w') as result: |
| 87 | subprocess.run(['cargo', 'test', '-p', 'notebook', '--features', 'smb', 'smb::tests::live_directory', '--', '--ignored', '--exact'], |
| 88 | env=env, stdout=result, stderr=result, check=True, timeout=120) |
| 89 | print('reconnected: passed', flush=True) |
| 90 | |
| 91 | |
| 92 | if __name__ == '__main__': |
| 93 | main() |