TorNet.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  1. #!/usr/bin/env python2
  2. #
  3. # Copyright 2011 Nick Mathewson, Michael Stone
  4. # Copyright 2013 The Tor Project
  5. #
  6. # You may do anything with this work that copyright law would normally
  7. # restrict, so long as you retain the above notice(s) and this license
  8. # in all redistributed copies and derived works. There is no warranty.
  9. from __future__ import print_function
  10. from __future__ import with_statement
  11. import cgitb
  12. import os
  13. import signal
  14. import subprocess
  15. import sys
  16. import re
  17. import errno
  18. import time
  19. import shutil
  20. import importlib
  21. from chutney.Debug import debug_flag, debug
  22. import chutney.Templating
  23. import chutney.Traffic
  24. _BASE_ENVIRON = None
  25. _TORRC_OPTIONS = None
  26. _THE_NETWORK = None
  27. # Get verbose tracebacks, so we can diagnose better.
  28. cgitb.enable(format="plain")
  29. def mkdir_p(d, mode=448):
  30. """Create directory 'd' and all of its parents as needed. Unlike
  31. os.makedirs, does not give an error if d already exists.
  32. 448 is the decimal representation of the octal number 0700. Since
  33. python2 only supports 0700 and python3 only supports 0o700, we can use
  34. neither.
  35. """
  36. try:
  37. os.makedirs(d, mode=mode)
  38. except OSError as e:
  39. if e.errno == errno.EEXIST:
  40. return
  41. raise
  42. def get_absolute_chutney_path():
  43. # use the current directory as the default
  44. # (./chutney already sets CHUTNEY_PATH using the path to the script)
  45. # use tools/test-network.sh if you want chutney to try really hard to find
  46. # itself
  47. relative_chutney_path = os.environ.get('CHUTNEY_PATH', os.getcwd())
  48. return os.path.abspath(relative_chutney_path)
  49. def get_absolute_net_path():
  50. # use the chutney path as the default
  51. absolute_chutney_path = get_absolute_chutney_path()
  52. relative_net_path = os.environ.get('CHUTNEY_DATA_DIR', 'net')
  53. # but what is it relative to?
  54. # let's check if it's in CHUTNEY_PATH first, to preserve
  55. # backwards-compatible behaviour
  56. chutney_net_path = os.path.join(absolute_chutney_path, relative_net_path)
  57. if os.path.isdir(chutney_net_path):
  58. return chutney_net_path
  59. # ok, it's relative to the current directory, whatever that is
  60. return os.path.abspath(relative_net_path)
  61. def get_absolute_nodes_path():
  62. # there's no way to customise this: we really don't need more options
  63. return os.path.join(get_absolute_net_path(), 'nodes')
  64. def get_new_absolute_nodes_path(now=time.time()):
  65. # automatically chosen to prevent path collisions, and result in an ordered
  66. # series of directory path names
  67. # should only be called by 'chutney configure', all other chutney commands
  68. # should use get_absolute_nodes_path()
  69. nodesdir = get_absolute_nodes_path()
  70. newdir = newdirbase = "%s.%d" % (nodesdir, now)
  71. # if the time is the same, fall back to a simple integer count
  72. # (this is very unlikely to happen unless the clock changes: it's not
  73. # possible to run multiple chutney networks at the same time)
  74. i = 0
  75. while os.path.exists(newdir):
  76. i += 1
  77. newdir = "%s.%d" % (newdirbase, i)
  78. return newdir
  79. class Node(object):
  80. """A Node represents a Tor node or a set of Tor nodes. It's created
  81. in a network configuration file.
  82. This class is responsible for holding the user's selected node
  83. configuration, and figuring out how the node needs to be
  84. configured and launched.
  85. """
  86. # Fields:
  87. # _parent
  88. # _env
  89. # _builder
  90. # _controller
  91. ########
  92. # Users are expected to call these:
  93. def __init__(self, parent=None, **kwargs):
  94. self._parent = parent
  95. self._env = self._createEnviron(parent, kwargs)
  96. self._builder = None
  97. self._controller = None
  98. def getN(self, N):
  99. return [Node(self) for _ in range(N)]
  100. def specialize(self, **kwargs):
  101. return Node(parent=self, **kwargs)
  102. ######
  103. # Chutney uses these:
  104. def getBuilder(self):
  105. """Return a NodeBuilder instance to set up this node (that is, to
  106. write all the files that need to be in place so that this
  107. node can be run by a NodeController).
  108. """
  109. if self._builder is None:
  110. self._builder = LocalNodeBuilder(self._env)
  111. return self._builder
  112. def getController(self):
  113. """Return a NodeController instance to control this node (that is,
  114. to start it, stop it, see if it's running, etc.)
  115. """
  116. if self._controller is None:
  117. self._controller = LocalNodeController(self._env)
  118. return self._controller
  119. def setNodenum(self, num):
  120. """Assign a value to the 'nodenum' element of this node. Each node
  121. in a network gets its own nodenum.
  122. """
  123. self._env['nodenum'] = num
  124. #####
  125. # These are internal:
  126. def _createEnviron(self, parent, argdict):
  127. """Return an Environ that delegates to the parent node's Environ (if
  128. there is a parent node), or to the default environment.
  129. """
  130. if parent:
  131. parentenv = parent._env
  132. else:
  133. parentenv = self._getDefaultEnviron()
  134. return TorEnviron(parentenv, **argdict)
  135. def _getDefaultEnviron(self):
  136. """Return the default environment. Any variables that we can't find
  137. set for any particular node, we look for here.
  138. """
  139. return _BASE_ENVIRON
  140. class _NodeCommon(object):
  141. """Internal helper class for functionality shared by some NodeBuilders
  142. and some NodeControllers."""
  143. # XXXX maybe this should turn into a mixin.
  144. def __init__(self, env):
  145. self._env = env
  146. def expand(self, pat, includePath=(".",)):
  147. return chutney.Templating.Template(pat, includePath).format(self._env)
  148. def _getTorrcFname(self):
  149. """Return the name of the file where we'll be writing torrc"""
  150. return self.expand("${torrc_fname}")
  151. class NodeBuilder(_NodeCommon):
  152. """Abstract base class. A NodeBuilder is responsible for doing all the
  153. one-time prep needed to set up a node in a network.
  154. """
  155. def __init__(self, env):
  156. _NodeCommon.__init__(self, env)
  157. def checkConfig(self, net):
  158. """Try to format our torrc; raise an exception if we can't.
  159. """
  160. def preConfig(self, net):
  161. """Called on all nodes before any nodes configure: generates keys as
  162. needed.
  163. """
  164. def config(self, net):
  165. """Called to configure a node: creates a torrc file for it."""
  166. def postConfig(self, net):
  167. """Called on each nodes after all nodes configure."""
  168. class NodeController(_NodeCommon):
  169. """Abstract base class. A NodeController is responsible for running a
  170. node on the network.
  171. """
  172. def __init__(self, env):
  173. _NodeCommon.__init__(self, env)
  174. def check(self, listRunning=True, listNonRunning=False):
  175. """See if this node is running, stopped, or crashed. If it's running
  176. and listRunning is set, print a short statement. If it's
  177. stopped and listNonRunning is set, then print a short statement.
  178. If it's crashed, print a statement. Return True if the
  179. node is running, false otherwise.
  180. """
  181. def start(self):
  182. """Try to start this node; return True if we succeeded or it was
  183. already running, False if we failed."""
  184. def stop(self, sig=signal.SIGINT):
  185. """Try to stop this node by sending it the signal 'sig'."""
  186. class LocalNodeBuilder(NodeBuilder):
  187. # Environment members used:
  188. # torrc -- which torrc file to use
  189. # torrc_template_path -- path to search for torrc files and include files
  190. # authority -- bool -- are we an authority?
  191. # bridgeauthority -- bool -- are we a bridge authority?
  192. # relay -- bool -- are we a relay?
  193. # bridge -- bool -- are we a bridge?
  194. # hs -- bool -- are we a hidden service?
  195. # nodenum -- int -- set by chutney -- which unique node index is this?
  196. # dir -- path -- set by chutney -- data directory for this tor
  197. # tor_gencert -- path to tor_gencert binary
  198. # tor -- path to tor binary
  199. # auth_cert_lifetime -- lifetime of authority certs, in months.
  200. # ip -- IP to listen on
  201. # ipv6_addr -- IPv6 address to listen on
  202. # orport, dirport -- used on authorities, relays, and bridges
  203. # fingerprint -- used only if authority
  204. # dirserver_flags -- used only if authority
  205. # nick -- nickname of this router
  206. # Environment members set
  207. # fingerprint -- hex router key fingerprint
  208. # nodenum -- int -- set by chutney -- which unique node index is this?
  209. def __init__(self, env):
  210. NodeBuilder.__init__(self, env)
  211. self._env = env
  212. def _warnMissingTor(self, tor, cmdline,
  213. tor_name="tor", tor_env="CHUTNEY_TOR"):
  214. """Log a warning that the binary tor can't be found while running
  215. cmdline. Advise the user to put the path to tor_name in the tor_env
  216. environmental variable.
  217. """
  218. print(("Cannot find the {} binary {} for the command line '{}'. " +
  219. "Use the {} environment variable to set the path, " +
  220. "or put the binary into $PATH: '{}'. " +
  221. "If your $PATH contains ~, use $HOME instead.")
  222. .format(tor_name, tor, " ".join(cmdline),
  223. tor_env, os.getenv("PATH")))
  224. def _createTorrcFile(self, checkOnly=False):
  225. """Write the torrc file for this node, disabling any options
  226. that are not supported by env's tor binary using comments.
  227. If checkOnly, just make sure that the formatting is indeed
  228. possible.
  229. """
  230. fn_out = self._getTorrcFname()
  231. torrc_template = self._getTorrcTemplate()
  232. output = torrc_template.format(self._env)
  233. if checkOnly:
  234. # XXXX Is it time-consuming to format? If so, cache here.
  235. return
  236. # now filter the options we're about to write, commenting out
  237. # the options that the current tor binary doesn't support
  238. tor = self._env['tor']
  239. # find the options the current tor binary supports, and cache them
  240. if tor not in _TORRC_OPTIONS:
  241. cmdline = [
  242. tor,
  243. "--list-torrc-options",
  244. "--hush"]
  245. try:
  246. opts = subprocess.check_output(cmdline, bufsize=-1,
  247. universal_newlines=True)
  248. except OSError as e:
  249. # only catch file not found error
  250. if e.errno == errno.ENOENT:
  251. self._warnMissingTor(tor, cmdline)
  252. sys.exit(0)
  253. else:
  254. raise
  255. # check we received a list of options, and nothing else
  256. assert re.match(r'(^\w+$)+', opts, flags=re.MULTILINE)
  257. torrc_opts = opts.split()
  258. # cache the options for this tor binary's path
  259. _TORRC_OPTIONS[tor] = torrc_opts
  260. else:
  261. torrc_opts = _TORRC_OPTIONS[tor]
  262. # check if each option is supported before writing it
  263. # Unsupported option values may need special handling.
  264. with open(fn_out, 'w') as f:
  265. # we need to do case-insensitive option comparison
  266. # even if this is a static whitelist,
  267. # so we convert to lowercase as close to the loop as possible
  268. lower_opts = [opt.lower() for opt in torrc_opts]
  269. # keep ends when splitting lines, so we can write them out
  270. # using writelines() without messing around with "\n"s
  271. for line in output.splitlines(True):
  272. # check if the first word on the line is a supported option,
  273. # preserving empty lines and comment lines
  274. sline = line.strip()
  275. if (len(sline) == 0 or
  276. sline[0] == '#' or
  277. sline.split()[0].lower() in lower_opts):
  278. f.writelines([line])
  279. else:
  280. # well, this could get spammy
  281. # TODO: warn once per option per tor binary
  282. # TODO: print tor version?
  283. print(("The tor binary at %r does not support the "
  284. "option in the torrc line:\n"
  285. "%r") % (tor, line.strip()))
  286. # we could decide to skip these lines entirely
  287. # TODO: write tor version?
  288. f.writelines(["# " + tor + " unsupported: " + line])
  289. def _getTorrcTemplate(self):
  290. """Return the template used to write the torrc for this node."""
  291. template_path = self._env['torrc_template_path']
  292. return chutney.Templating.Template("$${include:$torrc}",
  293. includePath=template_path)
  294. def _getFreeVars(self):
  295. """Return a set of the free variables in the torrc template for this
  296. node.
  297. """
  298. template = self._getTorrcTemplate()
  299. return template.freevars(self._env)
  300. def checkConfig(self, net):
  301. """Try to format our torrc; raise an exception if we can't.
  302. """
  303. self._createTorrcFile(checkOnly=True)
  304. def preConfig(self, net):
  305. """Called on all nodes before any nodes configure: generates keys and
  306. hidden service directories as needed.
  307. """
  308. self._makeDataDir()
  309. if self._env['authority']:
  310. self._genAuthorityKey()
  311. if self._env['relay']:
  312. self._genRouterKey()
  313. if self._env['hs']:
  314. self._makeHiddenServiceDir()
  315. def config(self, net):
  316. """Called to configure a node: creates a torrc file for it."""
  317. self._createTorrcFile()
  318. # self._createScripts()
  319. def postConfig(self, net):
  320. """Called on each nodes after all nodes configure."""
  321. # self.net.addNode(self)
  322. pass
  323. def _makeDataDir(self):
  324. """Create the data directory (with keys subdirectory) for this node.
  325. """
  326. datadir = self._env['dir']
  327. mkdir_p(os.path.join(datadir, 'keys'))
  328. def _makeHiddenServiceDir(self):
  329. """Create the hidden service subdirectory for this node.
  330. The directory name is stored under the 'hs_directory' environment
  331. key. It is combined with the 'dir' data directory key to yield the
  332. path to the hidden service directory.
  333. """
  334. datadir = self._env['dir']
  335. mkdir_p(os.path.join(datadir, self._env['hs_directory']))
  336. def _genAuthorityKey(self):
  337. """Generate an authority identity and signing key for this authority,
  338. if they do not already exist."""
  339. datadir = self._env['dir']
  340. tor_gencert = self._env['tor_gencert']
  341. lifetime = self._env['auth_cert_lifetime']
  342. idfile = os.path.join(datadir, 'keys', "authority_identity_key")
  343. skfile = os.path.join(datadir, 'keys', "authority_signing_key")
  344. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  345. addr = self.expand("${ip}:${dirport}")
  346. passphrase = self._env['auth_passphrase']
  347. if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
  348. return
  349. cmdline = [
  350. tor_gencert,
  351. '--create-identity-key',
  352. '--passphrase-fd', '0',
  353. '-i', idfile,
  354. '-s', skfile,
  355. '-c', certfile,
  356. '-m', str(lifetime),
  357. '-a', addr]
  358. print("Creating identity key %s for %s with %s" % (
  359. idfile, self._env['nick'], " ".join(cmdline)))
  360. try:
  361. p = subprocess.Popen(cmdline, stdin=subprocess.PIPE)
  362. except OSError as e:
  363. # only catch file not found error
  364. if e.errno == errno.ENOENT:
  365. self._warnMissingTor(tor_gencert, cmdline,
  366. tor_name="tor-gencert",
  367. tor_env="CHUTNEY_TOR_GENCERT")
  368. sys.exit(0)
  369. else:
  370. raise
  371. p.communicate(passphrase + "\n")
  372. assert p.returncode == 0 # XXXX BAD!
  373. def _genRouterKey(self):
  374. """Generate an identity key for this router, unless we already have,
  375. and set up the 'fingerprint' entry in the Environ.
  376. """
  377. datadir = self._env['dir']
  378. tor = self._env['tor']
  379. torrc = self._getTorrcFname()
  380. cmdline = [
  381. tor,
  382. "--ignore-missing-torrc",
  383. "-f", torrc,
  384. "--list-fingerprint",
  385. "--orport", "1",
  386. "--datadirectory", datadir,
  387. "--quiet",
  388. ]
  389. try:
  390. p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
  391. except OSError as e:
  392. # only catch file not found error
  393. if e.errno == errno.ENOENT:
  394. self._warnMissingTor(tor, cmdline)
  395. sys.exit(0)
  396. else:
  397. raise
  398. stdout, stderr = p.communicate()
  399. fingerprint = "".join((stdout.rstrip().split('\n')[-1]).split()[1:])
  400. if not re.match(r'^[A-F0-9]{40}$', fingerprint):
  401. print(("Error when calling %r. It gave %r as a fingerprint "
  402. " and %r on stderr.") % (" ".join(cmdline), stdout, stderr))
  403. sys.exit(1)
  404. self._env['fingerprint'] = fingerprint
  405. def _getAltAuthLines(self, hasbridgeauth=False):
  406. """Return a combination of AlternateDirAuthority,
  407. and AlternateBridgeAuthority lines for
  408. this Node, appropriately. Non-authorities return ""."""
  409. if not self._env['authority']:
  410. return ""
  411. datadir = self._env['dir']
  412. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  413. v3id = None
  414. with open(certfile, 'r') as f:
  415. for line in f:
  416. if line.startswith("fingerprint"):
  417. v3id = line.split()[1].strip()
  418. break
  419. assert v3id is not None
  420. if self._env['bridgeauthority']:
  421. # Bridge authorities return AlternateBridgeAuthority with
  422. # the 'bridge' flag set.
  423. options = ("AlternateBridgeAuthority",)
  424. self._env['dirserver_flags'] += " bridge"
  425. else:
  426. # Directory authorities return AlternateDirAuthority with
  427. # the 'v3ident' flag set.
  428. # XXXX This next line is needed for 'bridges' but breaks
  429. # 'basic'
  430. if hasbridgeauth:
  431. options = ("AlternateDirAuthority",)
  432. else:
  433. options = ("DirAuthority",)
  434. self._env['dirserver_flags'] += " v3ident=%s" % v3id
  435. authlines = ""
  436. for authopt in options:
  437. authlines += "%s %s orport=%s" % (
  438. authopt, self._env['nick'], self._env['orport'])
  439. # It's ok to give an authority's IPv6 address to an IPv4-only
  440. # client or relay: it will and must ignore it
  441. if self._env['ipv6_addr'] is not None:
  442. authlines += " ipv6=%s:%s" % (self._env['ipv6_addr'],
  443. self._env['orport'])
  444. authlines += " %s %s:%s %s\n" % (
  445. self._env['dirserver_flags'], self._env['ip'],
  446. self._env['dirport'], self._env['fingerprint'])
  447. return authlines
  448. def _getBridgeLines(self):
  449. """Return potential Bridge line for this Node. Non-bridge
  450. relays return "".
  451. """
  452. if not self._env['bridge']:
  453. return ""
  454. bridgelines = "Bridge %s:%s\n" % (self._env['ip'],
  455. self._env['orport'])
  456. if self._env['ipv6_addr'] is not None:
  457. bridgelines += "Bridge %s:%s\n" % (self._env['ipv6_addr'],
  458. self._env['orport'])
  459. return bridgelines
  460. class LocalNodeController(NodeController):
  461. def __init__(self, env):
  462. NodeController.__init__(self, env)
  463. self._env = env
  464. def getPid(self):
  465. """Assuming that this node has its pidfile in ${dir}/pid, return
  466. the pid of the running process, or None if there is no pid in the
  467. file.
  468. """
  469. pidfile = os.path.join(self._env['dir'], 'pid')
  470. if not os.path.exists(pidfile):
  471. return None
  472. with open(pidfile, 'r') as f:
  473. return int(f.read())
  474. def isRunning(self, pid=None):
  475. """Return true iff this node is running. (If 'pid' is provided, we
  476. assume that the pid provided is the one of this node. Otherwise
  477. we call getPid().
  478. """
  479. if pid is None:
  480. pid = self.getPid()
  481. if pid is None:
  482. return False
  483. try:
  484. os.kill(pid, 0) # "kill 0" == "are you there?"
  485. except OSError as e:
  486. if e.errno == errno.ESRCH:
  487. return False
  488. raise
  489. # okay, so the process exists. Say "True" for now.
  490. # XXXX check if this is really tor!
  491. return True
  492. def check(self, listRunning=True, listNonRunning=False):
  493. """See if this node is running, stopped, or crashed. If it's running
  494. and listRunning is set, print a short statement. If it's
  495. stopped and listNonRunning is set, then print a short statement.
  496. If it's crashed, print a statement. Return True if the
  497. node is running, false otherwise.
  498. """
  499. # XXX Split this into "check" and "print" parts.
  500. pid = self.getPid()
  501. nick = self._env['nick']
  502. datadir = self._env['dir']
  503. corefile = "core.%s" % pid
  504. if self.isRunning(pid):
  505. if listRunning:
  506. print("%s is running with PID %s" % (nick, pid))
  507. return True
  508. elif os.path.exists(os.path.join(datadir, corefile)):
  509. if listNonRunning:
  510. print("%s seems to have crashed, and left core file %s" % (
  511. nick, corefile))
  512. return False
  513. else:
  514. if listNonRunning:
  515. print("%s is stopped" % nick)
  516. return False
  517. def hup(self):
  518. """Send a SIGHUP to this node, if it's running."""
  519. pid = self.getPid()
  520. nick = self._env['nick']
  521. if self.isRunning(pid):
  522. print("Sending sighup to %s" % nick)
  523. os.kill(pid, signal.SIGHUP)
  524. return True
  525. else:
  526. print("%s is not running" % nick)
  527. return False
  528. def start(self):
  529. """Try to start this node; return True if we succeeded or it was
  530. already running, False if we failed."""
  531. if self.isRunning():
  532. print("%s is already running" % self._env['nick'])
  533. return True
  534. tor_path = self._env['tor']
  535. torrc = self._getTorrcFname()
  536. cmdline = [
  537. tor_path,
  538. "-f", torrc,
  539. "--quiet",
  540. ]
  541. try:
  542. p = subprocess.Popen(cmdline)
  543. except OSError as e:
  544. # only catch file not found error
  545. if e.errno == errno.ENOENT:
  546. print("Cannot find tor binary %r. Use CHUTNEY_TOR "
  547. "environment variable to set the path, or put the "
  548. "binary into $PATH." % tor_path)
  549. sys.exit(0)
  550. else:
  551. raise
  552. if self.waitOnLaunch():
  553. # this requires that RunAsDaemon is set
  554. p.wait()
  555. else:
  556. # this does not require RunAsDaemon to be set, but is slower.
  557. #
  558. # poll() only catches failures before the call itself
  559. # so let's sleep a little first
  560. # this does, of course, slow down process launch
  561. # which can require an adjustment to the voting interval
  562. #
  563. # avoid writing a newline or space when polling
  564. # so output comes out neatly
  565. sys.stdout.write('.')
  566. sys.stdout.flush()
  567. time.sleep(self._env['poll_launch_time'])
  568. p.poll()
  569. if p.returncode is not None and p.returncode != 0:
  570. if self._env['poll_launch_time'] is None:
  571. print("Couldn't launch %s (%s): %s" % (self._env['nick'],
  572. " ".join(cmdline),
  573. p.returncode))
  574. else:
  575. print("Couldn't poll %s (%s) "
  576. "after waiting %s seconds for launch"
  577. ": %s" % (self._env['nick'],
  578. " ".join(cmdline),
  579. self._env['poll_launch_time'],
  580. p.returncode))
  581. return False
  582. return True
  583. def stop(self, sig=signal.SIGINT):
  584. """Try to stop this node by sending it the signal 'sig'."""
  585. pid = self.getPid()
  586. if not self.isRunning(pid):
  587. print("%s is not running" % self._env['nick'])
  588. return
  589. os.kill(pid, sig)
  590. def cleanup_lockfile(self):
  591. lf = self._env['lockfile']
  592. if not self.isRunning() and os.path.exists(lf):
  593. print('Removing stale lock file for {0} ...'.format(
  594. self._env['nick']))
  595. os.remove(lf)
  596. def waitOnLaunch(self):
  597. """Check whether we can wait() for the tor process to launch"""
  598. # TODO: is this the best place for this code?
  599. # RunAsDaemon default is 0
  600. runAsDaemon = False
  601. with open(self._getTorrcFname(), 'r') as f:
  602. for line in f.readlines():
  603. stline = line.strip()
  604. # if the line isn't all whitespace or blank
  605. if len(stline) > 0:
  606. splline = stline.split()
  607. # if the line has at least two tokens on it
  608. if (len(splline) > 0 and
  609. splline[0].lower() == "RunAsDaemon".lower() and
  610. splline[1] == "1"):
  611. # use the RunAsDaemon value from the torrc
  612. # TODO: multiple values?
  613. runAsDaemon = True
  614. if runAsDaemon:
  615. # we must use wait() instead of poll()
  616. self._env['poll_launch_time'] = None
  617. return True
  618. else:
  619. # we must use poll() instead of wait()
  620. if self._env['poll_launch_time'] is None:
  621. self._env['poll_launch_time'] = \
  622. self._env['poll_launch_time_default']
  623. return False
  624. # XXX: document these options
  625. DEFAULTS = {
  626. 'authority': False,
  627. 'bridgeauthority': False,
  628. 'hasbridgeauth': False,
  629. 'relay': False,
  630. 'bridge': False,
  631. 'hs': False,
  632. 'hs_directory': 'hidden_service',
  633. 'hs-hostname': None,
  634. 'connlimit': 60,
  635. 'net_base_dir': get_absolute_net_path(),
  636. 'tor': os.environ.get('CHUTNEY_TOR', 'tor'),
  637. 'tor-gencert': os.environ.get('CHUTNEY_TOR_GENCERT', None),
  638. 'auth_cert_lifetime': 12,
  639. 'ip': os.environ.get('CHUTNEY_LISTEN_ADDRESS', '127.0.0.1'),
  640. # we default to ipv6_addr None to support IPv4-only systems
  641. 'ipv6_addr': os.environ.get('CHUTNEY_LISTEN_ADDRESS_V6', None),
  642. 'dirserver_flags': 'no-v2',
  643. 'chutney_dir': get_absolute_chutney_path(),
  644. 'torrc_fname': '${dir}/torrc',
  645. 'orport_base': 5000,
  646. 'dirport_base': 7000,
  647. 'controlport_base': 8000,
  648. 'socksport_base': 9000,
  649. 'authorities': "AlternateDirAuthority bleargh bad torrc file!",
  650. 'bridges': "Bridge bleargh bad torrc file!",
  651. 'core': True,
  652. # poll_launch_time: None means wait on launch (requires RunAsDaemon),
  653. # otherwise, poll after that many seconds (can be fractional/decimal)
  654. 'poll_launch_time': None,
  655. # Used when poll_launch_time is None, but RunAsDaemon is not set
  656. # Set low so that we don't interfere with the voting interval
  657. 'poll_launch_time_default': 0.1,
  658. # the number of bytes of random data we send on each connection
  659. 'data_bytes': int(os.environ.get('CHUTNEY_DATA_BYTES', 10 * 1024)),
  660. # the number of times each client will connect
  661. 'connection_count': int(os.environ.get('CHUTNEY_CONNECTIONS', 1)),
  662. # Do we want every client to connect to every HS, or one client
  663. # to connect to each HS?
  664. # (Clients choose an exit at random, so this doesn't apply to exits.)
  665. 'hs_multi_client': int(os.environ.get('CHUTNEY_HS_MULTI_CLIENT', 0)),
  666. # How long should verify (and similar commands) wait for a successful
  667. # outcome? (seconds)
  668. # We check BOOTSTRAP_TIME for compatibility with old versions of
  669. # test-network.sh
  670. 'bootstrap_time': int(os.environ.get('CHUTNEY_BOOTSTRAP_TIME',
  671. os.environ.get('BOOTSTRAP_TIME',
  672. 60))),
  673. # the PID of the controlling script (for __OwningControllerProcess)
  674. 'controlling_pid': (int(os.environ.get('CHUTNEY_CONTROLLING_PID', 0))
  675. if 'CHUTNEY_CONTROLLING_PID' in os.environ
  676. else None),
  677. # a DNS config file (for ServerDNSResolvConfFile)
  678. 'dns_conf': (os.environ.get('CHUTNEY_DNS_CONF', '/etc/resolv.conf')
  679. if 'CHUTNEY_DNS_CONF' in os.environ
  680. else None),
  681. }
  682. class TorEnviron(chutney.Templating.Environ):
  683. """Subclass of chutney.Templating.Environ to implement commonly-used
  684. substitutions.
  685. Environment fields provided:
  686. orport, controlport, socksport, dirport: *Port torrc option
  687. dir: DataDirectory torrc option
  688. nick: Nickname torrc option
  689. tor_gencert: name or path of the tor-gencert binary
  690. auth_passphrase: obsoleted by CookieAuthentication
  691. torrc_template_path: path to chutney torrc_templates directory
  692. hs_hostname: the hostname of the key generated by a hidden service
  693. owning_controller_process: the __OwningControllerProcess torrc line,
  694. disabled if tor should continue after the script exits
  695. server_dns_resolv_conf: the ServerDNSResolvConfFile torrc line,
  696. disabled if tor should use the default DNS conf.
  697. If the dns_conf file is missing, this option is also disabled:
  698. otherwise, exits would not work due to tor bug #21900.
  699. Environment fields used:
  700. nodenum: chutney's internal node number for the node
  701. tag: a short text string that represents the type of node
  702. orport_base, controlport_base, socksport_base, dirport_base: the
  703. initial port numbers used by nodenum 0. Each additional node adds
  704. 1 to the port numbers.
  705. tor-gencert (note hyphen): name or path of the tor-gencert binary (if
  706. present)
  707. chutney_dir: directory of the chutney source code
  708. tor: name or path of the tor binary
  709. net_base_dir: path to the chutney net directory
  710. hs_directory: name of the hidden service directory
  711. nick: Nickname torrc option (debugging only)
  712. hs-hostname (note hyphen): cached hidden service hostname value
  713. controlling_pid: the PID of the controlling process. After this
  714. process exits, the child tor processes will exit
  715. dns_conf: the path to a DNS config file for Tor Exits. If this file
  716. is empty or unreadable, Tor will try 127.0.0.1:53.
  717. """
  718. def __init__(self, parent=None, **kwargs):
  719. chutney.Templating.Environ.__init__(self, parent=parent, **kwargs)
  720. def _get_orport(self, my):
  721. return my['orport_base'] + my['nodenum']
  722. def _get_controlport(self, my):
  723. return my['controlport_base'] + my['nodenum']
  724. def _get_socksport(self, my):
  725. return my['socksport_base'] + my['nodenum']
  726. def _get_dirport(self, my):
  727. return my['dirport_base'] + my['nodenum']
  728. def _get_dir(self, my):
  729. return os.path.abspath(os.path.join(my['net_base_dir'],
  730. "nodes",
  731. "%03d%s" % (
  732. my['nodenum'], my['tag'])))
  733. def _get_nick(self, my):
  734. return "test%03d%s" % (my['nodenum'], my['tag'])
  735. def _get_tor_gencert(self, my):
  736. return my['tor-gencert'] or '{0}-gencert'.format(my['tor'])
  737. def _get_auth_passphrase(self, my):
  738. return self['nick'] # OMG TEH SECURE!
  739. def _get_torrc_template_path(self, my):
  740. return [os.path.join(my['chutney_dir'], 'torrc_templates')]
  741. def _get_lockfile(self, my):
  742. return os.path.join(self['dir'], 'lock')
  743. # A hs generates its key on first run,
  744. # so check for it at the last possible moment,
  745. # but cache it in memory to avoid repeatedly reading the file
  746. # XXXX - this is not like the other functions in this class,
  747. # as it reads from a file created by the hidden service
  748. def _get_hs_hostname(self, my):
  749. if my['hs-hostname'] is None:
  750. datadir = my['dir']
  751. # a file containing a single line with the hs' .onion address
  752. hs_hostname_file = os.path.join(datadir, my['hs_directory'],
  753. 'hostname')
  754. try:
  755. with open(hs_hostname_file, 'r') as hostnamefp:
  756. hostname = hostnamefp.read()
  757. # the hostname file ends with a newline
  758. hostname = hostname.strip()
  759. my['hs-hostname'] = hostname
  760. except IOError as e:
  761. print("Error: hs %r error %d: %r opening hostname file '%r'" %
  762. (my['nick'], e.errno, e.strerror, hs_hostname_file))
  763. return my['hs-hostname']
  764. def _get_owning_controller_process(self, my):
  765. cpid = my['controlling_pid']
  766. if cpid is None:
  767. cpid = 0
  768. ocp_line = ('__OwningControllerProcess %d' % (cpid))
  769. # if we want to leave the network running, or controlling_pid is 1
  770. # (or invalid)
  771. if (os.environ.get('CHUTNEY_START_TIME', 0) < 0 or
  772. os.environ.get('CHUTNEY_BOOTSTRAP_TIME', 0) < 0 or
  773. os.environ.get('CHUTNEY_STOP_TIME', 0) < 0 or
  774. cpid <= 1):
  775. return '#' + ocp_line
  776. else:
  777. return ocp_line
  778. # the default resolv.conf path is set at compile time
  779. # there's no easy way to get it out of tor, so we use the typical value
  780. DEFAULT_DNS_RESOLV_CONF = "/etc/resolv.conf"
  781. # if we can't find the specified file, use this one as a substitute
  782. OFFLINE_DNS_RESOLV_CONF = "/dev/null"
  783. def _get_server_dns_resolv_conf(self, my):
  784. if my['dns_conf'] == "":
  785. # if the user asked for tor's default
  786. return "#ServerDNSResolvConfFile using tor's compile-time default"
  787. elif my['dns_conf'] is None:
  788. # if there is no DNS conf file set
  789. print("CHUTNEY_DNS_CONF not specified, using '%s'."
  790. % (TorEnviron.DEFAULT_DNS_RESOLV_CONF))
  791. dns_conf = TorEnviron.DEFAULT_DNS_RESOLV_CONF
  792. else:
  793. dns_conf = my['dns_conf']
  794. dns_conf = os.path.abspath(dns_conf)
  795. # work around Tor bug #21900, where exits fail when the DNS conf
  796. # file does not exist, or is a broken symlink
  797. # (os.path.exists returns False for broken symbolic links)
  798. if not os.path.exists(dns_conf):
  799. # Issue a warning so the user notices
  800. print("CHUTNEY_DNS_CONF '%s' does not exist, using '%s'."
  801. % (dns_conf, TorEnviron.OFFLINE_DNS_RESOLV_CONF))
  802. dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
  803. return "ServerDNSResolvConfFile %s" % (dns_conf)
  804. class Network(object):
  805. """A network of Tor nodes, plus functions to manipulate them
  806. """
  807. def __init__(self, defaultEnviron):
  808. self._nodes = []
  809. self._dfltEnv = defaultEnviron
  810. self._nextnodenum = 0
  811. def _addNode(self, n):
  812. n.setNodenum(self._nextnodenum)
  813. self._nextnodenum += 1
  814. self._nodes.append(n)
  815. def move_aside_nodes_dir(self):
  816. """Move aside the nodes directory, if it exists and is not a link.
  817. Used for backwards-compatibility only: nodes is created as a link to
  818. a new directory with a unique name in the current implementation.
  819. """
  820. nodesdir = get_absolute_nodes_path()
  821. # only move the directory if it exists
  822. if not os.path.exists(nodesdir):
  823. return
  824. # and if it's not a link
  825. if os.path.islink(nodesdir):
  826. return
  827. # subtract 1 second to avoid collisions and get the correct ordering
  828. newdir = get_new_absolute_nodes_path(time.time() - 1)
  829. print("NOTE: renaming %r to %r" % (nodesdir, newdir))
  830. os.rename(nodesdir, newdir)
  831. def create_new_nodes_dir(self):
  832. """Create a new directory with a unique name, and symlink it to nodes
  833. """
  834. # for backwards compatibility, move aside the old nodes directory
  835. # (if it's not a link)
  836. self.move_aside_nodes_dir()
  837. # the unique directory we'll create
  838. newnodesdir = get_new_absolute_nodes_path()
  839. # the canonical name we'll link it to
  840. nodeslink = get_absolute_nodes_path()
  841. # this path should be unique and should not exist
  842. if os.path.exists(newnodesdir):
  843. raise RuntimeError(
  844. 'get_new_absolute_nodes_path returned a path that exists')
  845. # if this path exists, it must be a link
  846. if os.path.exists(nodeslink) and not os.path.islink(nodeslink):
  847. raise RuntimeError(
  848. 'get_absolute_nodes_path returned a path that exists and is not a link')
  849. # create the new, uniquely named directory, and link it to nodes
  850. print("NOTE: creating %r, linking to %r" % (newnodesdir, nodeslink))
  851. # this gets created with mode 0700, that's probably ok
  852. mkdir_p(newnodesdir)
  853. try:
  854. os.unlink(nodeslink)
  855. except OSError as e:
  856. # it's ok if the link doesn't exist, we're just about to make it
  857. if e.errno == errno.ENOENT:
  858. pass
  859. else:
  860. raise
  861. os.symlink(newnodesdir, nodeslink)
  862. def _checkConfig(self):
  863. for n in self._nodes:
  864. n.getBuilder().checkConfig(self)
  865. def configure(self):
  866. self.create_new_nodes_dir()
  867. network = self
  868. altauthlines = []
  869. bridgelines = []
  870. builders = [n.getBuilder() for n in self._nodes]
  871. self._checkConfig()
  872. # XXX don't change node names or types or count if anything is
  873. # XXX running!
  874. for b in builders:
  875. b.preConfig(network)
  876. altauthlines.append(b._getAltAuthLines(
  877. self._dfltEnv['hasbridgeauth']))
  878. bridgelines.append(b._getBridgeLines())
  879. self._dfltEnv['authorities'] = "".join(altauthlines)
  880. self._dfltEnv['bridges'] = "".join(bridgelines)
  881. for b in builders:
  882. b.config(network)
  883. for b in builders:
  884. b.postConfig(network)
  885. def status(self):
  886. statuses = [n.getController().check() for n in self._nodes]
  887. n_ok = len([x for x in statuses if x])
  888. print("%d/%d nodes are running" % (n_ok, len(self._nodes)))
  889. return n_ok == len(self._nodes)
  890. def restart(self):
  891. self.stop()
  892. self.start()
  893. def start(self):
  894. if self._dfltEnv['poll_launch_time'] is not None:
  895. # format polling correctly - avoid printing a newline
  896. sys.stdout.write("Starting nodes")
  897. sys.stdout.flush()
  898. else:
  899. print("Starting nodes")
  900. rv = all([n.getController().start() for n in self._nodes])
  901. # now print a newline unconditionally - this stops poll()ing
  902. # output from being squashed together, at the cost of a blank
  903. # line in wait()ing output
  904. print("")
  905. return rv
  906. def hup(self):
  907. print("Sending SIGHUP to nodes")
  908. return all([n.getController().hup() for n in self._nodes])
  909. def stop(self):
  910. controllers = [n.getController() for n in self._nodes]
  911. for sig, desc in [(signal.SIGINT, "SIGINT"),
  912. (signal.SIGINT, "another SIGINT"),
  913. (signal.SIGKILL, "SIGKILL")]:
  914. print("Sending %s to nodes" % desc)
  915. for c in controllers:
  916. if c.isRunning():
  917. c.stop(sig=sig)
  918. print("Waiting for nodes to finish.")
  919. wrote_dot = False
  920. for n in range(15):
  921. time.sleep(1)
  922. if all(not c.isRunning() for c in controllers):
  923. # make the output clearer by adding a newline
  924. if wrote_dot:
  925. sys.stdout.write("\n")
  926. sys.stdout.flush()
  927. # check for stale lock file when Tor crashes
  928. for c in controllers:
  929. c.cleanup_lockfile()
  930. return
  931. sys.stdout.write(".")
  932. wrote_dot = True
  933. sys.stdout.flush()
  934. for c in controllers:
  935. c.check(listNonRunning=False)
  936. # make the output clearer by adding a newline
  937. if wrote_dot:
  938. sys.stdout.write("\n")
  939. sys.stdout.flush()
  940. def ConfigureNodes(nodelist):
  941. network = _THE_NETWORK
  942. for n in nodelist:
  943. network._addNode(n)
  944. if n._env['bridgeauthority']:
  945. network._dfltEnv['hasbridgeauth'] = True
  946. def getTests():
  947. tests = []
  948. chutney_path = get_absolute_chutney_path()
  949. if len(chutney_path) > 0 and chutney_path[-1] != '/':
  950. chutney_path += "/"
  951. for x in os.listdir(chutney_path + "scripts/chutney_tests/"):
  952. if not x.startswith("_") and os.path.splitext(x)[1] == ".py":
  953. tests.append(os.path.splitext(x)[0])
  954. return tests
  955. def usage(network):
  956. return "\n".join(["Usage: chutney {command/test} {networkfile}",
  957. "Known commands are: %s" % (
  958. " ".join(x for x in dir(network)
  959. if not x.startswith("_"))),
  960. "Known tests are: %s" % (
  961. " ".join(getTests()))
  962. ])
  963. def exit_on_error(err_msg):
  964. print("Error: {0}\n".format(err_msg))
  965. print(usage(_THE_NETWORK))
  966. sys.exit(1)
  967. def runConfigFile(verb, data):
  968. _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
  969. Node=Node,
  970. ConfigureNodes=ConfigureNodes,
  971. _THE_NETWORK=_THE_NETWORK)
  972. exec(data, _GLOBALS)
  973. network = _GLOBALS['_THE_NETWORK']
  974. # let's check if the verb is a valid test and run it
  975. if verb in getTests():
  976. test_module = importlib.import_module("chutney_tests.{}".format(verb))
  977. try:
  978. return test_module.run_test(network)
  979. except AttributeError as e:
  980. print("Error running test {!r}: {}".format(verb, e))
  981. return False
  982. # tell the user we don't know what their verb meant
  983. if not hasattr(network, verb):
  984. print(usage(network))
  985. print("Error: I don't know how to %s." % verb)
  986. return
  987. return getattr(network, verb)()
  988. def parseArgs():
  989. if len(sys.argv) < 3:
  990. exit_on_error("Not enough arguments given.")
  991. if not os.path.isfile(sys.argv[2]):
  992. exit_on_error("Cannot find networkfile: {0}.".format(sys.argv[2]))
  993. return {'network_cfg': sys.argv[2], 'action': sys.argv[1]}
  994. def main():
  995. global _BASE_ENVIRON
  996. global _TORRC_OPTIONS
  997. global _THE_NETWORK
  998. _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
  999. # _TORRC_OPTIONS gets initialised on demand as a map of
  1000. # "/path/to/tor" => ["SupportedOption1", "SupportedOption2", ...]
  1001. # Or it can be pre-populated as a static whitelist of options
  1002. _TORRC_OPTIONS = dict()
  1003. _THE_NETWORK = Network(_BASE_ENVIRON)
  1004. args = parseArgs()
  1005. f = open(args['network_cfg'])
  1006. result = runConfigFile(args['action'], f)
  1007. if result is False:
  1008. return -1
  1009. return 0
  1010. if __name__ == '__main__':
  1011. sys.exit(main())