TorNet.py 37 KB

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