TorNet.py 33 KB

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