Merge PR437: add tub.port=listen:i2p

This commit is contained in:
Brian Warner 2017-11-03 00:46:28 -07:00
commit e89c99c578
9 changed files with 145 additions and 29 deletions

View File

@ -150,6 +150,14 @@ set the ``tub.location`` option described below.
Lists of endpoint descriptor strings like the following ``tcp:12345,tcp6:12345``
are known to not work because an ``Address already in use.`` error.
If any descriptor begins with ``listen:tor``, or ``listen:i2p``, the
corresponding tor/i2p Provider object will construct additional endpoints
for the Tub to listen on. This allows the ``[tor]`` or ``[i2p]`` sections
in ``tahoe.cfg`` to customize the endpoint; e.g. to add I2CP control
options. If you use ``listen:i2p``, you should not also have an
``i2p:..`` endpoint in ``tub.port``, as that would result in multiple
I2P-based listeners.
If ``tub.port`` is the string ``disabled``, the node will not listen at
all, and thus cannot accept connections from other nodes. If ``[storage]
enabled = true``, or ``[helper] enabled = true``, or the node is an

View File

@ -400,7 +400,18 @@ class Node(service.MultiService):
for port in tubport.split(","):
if port in ("0", "tcp:0"):
raise ValueError("tub.port cannot be 0: you must choose")
self.tub.listenOn(port)
if port == "listen:i2p":
# the I2P provider will read its section of tahoe.cfg and
# return either a fully-formed Endpoint, or a descriptor
# that will create one, so we don't have to stuff all the
# options into the tub.port string (which would need a lot
# of escaping)
port_or_endpoint = self._i2p_provider.get_listener()
elif port == "listen:tor":
port_or_endpoint = self._tor_provider.get_listener()
else:
port_or_endpoint = port
self.tub.listenOn(port_or_endpoint)
self.tub.setLocation(location)
self._tub_is_listening = True
self.log("Tub location set to %s" % (location,))

View File

@ -251,12 +251,12 @@ def write_node_config(c, config):
else:
if "tor" in listeners:
(tor_config, tor_port, tor_location) = \
yield tor_provider.create_onion(reactor, config)
yield tor_provider.create_config(reactor, config)
tub_ports.append(tor_port)
tub_locations.append(tor_location)
if "i2p" in listeners:
(i2p_config, i2p_port, i2p_location) = \
yield i2p_provider.create_dest(reactor, config)
yield i2p_provider.create_config(reactor, config)
tub_ports.append(i2p_port)
tub_locations.append(i2p_location)
if "tcp" in listeners:

View File

@ -261,7 +261,7 @@ class Config(unittest.TestCase):
def test_node_slow_tor(self):
basedir = self.mktemp()
d = defer.Deferred()
with mock.patch("allmydata.util.tor_provider.create_onion",
with mock.patch("allmydata.util.tor_provider.create_config",
return_value=d):
d2 = run_cli("create-node", "--listen=tor", basedir)
d.callback(({}, "port", "location"))
@ -274,7 +274,7 @@ class Config(unittest.TestCase):
def test_node_slow_i2p(self):
basedir = self.mktemp()
d = defer.Deferred()
with mock.patch("allmydata.util.i2p_provider.create_dest",
with mock.patch("allmydata.util.i2p_provider.create_config",
return_value=d):
d2 = run_cli("create-node", "--listen=i2p", basedir)
d.callback(({}, "port", "location"))
@ -325,9 +325,9 @@ class Tor(unittest.TestCase):
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
config_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_config",
return_value=config_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", basedir))
self.assertEqual(len(co.mock_calls), 1)
@ -345,9 +345,9 @@ class Tor(unittest.TestCase):
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
config_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_config",
return_value=config_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", "--tor-launch",
basedir))
@ -361,9 +361,9 @@ class Tor(unittest.TestCase):
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
config_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_config",
return_value=config_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", "--tor-control-port=mno",
basedir))
@ -400,7 +400,7 @@ class I2P(unittest.TestCase):
i2p_port = "ghi"
i2p_location = "jkl"
dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) )
with mock.patch("allmydata.util.i2p_provider.create_dest",
with mock.patch("allmydata.util.i2p_provider.create_config",
return_value=dest_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=i2p", basedir))
@ -427,7 +427,7 @@ class I2P(unittest.TestCase):
i2p_port = "ghi"
i2p_location = "jkl"
dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) )
with mock.patch("allmydata.util.i2p_provider.create_dest",
with mock.patch("allmydata.util.i2p_provider.create_config",
return_value=dest_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=i2p", "--i2p-sam-port=mno",

View File

@ -124,7 +124,7 @@ class CreateDest(unittest.TestCase):
def test_no_txi2p(self):
with mock.patch("allmydata.util.i2p_provider._import_txi2p",
return_value=None):
d = i2p_provider.create_dest("reactor", "cli_config")
d = i2p_provider.create_config("reactor", "cli_config")
f = self.failureResultOf(d)
self.assertIsInstance(f.value, ValueError)
self.assertEqual(str(f.value),
@ -164,7 +164,7 @@ class CreateDest(unittest.TestCase):
connect_to_i2p):
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
d = i2p_provider.create_dest(reactor, cli_config)
d = i2p_provider.create_config(reactor, cli_config)
tahoe_config_i2p, i2p_port, i2p_location = self.successResultOf(d)
connect_to_i2p.assert_called_with(reactor, cli_config, txi2p)
@ -178,7 +178,7 @@ class CreateDest(unittest.TestCase):
"i2p_dest.privkey"),
}
self.assertEqual(tahoe_config_i2p, expected)
self.assertEqual(i2p_port, "i2p:%s:3457:api=SAM:apiEndpoint=goodport" % privkeyfile)
self.assertEqual(i2p_port, "listen:i2p")
self.assertEqual(i2p_location, "i2p:FOOBAR.b32.i2p:3457")
_None = object()
@ -294,6 +294,33 @@ class Provider(unittest.TestCase):
self.assertIs(h, handler)
i2p.default.assert_called_with(reactor, keyfile=None)
class ProviderListener(unittest.TestCase):
def test_listener(self):
"""Does the I2P Provider object's get_listener() method correctly
convert the [i2p] section of tahoe.cfg into an
endpoint/descriptor?
"""
i2p = mock.Mock()
handler = object()
i2p.local_i2p = mock.Mock(return_value=handler)
reactor = object()
privkeyfile = os.path.join("private", "i2p_dest.privkey")
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{
"i2p.configdir": "configdir",
"sam.port": "good:port",
"dest": "true",
"dest.port": "3457",
"dest.private_key_file": privkeyfile,
}),
reactor)
endpoint_or_description = p.get_listener()
self.assertEqual(endpoint_or_description,
"i2p:%s:3457:api=SAM:apiEndpoint=good\\:port" % privkeyfile)
class Provider_CheckI2PConfig(unittest.TestCase):
def test_default(self):
# default config doesn't start an I2P service, so it should be

View File

@ -349,7 +349,7 @@ class FakeTub:
def setLocation(self, location): pass
def setServiceParent(self, parent): pass
class MultiplePorts(unittest.TestCase):
class Listeners(unittest.TestCase):
def test_multiple_ports(self):
n = EmptyNode()
n.basedir = self.mktemp()
@ -381,6 +381,36 @@ class MultiplePorts(unittest.TestCase):
["tcp:%d:interface=127.0.0.1" % port1,
"tcp:%d:interface=127.0.0.1" % port2])
def test_tor_i2p_listeners(self):
n = EmptyNode()
n.basedir = self.mktemp()
n.config_fname = os.path.join(n.basedir, "tahoe.cfg")
os.mkdir(n.basedir)
os.mkdir(os.path.join(n.basedir, "private"))
with open(n.config_fname, "w") as f:
f.write(BASE_CONFIG)
f.write("tub.port = listen:i2p,listen:tor\n")
f.write("tub.location = tcp:example.org:1234\n")
# we're doing a lot of calling-into-setup-methods here, it might be
# better to just create a real Node instance, I'm not sure.
n.read_config()
n.check_privacy()
n.services = []
i2p_ep = object()
tor_ep = object()
n._i2p_provider = mock.Mock()
n._i2p_provider.get_listener = mock.Mock(return_value=i2p_ep)
n._tor_provider = mock.Mock()
n._tor_provider.get_listener = mock.Mock(return_value=tor_ep)
n.init_connections()
n.set_tub_options()
t = FakeTub()
with mock.patch("allmydata.node.Tub", return_value=t):
n.create_main_tub()
self.assertEqual(n._i2p_provider.get_listener.mock_calls, [mock.call()])
self.assertEqual(n._tor_provider.get_listener.mock_calls, [mock.call()])
self.assertEqual(t.listening_ports, [i2p_ep, tor_ep])
class ClientNotListening(unittest.TestCase):
def test_disabled(self):
basedir = "test_node/test_disabled"

View File

@ -152,7 +152,7 @@ class CreateOnion(unittest.TestCase):
def test_no_txtorcon(self):
with mock.patch("allmydata.util.tor_provider._import_txtorcon",
return_value=None):
d = tor_provider.create_onion("reactor", "cli_config")
d = tor_provider.create_config("reactor", "cli_config")
f = self.failureResultOf(d)
self.assertIsInstance(f.value, ValueError)
self.assertEqual(str(f.value),
@ -184,7 +184,7 @@ class CreateOnion(unittest.TestCase):
launch_tor):
with mock.patch("allmydata.util.tor_provider.allocate_tcp_port",
return_value=999999):
d = tor_provider.create_onion(reactor, cli_config)
d = tor_provider.create_config(reactor, cli_config)
tahoe_config_tor, tor_port, tor_location = self.successResultOf(d)
launch_tor.assert_called_with(reactor, executable,
@ -238,7 +238,7 @@ class CreateOnion(unittest.TestCase):
connect_to_tor):
with mock.patch("allmydata.util.tor_provider.allocate_tcp_port",
return_value=999999):
d = tor_provider.create_onion(reactor, cli_config)
d = tor_provider.create_config(reactor, cli_config)
tahoe_config_tor, tor_port, tor_location = self.successResultOf(d)
connect_to_tor.assert_called_with(reactor, cli_config, txtorcon)
@ -393,6 +393,29 @@ class Provider(unittest.TestCase):
self.assertIs(h, handler)
tor.default_socks.assert_called_with()
class ProviderListener(unittest.TestCase):
def test_listener(self):
"""Does the Tor Provider object's get_listener() method correctly
convert the [tor] section of tahoe.cfg into an
endpoint/descriptor?
"""
tor = mock.Mock()
handler = object()
tor.socks_endpoint = mock.Mock(return_value=handler)
reactor = object()
with mock_tor(tor):
p = tor_provider.Provider("basedir",
FakeConfig(**{"onion.local_port": "321"}),
reactor)
fake_ep = object()
with mock.patch("allmydata.util.tor_provider.TCP4ServerEndpoint",
return_value=fake_ep) as e:
endpoint_or_description = p.get_listener()
self.assertIs(endpoint_or_description, fake_ep)
self.assertEqual(e.mock_calls, [mock.call(reactor, 321,
interface="127.0.0.1")])
class Provider_CheckOnionConfig(unittest.TestCase):
def test_default(self):
# default config doesn't start an onion service, so it should be

View File

@ -65,7 +65,7 @@ def _connect_to_i2p(reactor, cli_config, txi2p):
raise ValueError("unable to reach any default I2P SAM port")
@inlineCallbacks
def create_dest(reactor, cli_config):
def create_config(reactor, cli_config):
txi2p = _import_txi2p()
if not txi2p:
raise ValueError("Cannot create I2P Destination without txi2p. "
@ -88,9 +88,7 @@ def create_dest(reactor, cli_config):
print("allocating .i2p address...", file=stdout)
dest = yield txi2p.generateDestination(reactor, privkeyfile, 'SAM', sam_endpoint)
print(".i2p address allocated", file=stdout)
escaped_sam_port = sam_port.replace(':', '\:')
i2p_port = "i2p:%s:%d:api=SAM:apiEndpoint=%s" % \
(privkeyfile, external_port, escaped_sam_port)
i2p_port = "listen:i2p" # means "see [i2p]", calls Provider.get_listener()
i2p_location = "i2p:%s:%d" % (dest.host, external_port)
# in addition to the "how to launch/connect-to i2p" keys above, we also
@ -137,6 +135,20 @@ class Provider(service.MultiService):
def _get_i2p_config(self, *args, **kwargs):
return self._node_for_config.get_config("i2p", *args, **kwargs)
def get_listener(self):
# this is relative to BASEDIR, and our cwd should be BASEDIR
privkeyfile = self._get_i2p_config("dest.private_key_file")
external_port = self._get_i2p_config("dest.port")
sam_port = self._get_i2p_config("sam.port")
escaped_sam_port = sam_port.replace(':', '\:')
# for now, this returns a string, which then gets passed to
# endpoints.serverFromString . But it can also return an Endpoint
# directly, which means we don't need to encode all these options
# into a string
i2p_port = "i2p:%s:%s:api=SAM:apiEndpoint=%s" % \
(privkeyfile, external_port, escaped_sam_port)
return i2p_port
def get_i2p_handler(self):
enabled = self._get_i2p_config("enabled", True, boolean=True)
if not enabled:

View File

@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function, with_statement
import os
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.endpoints import clientFromString
from twisted.internet.endpoints import clientFromString, TCP4ServerEndpoint
from twisted.internet.error import ConnectionRefusedError, ConnectError
from twisted.application import service
@ -124,7 +124,7 @@ def _connect_to_tor(reactor, cli_config, txtorcon):
raise ValueError("unable to reach any default Tor control port")
@inlineCallbacks
def create_onion(reactor, cli_config):
def create_config(reactor, cli_config):
txtorcon = _import_txtorcon()
if not txtorcon:
raise ValueError("Cannot create onion without txtorcon. "
@ -216,6 +216,11 @@ class Provider(service.MultiService):
def _get_tor_config(self, *args, **kwargs):
return self._node_for_config.get_config("tor", *args, **kwargs)
def get_listener(self):
local_port = int(self._get_tor_config("onion.local_port"))
ep = TCP4ServerEndpoint(self._reactor, local_port, interface="127.0.0.1")
return ep
def get_tor_handler(self):
enabled = self._get_tor_config("enabled", True, boolean=True)
if not enabled: