diff --git a/python/tools/README.md b/python/tools/README.md
index ffc6b6e0740f4645ff2491f9340f79554ce68779..f01f1a2dddea2ad1266cb8a75054471ea0fb0634 100644
--- a/python/tools/README.md
+++ b/python/tools/README.md
@@ -20,7 +20,7 @@ help).
 
 ## Python dependencies
 
-- pyroute2 >=0.3.14
+- pyroute2 >=0.6.9
 - matplotlib
 - GeoIP (used by `scanner.py` for drawing map of the world)
 - ipaddress
diff --git a/python/tools/dht/virtual_network_builder.py b/python/tools/dht/virtual_network_builder.py
index 7b9edf10f16e85ecc8f3ec1eafc91041217393fe..75d4449f91a832a9a15ece47d83f6b64017513fd 100755
--- a/python/tools/dht/virtual_network_builder.py
+++ b/python/tools/dht/virtual_network_builder.py
@@ -15,120 +15,253 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program; If not, see <http://www.gnu.org/licenses/>.
+import argparse
+import os
+import subprocess
 
-import argparse, subprocess
+from pyroute2 import NDB, NSPopen
 
-from pyroute2 import NDB, NetNS, NSPopen
 
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description='Creates a virtual network topology for testing')
-    parser.add_argument('-i', '--ifname', help='interface name', default='ethdht')
-    parser.add_argument('-n', '--ifnum', type=int, help='number of isolated interfaces to create', default=1)
-    parser.add_argument('-r', '--remove', help='remove instead of adding network interfaces', action="store_true")
-    parser.add_argument('-l', '--loss', help='simulated packet loss (percent)', type=int, default=0)
-    parser.add_argument('-d', '--delay', help='simulated latency (ms)', type=int, default=0)
-    parser.add_argument('-4', '--ipv4', help='Enable IPv4', action="store_true")
-    parser.add_argument('-6', '--ipv6', help='Enable IPv6', action="store_true")
+def int_range(mini, maxi):
+    def check_ifnum(arg):
+        try:
+            ret = int(arg)
+        except ValueError:
+            raise argparse.ArgumentTypeError('must be an integer')
+        if ret > maxi or ret < mini:
+            raise argparse.ArgumentTypeError(
+                f'must be {mini} <= int <= {maxi}'
+            )
+        return ret
+
+    return check_ifnum
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description='Creates a virtual network topology for testing'
+    )
+    parser.add_argument(
+        '-i', '--ifname', help='interface name', default='ethdht'
+    )
+    parser.add_argument(
+        '-n',
+        '--ifnum',
+        type=int_range(1, 245),
+        help='number of isolated interfaces to create',
+        default=1,
+    )
+    parser.add_argument(
+        '-r',
+        '--remove',
+        help='remove instead of adding network interfaces',
+        action='store_true',
+    )
+    parser.add_argument(
+        '-l',
+        '--loss',
+        help='simulated packet loss (percent)',
+        type=int,
+        default=0,
+    )
+    parser.add_argument(
+        '-d', '--delay', help='simulated latency (ms)', type=int, default=0
+    )
+    parser.add_argument(
+        '-4', '--ipv4', help='Enable IPv4', action='store_true'
+    )
+    parser.add_argument(
+        '-6', '--ipv6', help='Enable IPv6', action='store_true'
+    )
+    parser.add_argument(
+        '-b',
+        '--debug',
+        help='Turn on debug logging and dump topology databases',
+        action='store_true',
+    )
+    parser.add_argument(
+        '-v',
+        '--verbose',
+        help='Turn on verbose output on netns and interfaces operations',
+        action='store_true',
+    )
 
     args = parser.parse_args()
 
     local_addr4 = '10.0.42.'
     local_addr6 = '2001:db9::'
-    brige_name = 'br'+args.ifname
+    bripv4 = f'{local_addr4}1/24'
+    bripv6 = f'{local_addr6}1/64'
+    bridge_name = f'br{args.ifname}'
+    tap_name = f'tap{args.ifname}'
+    veth_names = []
+    namespaces = []
+    ipv4addrs = []
+    ipv6addrs = []
+    for ifn in range(args.ifnum):
+        namespaces.append(f'node{ifn}')
+        veth_names.append(f'{args.ifname}{ifn}')
+        ipv4addrs.append(f'{local_addr4}{ifn+8}/24' if args.ipv4 else None)
+        ipv6addrs.append(f'{local_addr6}{ifn+8}/64' if args.ipv6 else None)
 
-    ip = None
-    try:
-        ip = NDB()
+    with NDB(log='debug' if args.debug else None) as ndb:
         if args.remove:
-            # cleanup interfaces
-            for ifn in range(args.ifnum):
-                iface = args.ifname+str(ifn)
-                if iface in ip.interfaces:
-                    with ip.interfaces[iface] as i:
-                        i.remove()
-            if 'tap'+args.ifname in ip.interfaces:
-                with ip.interfaces['tap'+args.ifname] as i:
-                    i.remove()
-            if brige_name in ip.interfaces:
-                with ip.interfaces[brige_name] as i:
-                    i.remove()
-            for ifn in range(args.ifnum):
-                netns = NetNS('node'+str(ifn))
-                netns.close()
-                netns.remove()
+            # cleanup interfaces in the main namespace
+            for iface in veth_names + [bridge_name] + [tap_name]:
+                if iface in ndb.interfaces:
+                    ndb.interfaces[iface].remove().commit()
+                    if args.verbose:
+                        print(f'link: del main/{iface}')
+
+            # cleanup namespaces
+            for nsname in namespaces:
+                try:
+                    ndb.netns[nsname].remove().commit()
+                    if args.verbose:
+                        print(f'netns: del {nsname}')
+                except KeyError:
+                    pass
         else:
-            for ifn in range(args.ifnum):
-                iface = args.ifname+str(ifn)
-                if not iface in ip.interfaces:
-                    ip.interfaces.create(
-                        kind='veth',
-                        ifname=iface,
-                        peer=iface+'.1',
+            # create ports
+            for veth, nsname, ipv4addr, ipv6addr in zip(
+                veth_names, namespaces, ipv4addrs, ipv6addrs
+            ):
+                # create a network namespace and launch NDB for it
+                #
+                # another possible solution could be simply to attach
+                # the namespace to the main NDB instance, but it can
+                # take a lot of memory in case of many interfaces, thus
+                # launch and discard netns NDB instances
+                netns = NDB(
+                    log='debug' if args.debug else None,
+                    sources=[
+                        {
+                            'target': 'localhost',
+                            'netns': nsname,
+                            'kind': 'netns',
+                        }
+                    ],
+                )
+                if args.verbose:
+                    print(f'netns: add {nsname}')
+                # create the port and push the peer into the namespace
+                (
+                    ndb.interfaces.create(
+                        **{
+                            'ifname': veth,
+                            'kind': 'veth',
+                            'state': 'up',
+                            'peer': {'ifname': veth, 'net_ns_fd': nsname},
+                        }
                     ).commit()
-
-            ip.interfaces.create(
-                kind='tuntap',
-                ifname='tap'+args.ifname,
-                mode='tap',
-            ).commit()
-
-            with ip.interfaces.create(kind='bridge', ifname=brige_name) as i:
-                for ifn in range(args.ifnum):
-                    iface = args.ifname+str(ifn)
-                    i.add_port(ip.interfaces[iface])
-                i.add_port(ip.interfaces['tap'+args.ifname])
-                if args.ipv4:
-                    i.add_ip(local_addr4+'1/24')
-                if args.ipv6:
-                    i.add_ip(local_addr6+'1/64')
-                i.set('state', 'up')
-
-            with ip.interfaces['tap'+args.ifname] as tap:
-                tap.set('state', 'up')
-
-            for ifn in range(args.ifnum):
-                iface = args.ifname+str(ifn)
-
-                nsname = 'node'+str(ifn)
-                nns = NetNS(nsname)
-                iface1 = iface+'.1'
-                with ip.interfaces[iface1] as i:
-                    i['net_ns_fd'] = nns.netns
-
-                with ip.interfaces[iface] as i:
+                )
+                if args.verbose:
+                    print(f'link: add main/{veth} <-> {nsname}/{veth}')
+                # bring up namespace's loopback
+                (
+                    netns.interfaces.wait(ifname='lo', timeout=3)
+                    .set('state', 'up')
+                    .commit()
+                )
+                if args.verbose:
+                    print(f'link: set {nsname}/lo')
+                # bring up the peer
+                with netns.interfaces.wait(ifname=veth, timeout=3) as i:
                     i.set('state', 'up')
-
-                ip_ns = NDB(sources=[
-                    {
-                        'target': 'localhost',
-                        'netns': nsname,
-                        'kind': 'netns',
-                    }
-                ])
-                try:
-                    with ip_ns.interfaces['lo'] as lo:
-                        lo.set('state', 'up')
-                    with ip_ns.interfaces[iface1] as i:
-                        if args.ipv4:
-                            i.add_ip(local_addr4+str(ifn+8)+'/24')
-                        if args.ipv6:
-                            i.add_ip(local_addr6+str(ifn+8)+'/64')
-                        i.set('state', 'up')
-                finally:
-                    ip_ns.close()
-
-                nsp = NSPopen(nns.netns, ["tc", "qdisc", "add", "dev", iface1, "root", "netem", "delay", str(args.delay)+"ms", str(int(args.delay/2))+"ms", "loss", str(args.loss)+"%", "25%"], stdout=subprocess.PIPE)
+                    if args.ipv4:
+                        i.add_ip(ipv4addr)
+                    if args.ipv6:
+                        i.add_ip(ipv6addr)
+                if args.verbose:
+                    print(f'link: set {nsname}/{veth}, {ipv4addr}, {ipv6addr}')
+                # disconnect the namespace NDB agent, not removing the NS
+                if args.debug:
+                    fname = f'{nsname}-ndb.db'
+                    print(f'dump: netns topology database {fname}')
+                    netns.schema.backup(fname)
+                netns.close()
+                # set up the emulation QDisc
+                nsp = NSPopen(
+                    nsname,
+                    [
+                        'tc',
+                        'qdisc',
+                        'add',
+                        'dev',
+                        veth,
+                        'root',
+                        'netem',
+                        'delay',
+                        f'{args.delay}ms',
+                        f'{int(args.delay)/2}ms',
+                        'loss',
+                        f'{args.loss}%',
+                        '25%',
+                    ],
+                    stdout=subprocess.PIPE,
+                )
                 nsp.communicate()
                 nsp.wait()
                 nsp.release()
+                if args.verbose:
+                    print(
+                        f'netem: add {nsname}/{veth}, '
+                        f'{args.delay}, {args.loss}'
+                    )
 
-            if args.ipv4:
-                subprocess.call(["sysctl", "-w", "net.ipv4.conf."+brige_name+".forwarding=1"])
-            if args.ipv6:
-                subprocess.call(["sysctl", "-w", "net.ipv6.conf."+brige_name+".forwarding=1"])
+            # create the tap
+            #
+            # for some reason we should create the tap inteface first,
+            # and only then bring it up, thus two commit() calls
+            (
+                ndb.interfaces.create(
+                    kind='tuntap', ifname=tap_name, mode='tap'
+                )
+                .commit()
+                .set('state', 'up')
+                .commit()
+            )
+            if args.verbose:
+                print(f'link: add main/{tap_name}')
+
+            # create the bridge and add all the ports
+            with ndb.interfaces.create(
+                ifname=bridge_name, kind='bridge', state='up'
+            ) as i:
+                if args.ipv4:
+                    i.add_ip(bripv4)
+                if args.ipv6:
+                    i.add_ip(bripv6)
+                for iface in veth_names + [tap_name]:
+                    i.add_port(iface)
+            if args.verbose:
+                print(f'link: add main/{bridge_name}, {bripv4}, {bripv6}')
+
+            with open(os.devnull, 'w') as fnull:
+                if args.ipv4:
+                    subprocess.call(
+                        [
+                            'sysctl',
+                            '-w',
+                            f'net.ipv4.conf.{bridge_name}.forwarding=1',
+                        ],
+                        stdout=fnull,
+                    )
+                if args.verbose:
+                    print(f'sysctl: set {bridge_name} ipv4 forwarding')
+                if args.ipv6:
+                    subprocess.call(
+                        [
+                            'sysctl',
+                            '-w',
+                            f'net.ipv6.conf.{bridge_name}.forwarding=1',
+                        ],
+                        stdout=fnull,
+                    )
+                if args.verbose:
+                    print(f'sysctl: set {bridge_name} ipv4 forwarding')
 
-    except Exception as e:
-          print('Error',e)
-    finally:
-        if ip:
-            ip.close()
+            if args.debug:
+                fname = 'main-ndb.db'
+                print('dump: the main netns topology database')
+                ndb.schema.backup(fname)