TorNet.py 52 KB

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