# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- # https://github.com/nusenu/noContactInfo_Exit_Excluder # https://github.com/TheSmashy/TorExitRelayExclude """ This extends nusenu's basic idea of using the stem library to dynamically exclude nodes that are likely to be bad by putting them on the ExcludeNodes or ExcludeExitNodes setting of a running Tor. * https://github.com/nusenu/noContactInfo_Exit_Excluder * https://github.com/TheSmashy/TorExitRelayExclude The basic cut is to exclude Exit nodes that do not have a contact. That can be extended to nodes that do not have an email in the contact etc. """ """But there's a problem, and your Tor notice.log will tell you about it: you could exclude the nodes needed to access hidden services or directorues. So we need to add to the process the concept of a whitelist. In addition, we may have our own blacklist of nodes we want to exclude, or use these lists for other applications like selektor. So we make two files that are structured in YAML: ``` /etc/tor/yaml/torrc-goodnodes.yaml GoodNodes: Relays: IntroductionPoints: - NODEFINGERPRINT ... By default all sections of the goodnodes.yaml are used as a whitelist. /etc/tor/yaml/torrc-badnodes.yaml BadNodes: ExcludeExitNodes: BadExit: # $0000000000000000000000000000000000000007 ``` That part requires [PyYAML](https://pyyaml.org/wiki/PyYAML) https://github.com/yaml/pyyaml/ Right now only the ExcludeExitNodes section is used by we may add ExcludeNodes later, and by default all sub-sections of the badnodes.yaml are used as a ExcludeExitNodes but it can be customized with the lWanted commandline arg. The original idea has also been extended to add different conditions for exclusion: the ```--contact``` commandline arg is a comma sep list of conditions: * Empty - no contact info * NoEmail - no @ sign in the contact', More may be added later. Because you don't want to exclude the introduction points to any onion you want to connect to, ```--white_onions``` should whitelist the introduction points to a comma sep list of onions, but is currently broken in stem 1.8.0: see: * https://github.com/torproject/stem/issues/96 * https://gitlab.torproject.org/legacy/trac/-/issues/25417 ```--torrc_output``` will write the torrc ExcludeNodes configuration to a file. Now for the final part: we lookup the Contact info of every server that is currently in our Tor, and check it for its existence. If it fails to provide the well-know url, we assume its a bogus relay and add it to a list of nodes that goes on ExcludeNodes - not just exclude Exit. If the Contact info is good we add the list of fingerprints to add to ExitNodes, a whitelist of relays to use as exits. ```--proof_output``` will write the contact info as a ciiss dictionary to a YAML file. If the proof is uri-rsa, the well-known file of fingerprints is downloaded and the fingerprints are added on a 'fps' field we create of that fingerprint's entry of the YAML dictionary. This file is read at the beginning of the program to start with a trust database, and only new contact info from new relays are added to the dictionary. You can expect it to take an hour or two the first time this is run: >700 domains. For usage, do ```python3 exclude_badExits.py --help` """ import sys import os import re import time import argparse from io import StringIO from stem import InvalidRequest from stem.control import Controller from stem.connection import IncorrectPassword from stem.util.tor_tools import is_valid_fingerprint try: import yaml except: yaml = None try: from unbound import ub_ctx,RR_TYPE_TXT,RR_CLASS_IN except: ub_ctx = RR_TYPE_TXT = RR_CLASS_IN = None try: if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' # https://pypi.org/project/coloredlogs/ import coloredlogs except ImportError as e: coloredlogs = False from trustor_poc import lDownloadUrlFps, idns_validate global LOG import logging import warnings warnings.filterwarnings('ignore') LOG = logging.getLogger() aTRUST_DB = {} sDETAILS_URL = "https://metrics.torproject.org/rs.html#details/" # You can call this while bootstrapping sEXCLUDE_EXIT_KEY = 'ExcludeNodes' sINCLUDE_EXIT_KEY = 'ExitNodes' sINCLUDE_GUARD_KEY = 'EntryNodes' def oMakeController(sSock='', port=9051): import getpass if sSock and os.path.exists(sSock): controller = Controller.from_socket_file(path=sSock) else: controller = Controller.from_port(port=port) sys.stdout.flush() p = getpass.unix_getpass(prompt='Controller Password: ', stream=sys.stderr) controller.authenticate(p) return controller oBAD_NODES = {} oBAD_ROOT = 'BadNodes' def lYamlBadNodes(sFile, section=sEXCLUDE_EXIT_KEY, lWanted=['BadExit']): global oBAD_NODES root = 'ExcludeNodes' l = [] if not yaml: return l if os.path.exists(sFile): with open(sFile, 'rt') as oFd: o = yaml.safe_load(oFd) oBAD_NODES = o # BROKEN # for elt in o[oBAD_ROOT][root][section].keys(): # if lWanted and elt not in lWanted: continue # # l += o[oBAD_ROOT][root][section][elt] return l def icheck_torrc(sFile, oArgs): l = open(sFile, 'rt').readlines() a = {} for elt in l: k,v = elt.split(' ', 1) a[k] = v keys = list(a.keys()) if 'HashedControlPassword' not in keys: LOG.info('Add HashedControlPassword for security') print('run: tor --hashcontrolpassword ') if 'ExcludeNodes' in keys: elt = 'ExcludeNodes.ExcludeExitNodes.BadExit' LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}") print(f"move to the {elt} section as a list") if 'GuardNodes' in keys: elt = 'GoodNodes.GuardNodes' LOG.warn(f"Remove GuardNodes and move then to {oArgs.good_nodes}") print(f"move to the {elt} section as a list") if 'ExcludeNodes' in keys: elt = 'ExcludeNodes.ExcludeExitNodes.BadExit' LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}") print(f"move to the {elt} section as a list") if 'ControlSocket' not in keys and os.path.exists('/run/tor/control'): LOG.info('Add ControlSocket /run/tor/control for us') print('ControlSocket /run/tor/control GroupWritable RelaxDirModeCheck') if 'UseMicrodescriptors' not in keys or keys['UseMicrodescriptors'] != '1': LOG.info('Add UseMicrodescriptors 0 for us') print('UseMicrodescriptors 0') if 'AutomapHostsSuffixes' not in keys: LOG.info('Add AutomapHostsSuffixes for onions') print('AutomapHostsSuffixes .exit,.onion') if 'AutoMapHostsOnResolve' not in keys: LOG.info('Add AutoMapHostsOnResolve for onions') print('AutoMapHostsOnResolve 1') if 'VirtualAddrNetworkIPv4' not in keys: LOG.info('Add VirtualAddrNetworkIPv4 for onions') print('VirtualAddrNetworkIPv4 172.16.0.0/12') return 0 oGOOD_NODES = {} oGOOD_ROOT = 'GoodNodes' def lYamlGoodNodes(sFile='/etc/tor/torrc-goodnodes.yaml'): global oGOOD_NODES root = oGOOD_ROOT l = [] if not yaml: return l if os.path.exists(sFile): with open(sFile, 'rt') as oFd: o = yaml.safe_load(oFd) oGOOD_NODES = o if 'GuardNodes' in o[root].keys(): l += o[oGOOD_ROOT]['GuardNodes'] # yq '.Nodes.IntroductionPoints|.[]' < /etc/tor/torrc-goodnodes.yaml return l def lIntroductionPoints(controller, lOnions): """not working in stem 1.8.3""" l = [] for elt in lOnions: desc = controller.get_hidden_service_descriptor(elt, await_result=True, timeout=None) l = desc.introduction_points() if l: LOG.warn(f"{elt} NO introduction points\n") continue LOG.info(f"{elt} introduction points are...\n") for introduction_point in l: LOG.info(' %s:%s => %s' % (introduction_point.address, introduction_point.port, introduction_point.identifier)) l += [introduction_point.address] return l lBAD_URLS = [] lATS = ['abuse', 'email'] lINTS = ['ciissversion', 'uplinkbw', 'signingkeylifetime', 'memory'] lBOOLS = ['dnssec', 'dnsqname', 'aesni', 'autoupdate', 'dnslocalrootzone', 'sandbox', 'offlinemasterkey'] def aVerifyContact(a, fp, https_cafile, timeout=20, host='127.0.0.1', port=9050): global lBAD_URLS # cleanups for yaml for elt in lINTS: if elt in a: a[elt] = int(a[elt]) for elt in lBOOLS: if elt in a: if a[elt] in ['y','yes', 'true', 'True']: a[elt] = True else: a[elt] = False for elt in lATS: if elt in a: a[elt] = a[elt].replace('[]', '@') a.update({'fps': []}) # test the url for fps and add it to the array if 'proof' not in a: LOG.warn(f"{fp} 'proof' not in {list(a.keys())}") return a if 'url' not in a: if 'uri' not in a: a['url'] = '' LOG.warn(f"{fp} url and uri not in {list(a.keys())}") return a a['url'] = a['uri'] LOG.debug(f"{fp} 'uri' but not 'url' in {list(a.keys())}") # drop through if a['url'].startswith('http:'): a['url'] = 'https:' +a['url'][5:] elif not a['url'].startswith('https:'): a['url'] = 'https:' +a['url'] # domain should be a unique ket for contacts domain = a['url'][8:] if a['proof'] not in ['uri-rsa']: # only support uri for now if False and ub_ctx: fp_domain = fp +'.'+domain if idns_validate(fp_domain, libunbound_resolv_file='resolv.conf', dnssec_DS_file='dnssec-root-trust', ) == 0: pass LOG.warn(f"{fp} proof={a['proof']} not supported yet") return a LOG.debug(f"{len(list(a.keys()))} contact fields for {fp}") try: LOG.debug(f"Downloading from {domain} for {fp}") l = lDownloadUrlFps(domain, https_cafile, timeout=timeout, host=host, port=port) except Exception as e: LOG.exception(f"Error downloading from {domain} for {fp} {e}") lBAD_URLS += [a['url']] else: if not l: # already squacked in lD LOG.warn(f"Downloading from {domain} failed for {fp}") lBAD_URLS += [a['url']] else: a['fps'] = [elt for elt in l if elt and len(elt) == 40 and not elt.startswith('#')] return a def aParseContact(contact, fp): """ See the Tor ContactInfo Information Sharing Specification v2 https://nusenu.github.io/ContactInfo-Information-Sharing-Specification/ """ contact = str(contact, 'UTF-8') l = [line for line in contact.strip().replace('"', '').split(' ') if ':' in line] LOG.debug(f"{fp} {len(l)} fields") s = f'"{fp}":\n' s += '\n'.join([f" {line}\"".replace(':',': \"', 1) for line in l]) oFd = StringIO(s) a = yaml.safe_load(oFd) return a def bAreWeConnected(): # FixMe: Linux only sFile = f"/proc/{os.getpid()}/net/route" if not os.path.isfile(sFile): return None i = 0 for elt in open(sFile, "r").readlines(): if elt.startswith('Iface'): continue if elt.startswith('lo'): continue i += 1 return i > 0 def vwait_for_controller(controller, wait_boot): if bAreWeConnected() is False: raise SystemExit("we are not connected") percent = i = 0 # You can call this while boostrapping while percent < 100 and i < wait_boot: bootstrap_status = controller.get_info("status/bootstrap-phase") progress_percent = re.match('.* PROGRESS=([0-9]+).*', bootstrap_status) percent = int(progress_percent.group(1)) LOG.info(f"Bootstrapping {percent}%") time.sleep(5) i += 5 def vsetup_logging(log_level, logfile=''): global LOG add = True # stem fucks up logging from stem.util import log logging.getLogger('stem').setLevel(30) logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' logging._defaultFormatter.default_msec_format = '' kwargs = dict(level=log_level, force=True, format='%(levelname)-4s %(message)s') if logfile: add = logfile.startswith('+') sub = logfile.startswith('-') if add or sub: logfile = logfile[1:] kwargs['filename'] = logfile if coloredlogs: # https://pypi.org/project/coloredlogs/ aKw = dict(level=log_level, logger=LOG, stream=sys.stdout if add else None, fmt='%(levelname)-4s %(message)s' ) coloredlogs.install(**aKw) if logfile: oHandler = logging.FileHandler(logfile) LOG.addHandler(oHandler) LOG.info(f"CSetting log_level to {log_level!s}") else: logging.basicConfig(**kwargs) if add and logfile: oHandler = logging.StreamHandler(sys.stdout) LOG.addHandler(oHandler) LOG.info(f"SSetting log_level to {log_level!s}") def oMainArgparser(_=None): try: from OpenSSL import SSL lCAfs = SSL._CERTIFICATE_FILE_LOCATIONS except: lCAfs = [] CAfs = [] for elt in lCAfs: if os.path.exists(elt): CAfs.append(elt) if not CAfs: CAfs = [''] parser = argparse.ArgumentParser(add_help=True, epilog=__doc__) parser.add_argument('--https_cafile', type=str, help="Certificate Authority file (in PEM)", default=CAfs[0]) parser.add_argument('--proxy_host', '--proxy-host', type=str, default='127.0.0.1', help='proxy host') parser.add_argument('--proxy_port', '--proxy-port', default=9050, type=int, help='proxy control port') parser.add_argument('--proxy_ctl', '--proxy-ctl', default='/run/tor/control', type=str, help='control socket - or port') parser.add_argument('--torrc', default='', type=str, help='torrc to check for suggestions') parser.add_argument('--timeout', default=30, type=int, help='proxy download connect timeout') parser.add_argument('--good_nodes', type=str, default='/etc/tor/yaml/torrc-goodnodes.yaml', help="Yaml file of good nodes that should not be excluded") parser.add_argument('--bad_nodes', type=str, default='/etc/tor/yaml/torrc-badnodes.yaml', help="Yaml file of bad nodes that should also be excluded") parser.add_argument('--contact', type=str, default='Empty,NoEmail', help="comma sep list of conditions - Empty,NoEmail") parser.add_argument('--bad_contacts', type=str, default='/tmp/badcontacts.yaml', help="Yaml file of bad contacts that bad FPs are using") parser.add_argument('--wait_boot', type=int, default=120, help="Seconds to wait for Tor to booststrap") parser.add_argument('--log_level', type=int, default=20, help="10=debug 20=info 30=warn 40=error") parser.add_argument('--bad_sections', type=str, default='Hetzner,BadExit', help="sections of the badnodes.yaml to use, comma separated, '' BROKEN") parser.add_argument('--white_onions', type=str, default='', help="comma sep. list of onions to whitelist their introduction points - BROKEN") parser.add_argument('--torrc_output', type=str, default='', help="Write the torrc configuration to a file") parser.add_argument('--proof_output', type=str, default='', help="Write the proof data of the included nodes to a YAML file") return parser def vwrite_badnodes(oArgs): global oBAD_NODES if oArgs.bad_nodes: tmp = oArgs.bad_nodes +'.tmp' bak = oArgs.bad_nodes +'.bak' with open(tmp, 'wt') as oFYaml: yaml.dump(oBAD_NODES, indent=2, stream=oFYaml) LOG.info(f"Wrote {len(list(exit_excludelist))} proof details to {oArgs.bad_nodes}") oFYaml.close() if os.path.exists(oArgs.bad_nodes): os.rename(oArgs.bad_nodes, bak) os.rename(tmp, oArgs.bad_nodes) def vwrite_goodnodes(oArgs): global oGOOD_NODES if oArgs.good_nodes: tmp = oArgs.good_nodes +'.tmp' bak = oArgs.good_nodes +'.bak' with open(tmp, 'wt') as oFYaml: yaml.dump(oGOOD_NODES, indent=2, stream=oFYaml) LOG.info(f"Wrote {len(list(exit_excludelist))} proof details to {oArgs.good_nodes}") oFYaml.close() if os.path.exists(oArgs.good_nodes): os.rename(oArgs.good_nodes, bak) os.rename(tmp, oArgs.good_nodes) def iMain(lArgs): parser = oMainArgparser() oArgs = parser.parse_args(lArgs) vsetup_logging(oArgs.log_level) if bAreWeConnected() is False: raise SystemExit("we are not connected") sFile = oArgs.torrc if sFile and os.path.exists(sFile): icheck_torrc(sFile, oArgs) global aTRUST_DB sFile = oArgs.proof_output if sFile and os.path.exists(sFile): try: with open(sFile, 'rt') as oFd: aTRUST_DB = yaml.safe_load(oFd) except: aTRUST_DB = {} if oArgs.proxy_ctl.startswith('/') or os.path.exists(oArgs.proxy_ctl): controller = oMakeController(sSock=oArgs.proxy_ctl) else: port =int(oArgs.proxy_ctl) controller = oMakeController(port=port) vwait_for_controller(controller, oArgs.wait_boot) if oArgs.proof_output: proof_output_tmp = oArgs.proof_output + '.tmp' elt = controller.get_conf('UseMicrodescriptors') if elt != '0' : LOG.error('"UseMicrodescriptors 0" is required in your /etc/tor/torrc. Exiting.') controller.set_conf('UseMicrodescriptors', 0) # does it work dynamically? return 2 elt = controller.get_conf(sEXCLUDE_EXIT_KEY) if elt and elt != '{??}': LOG.warn(f"{sEXCLUDE_EXIT_KEY} is in use already") lGoodOverrideSet = lYamlGoodNodes(oArgs.good_nodes) LOG.info(f"lYamlGoodNodes {len(lGoodOverrideSet)} from {oArgs.good_nodes}") if oArgs.white_onions: l = lIntroductionPoints(controller, oArgs.white_onions.split(',')) lGoodOverrideSet += l exit_excludelist = [] if oArgs.bad_nodes and os.path.exists(oArgs.bad_nodes): if False and oArgs.bad_sections: # BROKEN sections = oArgs.bad_sections.split(',') exit_excludelist = lYamlBadNodes(oArgs.bad_nodes, lWanted=sections, section=sEXCLUDE_EXIT_KEY) else: exit_excludelist = lYamlBadNodes(oArgs.bad_nodes) LOG.info(f"lYamlBadNodes {len(exit_excludelist)}") relays = controller.get_server_descriptors() lProofGoodFps = [] iDnsContact = 0 iBadContact = 0 iFakeContact = 0 aBadContacts = {} aProofUri = {} lConds = oArgs.contact.split(',') for relay in relays: if not is_valid_fingerprint(relay.fingerprint): LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint) continue relay.fingerprint = relay.fingerprint.upper() sofar = f"G:{len(list(aProofUri.keys()))} U:{iDnsContact} F:{iFakeContact} BF:{len(exit_excludelist)} GF:{len(lProofGoodFps)}" if not relay.exit_policy.is_exiting_allowed(): if sEXCLUDE_EXIT_KEY == 'ExcludeNodes': LOG.debug(f"{relay.fingerprint} not an exit {sofar}") else: LOG.warn(f"{relay.fingerprint} not an exit {sofar}") # continue if relay.fingerprint in lProofGoodFps: # we already have it. continue if relay.fingerprint in aTRUST_DB: if aTRUST_DB[relay.fingerprint]['fps'] and \ relay.fingerprint in aTRUST_DB[relay.fingerprint]['fps']: lProofGoodFps += relay.fingerprint continue if relay.contact and b'dns-rsa' in relay.contact.lower(): LOG.info(f"{relay.fingerprint} skipping 'dns-rsa' {sofar}") iDnsContact += 1 continue if relay.contact and b'proof:uri-rsa' in relay.contact.lower(): a = aParseContact(relay.contact, relay.fingerprint) if not a: LOG.warn(f"{relay.fingerprint} did not parse {sofar}") exit_excludelist.append(relay.fingerprint) continue if 'url' in a and a['url'] and a['url'] in lBAD_URLS: # The fp is using a contact with a URL we know is bad LOG.info(f"{relay.fingerprint} skipping in lBAD_URLS {a['url']} {sofar}") exit_excludelist.append(relay.fingerprint) continue b = aVerifyContact(list(a.values())[0], relay.fingerprint, oArgs.https_cafile, timeout=oArgs.timeout, host=oArgs.proxy_host, port=oArgs.proxy_port) if not b['fps'] or not b['url']: LOG.warn(f"{relay.fingerprint} did not verify {sofar}") # If it's giving contact info that doesnt check out # it could be a bad exit with fake contact info exit_excludelist.append(relay.fingerprint) aBadContacts[relay.fingerprint] = b continue if relay.fingerprint not in b['fps']: LOG.warn(f"{relay.fingerprint} the fp is not in the list of fps {sofar}") # assume a fp is using a bogus contact exit_excludelist.append(relay.fingerprint) iFakeContact += 1 aBadContacts[relay.fingerprint] = b continue # great contact had good fps and we are in them lProofGoodFps += b['fps'] LOG.info(f"{relay.fingerprint} verified {b['url']} {sofar}") # add our contact info to the trustdb aProofUri[relay.fingerprint] = b if oArgs.proof_output and oArgs.log_level <= 20: # as we go along then clobber with open(proof_output_tmp, 'wt') as oFYaml: yaml.dump(aProofUri, indent=2, stream=oFYaml) oFYaml.close() continue if ('Empty' in lConds and not relay.contact) or \ ('NoEmail' in lConds and relay.contact and not b'@' in relay.contact): exit_excludelist.append(relay.fingerprint) exit_excludelist = list(set(exit_excludelist).difference(set(lGoodOverrideSet))) if oArgs.proof_output and aProofUri: with open(proof_output_tmp, 'wt') as oFYaml: yaml.dump(aProofUri, indent=2, stream=oFYaml) LOG.info(f"Wrote {len(list(aProofUri))} proof details to {oArgs.proof_output}") oFYaml.close() if os.path.exists(oArgs.proof_output): bak = oArgs.proof_output +'.bak' os.rename(oArgs.proof_output, bak) os.rename(proof_output_tmp, oArgs.proof_output) if oArgs.torrc_output and exit_excludelist: with open(oArgs.torrc_output, 'wt') as oFTorrc: oFTorrc.write(f"{sEXCLUDE_EXIT_KEY} {','.join(exit_excludelist)}\n") oFTorrc.write(f"{sINCLUDE_EXIT_KEY} {','.join(lProofGoodFps)}\n") oFTorrc.write(f"{sINCLUDE_GUARD_KEY} {','.join(o[oGOOD_ROOT]['GuardNodes'])}\n") LOG.info(f"Wrote tor configuration to {oArgs.torrc_output}") oFTorrc.close() if oArgs.bad_contacts and aBadContacts: # for later analysis with open(oArgs.bad_contacts, 'wt') as oFYaml: yaml.dump(aBadContacts, indent=2, stream=oFYaml) oFYaml.close() global oBAD_NODES oBAD_NODES['BadNodes']['ExcludeNodes']['BadExit'] = exit_excludelist vwrite_badnodes(oArgs) # nothing changed vwrite_goodnodes(oArgs) retval = 0 try: logging.getLogger('stem').setLevel(30) if exit_excludelist: LOG.info(f"{sEXCLUDE_EXIT_KEY} {len(exit_excludelist)} net bad exit nodes") controller.set_conf(sEXCLUDE_EXIT_KEY, exit_excludelist) if lProofGoodFps: LOG.info(f"{sINCLUDE_EXIT_KEY} {len(lProofGoodFps)} good nodes") controller.set_conf(sINCLUDE_EXIT_KEY, lProofGoodFps) o = oGOOD_NODES if 'GuardNodes' in o[oGOOD_ROOT].keys(): LOG.info(f"{sINCLUDE_GUARD_KEY} {len(o[oGOOD_ROOT]['GuardNodes'])} guard nodes") controller.set_conf(sINCLUDE_GUARD_KEY, o[oGOOD_ROOT]['GuardNodes']) return retval except InvalidRequest as e: # Unacceptable option value: Invalid router list. LOG.error(str(e)) LOG.warn(f"lProofGoodFps: {lProofGoodFps}") LOG.warn(f"{sEXCLUDE_EXIT_KEY}: {exit_excludelist}") retval = 1 return retval except KeyboardInterrupt: return 0 except Exception as e: LOG.exception(str(e)) retval = 2 return retval finally: # wierd we are getting stem errors during the final return # with a traceback that doesnt correspond to any real flow # File "/usr/lib/python3.9/site-packages/stem/control.py", line 2474, in set_conf # self.set_options({param: value}, False) logging.getLogger('stem').setLevel(40) try: for elt in controller._event_listeners: controller.remove_event_listener(elt) controller.close() except Exception as e: LOG.warn(str(e)) if __name__ == '__main__': try: i = iMain(sys.argv[1:]) except IncorrectPassword as e: LOG.error(e) i = 1 except KeyboardInterrupt: i = 0 except Exception as e: LOG.exception(e) i = 2 sys.exit(i)