Make introducer.furl unguessable. Closes #1802.

Previously, Introducers always used a swissnum of "introducer", so
anyone who could learn the (public) tubid of the introducer would be
able to connect to and use it. This changes new Introducers to use the
same randomly-generated swissnum as clients and storage servers do, so
that you absolutely must learn the introducer.furl from someone who
knows it already before you can connect.

This change also moves the location of the file that stores
introducer.furl from BASEDIR/introducer.furl to
BASEDIR/private/introducer.furl, since that's where we keep the private
things. The first time an introducer is started with the new code, it
will move any existing BASEDIR/introducer.furl into the new place.

Note that this will not change the FURL of existing introducers: it will
only affect newly created ones. When you change an introducer's FURL,
you must also update all of the nodes (clients and storage servers)
which connect to it, so upgrading it to an unguessable one isn't
something we should do automatically.
This commit is contained in:
Brian Warner 2013-03-20 15:10:47 -07:00 committed by David-Sarah Hopwood
parent 97a7cac7d3
commit 0a89b738bc
6 changed files with 79 additions and 26 deletions

View File

@ -294,9 +294,9 @@ Client Configuration
This FURL tells the client how to connect to the introducer. Each
Tahoe-LAFS grid is defined by an introducer. The introducer's FURL is
created by the introducer node and written into its base directory when
it starts, whereupon it should be published to everyone who wishes to
attach a client to that grid
created by the introducer node and written into its private base
directory when it starts, whereupon it should be published to everyone
who wishes to attach a client to that grid
``helper.furl = (FURL string, optional)``
@ -507,7 +507,7 @@ the others.
The Introducer node maintains some different state than regular client nodes.
``BASEDIR/introducer.furl``
``BASEDIR/private/introducer.furl``
This is generated the first time the introducer node is started, and used
again on subsequent runs, to give the introduction service a persistent

View File

@ -111,8 +111,8 @@ That is, it behaves like "``tahoe create-node --no-storage [NODEDIR]``".
"``tahoe create-introducer [NODEDIR]``" is used to create the Introducer node.
This node provides introduction services and nothing else. When started, this
node will produce an ``introducer.furl`` file, which should be published to all
clients.
node will produce a ``private/introducer.furl`` file, which should be
published to all clients.
"``tahoe create-key-generator [NODEDIR]``" is used to create a special
"key-generation" service, which allows a client to offload their RSA key

View File

@ -47,10 +47,11 @@ To construct an introducer, create a new base directory for it (the
name of the directory is up to you), ``cd`` into it, and run
"``tahoe create-introducer .``". Now run the introducer using
"``tahoe start .``". After it starts, it will write a file named
``introducer.furl`` in that base directory. This file contains the URL
the other nodes must use in order to connect to this introducer. (Note
that "``tahoe run .``" doesn't work for introducers, this is a known
issue: `#937 <http://allmydata.org/trac/tahoe-lafs/ticket/937>`_.)
``introducer.furl`` into the ``private/`` subdirectory of that base
directory. This file contains the URL the other nodes must use in order
to connect to this introducer. (Note that "``tahoe run .``" doesn't
work for introducers, this is a known issue: `#937
<http://allmydata.org/trac/tahoe-lafs/ticket/937>`_.)
The "``tahoe run``" command above will run the node in the foreground.
On Unix, you can run it in the background instead by using the

View File

@ -1,17 +1,21 @@
import time, os.path
import time, os.path, textwrap
from zope.interface import implements
from twisted.application import service
from foolscap.api import Referenceable
import allmydata
from allmydata import node
from allmydata.util import log, rrefutil
from allmydata.util.encodingutil import get_filesystem_encoding
from allmydata.introducer.interfaces import \
RIIntroducerPublisherAndSubscriberService_v2
from allmydata.introducer.common import convert_announcement_v1_to_v2, \
convert_announcement_v2_to_v1, unsign_from_foolscap, make_index, \
get_tubid_string_from_ann, SubscriberDescriptor, AnnouncementDescriptor
class FurlFileConflictError(Exception):
pass
class IntroducerNode(node.Node):
PORTNUMFILE = "introducer.port"
NODETYPE = "introducer"
@ -29,13 +33,27 @@ class IntroducerNode(node.Node):
introducerservice = IntroducerService(self.basedir)
self.add_service(introducerservice)
old_public_fn = os.path.join(self.basedir, "introducer.furl").encode(get_filesystem_encoding())
private_fn = os.path.join(self.basedir, "private", "introducer.furl").encode(get_filesystem_encoding())
if os.path.exists(old_public_fn):
if os.path.exists(private_fn):
msg = """This directory (%s) contains both an old public
'introducer.furl' file, and a new-style
'private/introducer.furl', so I cannot safely remove the old
one. Please make sure your desired FURL is in
private/introducer.furl, and remove the public file. If this
causes your Introducer's FURL to change, you need to inform
all grid members so they can update their tahoe.cfg.
"""
raise FurlFileConflictError(textwrap.dedent(msg))
os.rename(old_public_fn, private_fn)
d = self.when_tub_ready()
def _publish(res):
self.introducer_url = self.tub.registerReference(introducerservice,
"introducer")
self.log(" introducer is at %s" % self.introducer_url,
umid="qF2L9A")
self.write_config("introducer.furl", self.introducer_url + "\n")
furl = self.tub.registerReference(introducerservice,
furlFile=private_fn)
self.log(" introducer is at %s" % furl, umid="qF2L9A")
self.introducer_url = furl # for tests
d.addCallback(_publish)
d.addErrback(log.err, facility="tahoe.init",
level=log.BAD, umid="UaNs9A")

View File

@ -12,7 +12,7 @@ from twisted.application import service
from allmydata.interfaces import InsufficientVersionError
from allmydata.introducer.client import IntroducerClient, \
WrapV2ClientInV1Interface
from allmydata.introducer.server import IntroducerService
from allmydata.introducer.server import IntroducerService, FurlFileConflictError
from allmydata.introducer.common import get_tubid_string_from_ann, \
get_tubid_string, sign_to_foolscap, unsign_from_foolscap, \
UnknownKeyError
@ -29,15 +29,49 @@ class LoggingMultiService(service.MultiService):
log.msg(msg, **kw)
class Node(testutil.SignalMixin, unittest.TestCase):
def test_loadable(self):
basedir = "introducer.IntroducerNode.test_loadable"
def test_furl(self):
basedir = "introducer.IntroducerNode.test_furl"
os.mkdir(basedir)
q = IntroducerNode(basedir)
public_fn = os.path.join(basedir, "introducer.furl")
private_fn = os.path.join(basedir, "private", "introducer.furl")
q1 = IntroducerNode(basedir)
d = fireEventually(None)
d.addCallback(lambda res: q.startService())
d.addCallback(lambda res: q.when_tub_ready())
d.addCallback(lambda res: q.stopService())
d.addCallback(lambda res: q1.startService())
d.addCallback(lambda res: q1.when_tub_ready())
d.addCallback(lambda res: q1.stopService())
d.addCallback(flushEventualQueue)
def _check_furl(res):
# new nodes create unguessable furls in private/introducer.furl
ifurl = fileutil.read(private_fn)
self.failUnless(ifurl)
ifurl = ifurl.strip()
self.failIf(ifurl.endswith("/introducer"), ifurl)
# old nodes created guessable furls in BASEDIR/introducer.furl
guessable = ifurl[:ifurl.rfind("/")] + "/introducer"
fileutil.write(public_fn, guessable+"\n", mode="w") # text
# if we see both files, throw an error
self.failUnlessRaises(FurlFileConflictError,
IntroducerNode, basedir)
# when we see only the public one, move it to private/ and use
# the existing furl instead of creating a new one
os.unlink(private_fn)
q2 = IntroducerNode(basedir)
d2 = fireEventually(None)
d2.addCallback(lambda res: q2.startService())
d2.addCallback(lambda res: q2.when_tub_ready())
d2.addCallback(lambda res: q2.stopService())
d2.addCallback(flushEventualQueue)
def _check_furl2(res):
self.failIf(os.path.exists(public_fn))
ifurl2 = fileutil.read(private_fn)
self.failUnless(ifurl2)
self.failUnlessEqual(ifurl2.strip(), guessable)
d2.addCallback(_check_furl2)
return d2
d.addCallback(_check_furl)
return d
class ServiceMixin:

View File

@ -355,7 +355,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
c1 = os.path.join(basedir, "c1")
HOTLINE_FILE = os.path.join(c1, "suicide_prevention_hotline")
TWISTD_PID_FILE = os.path.join(c1, "twistd.pid")
INTRODUCER_FURL_FILE = os.path.join(c1, "introducer.furl")
INTRODUCER_FURL_FILE = os.path.join(c1, "private", "introducer.furl")
PORTNUM_FILE = os.path.join(c1, "introducer.port")
NODE_URL_FILE = os.path.join(c1, "node.url")
CONFIG_FILE = os.path.join(c1, "tahoe.cfg")
@ -406,8 +406,8 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
d.addCallback(lambda res: self.poll(_node_has_started))
def _started(res):
# read the introducer.furl and introducer.port files so we can check that their
# contents don't change on restart
# read the introducer.furl and introducer.port files so we can
# check that their contents don't change on restart
self.furl = fileutil.read(INTRODUCER_FURL_FILE)
self.failUnless(os.path.exists(PORTNUM_FILE))
self.portnum = fileutil.read(PORTNUM_FILE)