#!/usr/bin/env python3 """Check real NIC identities on a bridge isolated in user/net/mount namespaces.""" import argparse import json from pathlib import Path import subprocess import sys import tempfile sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from hydra.profiles import Profile from hydra.runtime import Runtime def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--namespace-root', type=Path, help=argparse.SUPPRESS) args = parser.parse_args() if args.namespace_root is None: root = Path(tempfile.mkdtemp(prefix='hydra-network-', dir='/tmp')) print('Evidence: ' + str(root), flush=True) (root / 'parent-namespaces.json').write_text(json.dumps({name: str(Path('/proc/self/ns', name).readlink()) for name in ('user', 'net', 'mnt')})) # All bridge and /etc changes below occur after this explicit namespace # boundary. The inherited host bridge configuration is never modified. subprocess.run(['unshare', '-Urnm', '--', str(Path(sys.executable).resolve()), str(Path(__file__).resolve()), '--namespace-root', str(root)], check=True) return root = args.namespace_root parent_namespaces = json.loads((root / 'parent-namespaces.json').read_text()) namespaces = {name: str(Path('/proc/self/ns', name).readlink()) for name in ('user', 'net', 'mnt')} assert all(namespaces[name] != parent_namespaces[name] for name in namespaces), 'private namespaces required' private_etc = root / 'etc' (private_etc / 'qemu').mkdir(parents=True) (private_etc / 'qemu/bridge.conf').write_text('allow hydrabr0\n') subprocess.run(['mount', '--bind', str(private_etc), '/etc'], check=True) subprocess.run(['ip', 'link', 'add', 'hydrabr0', 'type', 'bridge'], check=True) subprocess.run(['ip', 'link', 'set', 'hydrabr0', 'up'], check=True) manager = Runtime(root / 'config', root / 'state', root / 'run') result = {'evidence': str(root), 'namespace': 'private user/net/mount', 'parent_namespaces': parent_namespaces, 'probe_namespaces': namespaces, 'names': {}} try: for name in ('alpha', 'beta'): manager.add(Profile(name, memory=128, cpus=1, disk_size='8M', graphics='software', network='bridge')) manager.start(name) def actual_mac(name): with manager._connect(manager.paths(name)) as qmp: network = qmp.command('human-monitor-command', {'command-line': 'info network'}) mac = manager.profile(name).mac assert 'macaddr=' + mac in network, network assert int(mac.split(':')[0], 16) & 3 == 2 return mac, network for name in ('alpha', 'beta'): mac, network = actual_mac(name) result['names'][name] = {'mac': mac, 'network': network} assert result['names']['alpha']['mac'] != result['names']['beta']['mac'] links = json.loads(subprocess.check_output(['ip', '-j', 'link', 'show', 'master', 'hydrabr0'], text=True)) assert len(links) == 2, links result['bridge_ports'] = [link['ifname'] for link in links] manager.stop('alpha', force=True) manager.start('alpha') assert actual_mac('alpha')[0] == result['names']['alpha']['mac'] result['restart_preserved_mac'] = True result['dhcp_and_guest_network_drivers_pending'] = True result['passed'] = True (root / 'result.json').write_text(json.dumps(result, indent=2) + '\n') print(json.dumps(result, indent=2)) finally: for profile in manager.profiles(): if manager.status(profile.name)['status'] == 'running': manager.stop(profile.name, force=True) if __name__ == '__main__': main()