TorNet.py 32 KB

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