TorNet.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/usr/bin/python
  2. #
  3. # Copyright 2011 Nick Mathewson, Michael Stone
  4. #
  5. # You may do anything with this work that copyright law would normally
  6. # restrict, so long as you retain the above notice(s) and this license
  7. # in all redistributed copies and derived works. There is no warranty.
  8. from __future__ import with_statement
  9. # Get verbose tracebacks, so we can diagnose better.
  10. import cgitb
  11. cgitb.enable(format="plain")
  12. import os
  13. import signal
  14. import subprocess
  15. import sys
  16. import re
  17. import errno
  18. import time
  19. import chutney.Templating
  20. def mkdir_p(d, mode=0777):
  21. """Create directory 'd' and all of its parents as needed. Unlike
  22. os.makedirs, does not give an error if d already exists.
  23. """
  24. try:
  25. os.makedirs(d,mode=mode)
  26. except OSError, e:
  27. if e.errno == errno.EEXIST:
  28. return
  29. raise
  30. class Node(object):
  31. """A Node represents a Tor node or a set of Tor nodes. It's created
  32. in a network configuration file.
  33. This class is responsible for holding the user's selected node
  34. configuration, and figuring out how the node needs to be
  35. configured and launched.
  36. """
  37. # XXXXX Split this class up; its various methods are too ungainly,
  38. # and if we let them start talking to each other too intimately,
  39. # we'll never crowbar them apart. One possible design: it should
  40. # turn into a factory that can return a NodeLauncher and a
  41. # NodeConfigurator depending on options.
  42. ## Fields:
  43. # _parent
  44. # _env
  45. ## Environment members used:
  46. # torrc -- which torrc file to use
  47. # torrc_template_path -- path to search for torrc files and include files
  48. # authority -- bool -- are we an authority?
  49. # relay -- bool -- are we a relay
  50. # nodenum -- int -- set by chutney -- which unique node index is this?
  51. # dir -- path -- set by chutney -- data directory for this tor
  52. # tor_gencert -- path to tor_gencert binary
  53. # tor -- path to tor binary
  54. # auth_cert_lifetime -- lifetime of authority certs, in months.
  55. # ip -- IP to listen on (used only if authority)
  56. # orport, dirport -- (used only if authority)
  57. # fingerprint -- used only if authority
  58. # dirserver_flags -- used only if authority
  59. # nick -- nickname of this router
  60. ## Environment members set
  61. # fingerprint -- hex router key fingerprint
  62. # nodenum -- int -- set by chutney -- which unique node index is this?
  63. ########
  64. # Users are expected to call these:
  65. def __init__(self, parent=None, **kwargs):
  66. self._parent = parent
  67. self._env = self._createEnviron(parent, kwargs)
  68. def getN(self, N):
  69. return [ Node(self) for _ in xrange(N) ]
  70. def specialize(self, **kwargs):
  71. return Node(parent=self, **kwargs)
  72. def expand(self, pat, includePath=(".",)):
  73. return chutney.Templating.Template(pat, includePath).format(self._env)
  74. #######
  75. # Users are NOT expected to call these:
  76. def _getTorrcFname(self):
  77. """Return the name of the file where we'll be writing torrc"""
  78. return self.expand("${torrc_fname}")
  79. def _createTorrcFile(self, checkOnly=False):
  80. """Write the torrc file for this node. If checkOnly, just make sure
  81. that the formatting is indeed possible.
  82. """
  83. fn_out = self._getTorrcFname()
  84. torrc_template = self._getTorrcTemplate()
  85. output = torrc_template.format(self._env)
  86. if checkOnly:
  87. # XXXX Is it time-cosuming to format? If so, cache here.
  88. return
  89. with open(fn_out, 'w') as f:
  90. f.write(output)
  91. def _getTorrcTemplate(self):
  92. """Return the template used to write the torrc for this node."""
  93. template_path = self._env['torrc_template_path']
  94. return chutney.Templating.Template("$${include:$torrc}",
  95. includePath=template_path)
  96. def _getFreeVars(self):
  97. """Return a set of the free variables in the torrc template for this
  98. node.
  99. """
  100. template = self._getTorrcTemplate()
  101. return template.freevars(self._env)
  102. def _createEnviron(self, parent, argdict):
  103. """Return an Environ that delegates to the parent node's Environ (if
  104. there is a parent node), or to the default environment.
  105. """
  106. if parent:
  107. parentenv = parent._env
  108. else:
  109. parentenv = self._getDefaultEnviron()
  110. return TorEnviron(parentenv, **argdict)
  111. def _getDefaultEnviron(self):
  112. """Return the default environment. Any variables that we can't find
  113. set for any particular node, we look for here.
  114. """
  115. return _BASE_ENVIRON
  116. def _checkConfig(self, net):
  117. """Try to format our torrc; raise an exception if we can't.
  118. """
  119. self._createTorrcFile(checkOnly=True)
  120. def _preConfig(self, net):
  121. """Called on all nodes before any nodes configure: generates keys as
  122. needed.
  123. """
  124. self._makeDataDir()
  125. if self._env['authority']:
  126. self._genAuthorityKey()
  127. if self._env['relay']:
  128. self._genRouterKey()
  129. def _config(self, net):
  130. """Called to configure a node: creates a torrc file for it."""
  131. self._createTorrcFile()
  132. #self._createScripts()
  133. def _postConfig(self, net):
  134. """Called on each nodes after all nodes configure."""
  135. #self.net.addNode(self)
  136. pass
  137. def _setnodenum(self, num):
  138. """Assign a value to the 'nodenum' element of this node. Each node
  139. in a network gets its own nodenum.
  140. """
  141. self._env['nodenum'] = num
  142. def _makeDataDir(self):
  143. """Create the data directory (with keys subdirectory) for this node.
  144. """
  145. datadir = self._env['dir']
  146. mkdir_p(os.path.join(datadir, 'keys'))
  147. def _genAuthorityKey(self):
  148. """Generate an authority identity and signing key for this authority,
  149. if they do not already exist."""
  150. datadir = self._env['dir']
  151. tor_gencert = self._env['tor_gencert']
  152. lifetime = self._env['auth_cert_lifetime']
  153. idfile = os.path.join(datadir,'keys',"authority_identity_key")
  154. skfile = os.path.join(datadir,'keys',"authority_signing_key")
  155. certfile = os.path.join(datadir,'keys',"authority_certificate")
  156. addr = self.expand("${ip}:${dirport}")
  157. passphrase = self._env['auth_passphrase']
  158. if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
  159. return
  160. cmdline = [
  161. tor_gencert,
  162. '--create-identity-key',
  163. '--passphrase-fd', '0',
  164. '-i', idfile,
  165. '-s', skfile,
  166. '-c', certfile,
  167. '-m', str(lifetime),
  168. '-a', addr]
  169. print "Creating identity key %s for %s with %s"%(
  170. idfile,self._env['nick']," ".join(cmdline))
  171. p = subprocess.Popen(cmdline, stdin=subprocess.PIPE)
  172. p.communicate(passphrase+"\n")
  173. assert p.returncode == 0 #XXXX BAD!
  174. def _genRouterKey(self):
  175. """Generate an identity key for this router, unless we already have,
  176. and set up the 'fingerprint' entry in the Environ.
  177. """
  178. datadir = self._env['dir']
  179. tor = self._env['tor']
  180. idfile = os.path.join(datadir,'keys',"identity_key")
  181. cmdline = [
  182. tor,
  183. "--quiet",
  184. "--list-fingerprint",
  185. "--orport", "1",
  186. "--dirserver",
  187. "xyzzy 127.0.0.1:1 ffffffffffffffffffffffffffffffffffffffff",
  188. "--datadirectory", datadir ]
  189. p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
  190. stdout, stderr = p.communicate()
  191. fingerprint = "".join(stdout.split()[1:])
  192. assert re.match(r'^[A-F0-9]{40}$', fingerprint)
  193. self._env['fingerprint'] = fingerprint
  194. def _getDirServerLine(self):
  195. """Return a DirServer line for this Node. That'll be "" if this is
  196. not an authority."""
  197. if not self._env['authority']:
  198. return ""
  199. datadir = self._env['dir']
  200. certfile = os.path.join(datadir,'keys',"authority_certificate")
  201. v3id = None
  202. with open(certfile, 'r') as f:
  203. for line in f:
  204. if line.startswith("fingerprint"):
  205. v3id = line.split()[1].strip()
  206. break
  207. assert v3id is not None
  208. return "DirServer %s v3ident=%s orport=%s %s %s:%s %s\n" %(
  209. self._env['nick'], v3id, self._env['orport'],
  210. self._env['dirserver_flags'], self._env['ip'], self._env['dirport'],
  211. self._env['fingerprint'])
  212. ##### Controlling a node. This should probably get split into its
  213. # own class. XXXX
  214. def getPid(self):
  215. """Assuming that this node has its pidfile in ${dir}/pid, return
  216. the pid of the running process, or None if there is no pid in the
  217. file.
  218. """
  219. pidfile = os.path.join(self._env['dir'], 'pid')
  220. if not os.path.exists(pidfile):
  221. return None
  222. with open(pidfile, 'r') as f:
  223. return int(f.read())
  224. def isRunning(self, pid=None):
  225. """Return true iff this node is running. (If 'pid' is provided, we
  226. assume that the pid provided is the one of this node. Otherwise
  227. we call getPid().
  228. """
  229. if pid is None:
  230. pid = self.getPid()
  231. if pid is None:
  232. return False
  233. try:
  234. os.kill(pid, 0) # "kill 0" == "are you there?"
  235. except OSError, e:
  236. if e.errno == errno.ESRCH:
  237. return False
  238. raise
  239. # okay, so the process exists. Say "True" for now.
  240. # XXXX check if this is really tor!
  241. return True
  242. def check(self, listRunning=True, listNonRunning=False):
  243. """See if this node is running, stopped, or crashed. If it's running
  244. and listRunning is set, print a short statement. If it's
  245. stopped and listNonRunning is set, then print a short statement.
  246. If it's crashed, print a statement. Return True if the
  247. node is running, false otherwise.
  248. """
  249. # XXX Split this into "check" and "print" parts.
  250. pid = self.getPid()
  251. running = self.isRunning(pid)
  252. nick = self._env['nick']
  253. dir = self._env['dir']
  254. if running:
  255. if listRunning:
  256. print "%s is running with PID %s"%(nick,pid)
  257. return True
  258. elif os.path.exists(os.path.join(dir, "core.%s"%pid)):
  259. if listNonRunning:
  260. print "%s seems to have crashed, and left core file core.%s"%(
  261. nick,pid)
  262. return False
  263. else:
  264. if listNonRunning:
  265. print "%s is stopped"%nick
  266. return False
  267. def hup(self):
  268. """Send a SIGHUP to this node, if it's running."""
  269. pid = self.getPid()
  270. running = self.isRunning()
  271. nick = self._env['nick']
  272. if self.isRunning():
  273. print "Sending sighup to %s"%nick
  274. os.kill(pid, signal.SIGHUP)
  275. return True
  276. else:
  277. print "%s is not running"%nick
  278. return False
  279. def start(self):
  280. """Try to start this node; return True if we succeeded or it was
  281. already running, False if we failed."""
  282. if self.isRunning():
  283. print "%s is already running"%self._env['nick']
  284. return True
  285. torrc = self._getTorrcFname()
  286. cmdline = [
  287. self._env['tor'],
  288. "--quiet",
  289. "-f", torrc,
  290. ]
  291. p = subprocess.Popen(cmdline)
  292. # XXXX this requires that RunAsDaemon is set.
  293. p.wait()
  294. if p.returncode != 0:
  295. print "Couldn't launch %s (%s): %s"%(self._env['nick'],
  296. " ".join(cmdline),
  297. p.returncode)
  298. return False
  299. return True
  300. def stop(self, sig=signal.SIGINT):
  301. """Try to stop this node by sending it the signal 'sig'."""
  302. pid = self.getPid()
  303. if not self.isRunning(pid):
  304. print "%s is not running"%self._env['nick']
  305. return
  306. os.kill(pid, sig)
  307. DEFAULTS = {
  308. 'authority' : False,
  309. 'relay' : False,
  310. 'connlimit' : 60,
  311. 'net_base_dir' : 'net',
  312. 'tor' : 'tor',
  313. 'auth_cert_lifetime' : 12,
  314. 'ip' : '127.0.0.1',
  315. 'dirserver_flags' : 'no-v2',
  316. 'chutney_dir' : '.',
  317. 'torrc_fname' : '${dir}/torrc',
  318. 'orport_base' : 6000,
  319. 'dirport_base' : 7000,
  320. 'controlport_base' : 8000,
  321. 'socksport_base' : 9000,
  322. 'dirservers' : "Dirserver bleargh bad torrc file!",
  323. 'core' : True,
  324. }
  325. class TorEnviron(chutney.Templating.Environ):
  326. """Subclass of chutney.Templating.Environ to implement commonly-used
  327. substitutions.
  328. Environment fields provided:
  329. orport, controlport, socksport, dirport:
  330. dir:
  331. nick:
  332. tor_gencert:
  333. auth_passphrase:
  334. torrc_template_path:
  335. Environment fields used:
  336. nodenum
  337. tag
  338. orport_base, controlport_base, socksport_base, dirport_base
  339. chutney_dir
  340. tor
  341. XXXX document the above. Or document all fields in one place?
  342. """
  343. def __init__(self,parent=None,**kwargs):
  344. chutney.Templating.Environ.__init__(self, parent=parent, **kwargs)
  345. def _get_orport(self, my):
  346. return my['orport_base']+my['nodenum']
  347. def _get_controlport(self, my):
  348. return my['controlport_base']+my['nodenum']
  349. def _get_socksport(self, my):
  350. return my['socksport_base']+my['nodenum']
  351. def _get_dirport(self, my):
  352. return my['dirport_base']+my['nodenum']
  353. def _get_dir(self, my):
  354. return os.path.abspath(os.path.join(my['net_base_dir'],
  355. "nodes",
  356. "%03d%s"%(my['nodenum'], my['tag'])))
  357. def _get_nick(self, my):
  358. return "test%03d%s"%(my['nodenum'], my['tag'])
  359. def _get_tor_gencert(self, my):
  360. return my['tor']+"-gencert"
  361. def _get_auth_passphrase(self, my):
  362. return self['nick'] # OMG TEH SECURE!
  363. def _get_torrc_template_path(self, my):
  364. return [ os.path.join(my['chutney_dir'], 'torrc_templates') ]
  365. class Network(object):
  366. """A network of Tor nodes, plus functions to manipulate them
  367. """
  368. def __init__(self,defaultEnviron):
  369. self._nodes = []
  370. self._dfltEnv = defaultEnviron
  371. self._nextnodenum = 0
  372. def _addNode(self, n):
  373. n._setnodenum(self._nextnodenum)
  374. self._nextnodenum += 1
  375. self._nodes.append(n)
  376. def _checkConfig(self):
  377. for n in self._nodes:
  378. n._checkConfig(self)
  379. def configure(self):
  380. network = self
  381. dirserverlines = []
  382. self._checkConfig()
  383. # XXX don't change node names or types or count if anything is
  384. # XXX running!
  385. for n in self._nodes:
  386. n._preConfig(network)
  387. dirserverlines.append(n._getDirServerLine())
  388. self._dfltEnv['dirservers'] = "".join(dirserverlines)
  389. for n in self._nodes:
  390. n._config(network)
  391. for n in self._nodes:
  392. n._postConfig(network)
  393. def status(self):
  394. statuses = [n.check() for n in self._nodes]
  395. n_ok = len([x for x in statuses if x])
  396. print "%d/%d nodes are running"%(n_ok,len(self._nodes))
  397. def restart(self):
  398. self.stop()
  399. self.start()
  400. def start(self):
  401. print "Starting nodes"
  402. return all([n.start() for n in self._nodes])
  403. def hup(self):
  404. print "Sending SIGHUP to nodes"
  405. return all([n.hup() for n in self._nodes])
  406. def stop(self):
  407. for sig, desc in [(signal.SIGINT, "SIGINT"),
  408. (signal.SIGINT, "another SIGINT"),
  409. (signal.SIGKILL, "SIGKILL")]:
  410. print "Sending %s to nodes"%desc
  411. for n in self._nodes:
  412. if n.isRunning():
  413. n.stop(sig=sig)
  414. print "Waiting for nodes to finish."
  415. for n in xrange(15):
  416. time.sleep(1)
  417. if all(not n.isRunning() for n in self._nodes):
  418. return
  419. sys.stdout.write(".")
  420. sys.stdout.flush()
  421. for n in self._nodes:
  422. n.check(listNonRunning=False)
  423. def ConfigureNodes(nodelist):
  424. network = _THE_NETWORK
  425. for n in nodelist:
  426. network._addNode(n)
  427. def usage(network):
  428. return "\n".join(["Usage: chutney {command} {networkfile}",
  429. "Known commands are: %s" % (
  430. " ".join(x for x in dir(network) if not x.startswith("_")))])
  431. def runConfigFile(verb, f):
  432. _GLOBALS = dict(_BASE_ENVIRON= _BASE_ENVIRON,
  433. Node=Node,
  434. ConfigureNodes=ConfigureNodes,
  435. _THE_NETWORK=_THE_NETWORK)
  436. exec f in _GLOBALS
  437. network = _GLOBALS['_THE_NETWORK']
  438. if not hasattr(network, verb):
  439. print usage(network)
  440. print "Error: I don't know how to %s." % verb
  441. return
  442. getattr(network,verb)()
  443. def main():
  444. global _BASE_ENVIRON
  445. global _THE_NETWORK
  446. _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
  447. _THE_NETWORK = Network(_BASE_ENVIRON)
  448. if len(sys.argv) < 3:
  449. print usage(_THE_NETWORK)
  450. print "Error: Not enough arguments given."
  451. sys.exit(1)
  452. f = open(sys.argv[2])
  453. runConfigFile(sys.argv[1], f)
  454. if __name__ == '__main__':
  455. main()