[tor-commits] [bridgedb/develop] Don't burn active probing-resistant PTs.
phw at torproject.org
phw at torproject.org
Tue Jun 4 16:36:31 UTC 2019
commit efff3eb11dc62044ab2d64cf1a078ec913f7ae7c
Author: Philipp Winter <phw at nymity.ch>
Date: Wed Apr 10 17:31:43 2019 -0700
Don't burn active probing-resistant PTs.
The GFW used to block bridges by IP address:port but a while ago, it
started to block bridges by IP address instead. This means that if a
bridge runs obfs4 (which is active probing-resistant) and obfs3 (which
is not active probing-resistant), and BridgeDB happens to hand out the
bridge's obfs3 line, the GFW would manage to probe the bridge, and block
the entire IP address, *including* obfs4.
In this patch, we deal with the GFW's update by not handing out a
bridge's probe-able protocols if it also runs an active
probing-resistant protocol such as obfs4 or scramblesuit.
The patch adds a new configuration option, PROBING_RESISTANT_TRANSPORTS,
which must contain a list of active probing-resistant protocols.
This fixes bug 28655: <https://bugs.torproject.org/28655>
---
.test.requirements.txt | 2 +-
.travis.requirements.txt | 2 +-
CHANGELOG | 8 ++++++
bridgedb.conf | 8 ++++++
bridgedb/bridgerequest.py | 7 +++++
bridgedb/bridges.py | 21 +++++++++++++++
bridgedb/filters.py | 34 ++++++++++++++++++++++++
bridgedb/main.py | 8 ++++++
bridgedb/runner.py | 2 +-
bridgedb/test/test_distributors_moat_request.py | 3 ++-
bridgedb/test/test_filters.py | 35 +++++++++++++++++++++++++
bridgedb/test/test_https_distributor.py | 31 ++++++++++++++++++++++
bridgedb/test/test_main.py | 1 +
bridgedb/test/util.py | 17 +++++++++++-
doc/HACKING.md | 2 +-
scripts/setup-tests | 2 +-
16 files changed, 176 insertions(+), 7 deletions(-)
diff --git a/.test.requirements.txt b/.test.requirements.txt
index 6a85c00..c84fb58 100644
--- a/.test.requirements.txt
+++ b/.test.requirements.txt
@@ -6,7 +6,7 @@
# $ make coverage
#
coverage==4.2
-git+https://git.torproject.org/user/isis/leekspin.git@bad0bed11a9018f65555b3c6998b26e2cb06f5b5#egg=leekspin-2.2.0.dev1-py2.7
+git+https://git.torproject.org/user/phw/leekspin.git@d34c804cd0f01af5206833e62c0dedec8565b235#egg=leekspin
mechanize==0.2.5
pep8==1.5.7
# pylint must be pinned until pylint bug #203 is fixed. See
diff --git a/.travis.requirements.txt b/.travis.requirements.txt
index e05e643..2d56b79 100644
--- a/.travis.requirements.txt
+++ b/.travis.requirements.txt
@@ -15,7 +15,7 @@
#------------------------------------------------------------------------------
coverage==4.2
coveralls==1.2.0
-git+https://git.torproject.org/user/isis/leekspin.git@bad0bed11a9018f65555b3c6998b26e2cb06f5b5#egg=leekspin-2.2.0.dev1-py2.7
+git+https://git.torproject.org/user/phw/leekspin.git@d34c804cd0f01af5206833e62c0dedec8565b235#egg=leekspin
mechanize==0.2.5
sure==1.2.2
Babel==0.9.6
diff --git a/CHANGELOG b/CHANGELOG
index 9ffeac1..f45f56f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,11 @@
+Changes in version 0.6.X - YYYY-MM-DD
+
+ * FIXES #28655 https://bugs.torproject.org/28655
+ When a bridge supports an active probing-resistant transport, it should
+ not give out flavors that are vulnerable to active probing. For
+ example, if a bridge supports obfs4 and obfs3, it should only give out
+ obfs4.
+
Changes in version 0.6.3 - 2018-01-23
* FIXES #24432 https://bugs.torproject.org/24432
diff --git a/bridgedb.conf b/bridgedb.conf
index 7700917..73b65b8 100644
--- a/bridgedb.conf
+++ b/bridgedb.conf
@@ -282,6 +282,14 @@ SUPPORTED_TRANSPORTS = {
'fte': True,
}
+# PROBING_RESISTANT_TRANSPORTS is a list of transports that are resistant to
+# active probing attacks as pioneered by China's GFW. If a bridge supports any
+# of the following transports, only these transports are distributed, and no
+# others. Here's why: If we have a bridge that supports both obfs3 and obfs4,
+# we don't want to hand out its obfs3 line to users because this may get the
+# bridge probed and its IP address blocked, which also blocks the obfs4 PT.
+PROBING_RESISTANT_TRANSPORTS = ['scramblesuit', 'obfs4']
+
# DEFAULT_TRANSPORT is a string. It should be the PT methodname of the
# transport which is selected by default (e.g. in the webserver dropdown
# menu).
diff --git a/bridgedb/bridgerequest.py b/bridgedb/bridgerequest.py
index d5af05f..bf64069 100644
--- a/bridgedb/bridgerequest.py
+++ b/bridgedb/bridgerequest.py
@@ -28,6 +28,7 @@ from bridgedb.crypto import getHMACFunc
from bridgedb.filters import byIPv
from bridgedb.filters import byNotBlockedIn
from bridgedb.filters import byTransport
+from bridgedb.filters import byProbingResistance
class IRequestBridges(Interface):
@@ -238,6 +239,12 @@ class BridgeRequestBase(object):
msg = ("Adding a filter to %s for %s for IPv%d"
% (self.__class__.__name__, self.client, self.ipVersion))
+ # If this bridge runs any active probing-resistant PTs, we should
+ # *only* hand out its active probing-resistant PTs. Otherwise, a
+ # non-resistant PT would get this bridge scanned and blocked:
+ # <https://bugs.torproject.org/28655>
+ self.addFilter(byProbingResistance(pt, self.ipVersion))
+
if self.notBlockedIn:
for country in self.notBlockedIn:
logging.info("%s %s bridges not blocked in %s..." %
diff --git a/bridgedb/bridges.py b/bridgedb/bridges.py
index 8b4bc1b..cf90b3b 100644
--- a/bridgedb/bridges.py
+++ b/bridgedb/bridges.py
@@ -365,6 +365,9 @@ class PluggableTransport(BridgeAddressBase):
{'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'}
"""
+ # A list of PT names that are resistant to active probing attacks.
+ probing_resistant_transports = []
+
def __init__(self, fingerprint=None, methodname=None,
address=None, port=None, arguments=None):
"""Create a ``PluggableTransport`` describing a PT running on a bridge.
@@ -529,6 +532,17 @@ class PluggableTransport(BridgeAddressBase):
except (AttributeError, TypeError):
raise TypeError("methodname must be a str or unicode")
+ def isProbingResistant(self):
+ """Reveal if this pluggable transport is active probing-resistant.
+
+ :rtype: bool
+ :returns: ``True`` if this pluggable transport is resistant to active
+ probing attacks, ``False`` otherwise.
+ """
+
+ return self.methodname in PluggableTransport.probing_resistant_transports
+
+
def getTransportLine(self, includeFingerprint=True, bridgePrefix=False):
"""Get a Bridge Line for this :class:`PluggableTransport`.
@@ -1031,6 +1045,13 @@ class Bridge(BridgeBackwardsCompatibility):
return prefix + fingerprint + separator + nickname
+ def hasProbingResistantPT(self):
+ # We want to know if this bridge runs any active probing-resistant PTs
+ # because if so, we should *only* hand out its active probing-resistant
+ # PTs. Otherwise, a non-resistant PT would get this bridge scanned and
+ # blocked: <https://bugs.torproject.org/28655>
+ return any([t.isProbingResistant() for t in self.transports])
+
def _checkServerDescriptor(self, descriptor):
# If we're parsing the server-descriptor, require a networkstatus
# document:
diff --git a/bridgedb/filters.py b/bridgedb/filters.py
index cac587b..f268c83 100644
--- a/bridgedb/filters.py
+++ b/bridgedb/filters.py
@@ -127,6 +127,40 @@ def byIPv(ipVersion=None):
byIPv4 = byIPv(4)
byIPv6 = byIPv(6)
+def byProbingResistance(methodname=None, ipVersion=None):
+ """Return ``True`` if the bridge can be given out safely without
+ jeopardizing a probing-resistant transport that runs on the same bridge.
+
+ :param str methodname: A Pluggable Transport
+ :data:`~bridgedb.bridges.PluggableTransport.methodname`.
+ :param int ipVersion: Either ``4`` or ``6``. The IP version that the
+ ``Bridge``'s ``PluggableTransport``
+ :attr:`address <bridgedb.bridges.PluggableTransport.address>` should
+ have.
+ :rtype: callable
+ :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
+ """
+
+ if ipVersion not in (4, 6):
+ ipVersion = 4
+
+ methodname = "vanilla" if methodname is None else methodname.lower()
+ name = "by-probing-resistance-%s ipv%d" % (methodname, ipVersion)
+
+ try:
+ return _cache[name]
+ except KeyError:
+ def _byProbingResistance(bridge):
+ if bridge.hasProbingResistantPT():
+ return methodname in ('scramblesuit', 'obfs4')
+ return True
+
+ setattr(_byProbingResistance, "description", "probing_resistance")
+ _byProbingResistance.__name__ = "byProbingResistance(%s,%s)" % (methodname, ipVersion)
+ _byProbingResistance.name = name
+ _cache[name] = _byProbingResistance
+ return _byProbingResistance
+
def byTransport(methodname=None, ipVersion=None):
"""Returns a filter function for a :class:`~bridgedb.bridges.Bridge`.
diff --git a/bridgedb/main.py b/bridgedb/main.py
index 586ed83..71ae48a 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -331,6 +331,14 @@ def run(options, reactor=reactor):
pidfile.write("%s\n" % os.getpid())
pidfile.flush()
+ # Let our pluggable transport class know what transports are resistant to
+ # active probing. We need to know because we shouldn't hand out a
+ # probing-vulnerable transport on a bridge that supports a
+ # probing-resistant transport. See
+ # <https://bugs.torproject.org/28655> for details.
+ from bridgedb.bridges import PluggableTransport
+ PluggableTransport.probing_resistant_transports = config.PROBING_RESISTANT_TRANSPORTS
+
from bridgedb import persistent
state = persistent.State(config=config)
diff --git a/bridgedb/runner.py b/bridgedb/runner.py
index 35865e5..b1a21d2 100644
--- a/bridgedb/runner.py
+++ b/bridgedb/runner.py
@@ -90,7 +90,7 @@ def generateDescriptors(count=None, rundir=None):
descriptors, used to calculate the OR fingerprints, and sign the
descriptors, among other things.
- .. _Leekspin: https://gitweb.torproject.org/user/isis/leekspin.git
+ .. _Leekspin: https://gitweb.torproject.org/user/phw/leekspin.git
:param integer count: Number of mocked bridges to generate descriptor
for. (default: 3)
diff --git a/bridgedb/test/test_distributors_moat_request.py b/bridgedb/test/test_distributors_moat_request.py
index ed7f493..3c6ff51 100644
--- a/bridgedb/test/test_distributors_moat_request.py
+++ b/bridgedb/test/test_distributors_moat_request.py
@@ -32,7 +32,8 @@ class MoatBridgeRequest(unittest.TestCase):
self.assertItemsEqual(['byTransportNotBlockedIn(None,us,4)',
'byTransportNotBlockedIn(None,ir,4)',
- 'byTransportNotBlockedIn(None,sy,4)'],
+ 'byTransportNotBlockedIn(None,sy,4)',
+ 'byProbingResistance(vanilla,4)'],
[x.__name__ for x in self.bridgeRequest.filters])
def test_withoutBlockInCountry_not_a_valid_country_code(self):
diff --git a/bridgedb/test/test_filters.py b/bridgedb/test/test_filters.py
index 3b9efcb..f43124c 100644
--- a/bridgedb/test/test_filters.py
+++ b/bridgedb/test/test_filters.py
@@ -37,6 +37,8 @@ class FiltersTests(unittest.TestCase):
self.hmac = getHMACFunc('plasma')
+ PluggableTransport.probing_resistant_transports = ['scramblesuit', 'obfs4']
+
def addIPv4VoltronPT(self):
pt = PluggableTransport('a' * 40, 'voltron', '1.1.1.1', 1111, {})
self.bridge.transports.append(pt)
@@ -301,6 +303,39 @@ class FiltersTests(unittest.TestCase):
filtre = filters.byNotBlockedIn('cn', methodname='obfs3')
self.assertFalse(filtre(self.bridge))
+ def test_byProbingResistance(self):
+ """A bridge with probing-resistant transports must not hand out its
+ non-probing-resistant transports.
+ """
+
+ scramblesuitArgs = {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'}
+ obfs4Args = {'cert': 'UXj/cWm0qolGrROYpkl0UyD/7PEhzkoZkZXrOpjRKwImvkpQZwmF0nSzBXfyfbT9afBZEw',
+ 'iat-mode': '1'}
+
+ obfs2 = PluggableTransport('a' * 40, 'obfs2', '1.1.1.1', 1111, {})
+ scramblesuit = PluggableTransport('a' * 40, 'scramblesuit', '1.1.1.1',
+ 1111, scramblesuitArgs)
+ obfs4 = PluggableTransport('a' * 40, 'obfs4', '1.1.1.1', 111,
+ obfs4Args)
+ self.bridge.transports.append(obfs2)
+ self.bridge.transports.append(scramblesuit)
+ self.bridge.transports.append(obfs4)
+
+ filtre = filters.byProbingResistance(methodname='obfs2', ipVersion=4)
+ self.assertFalse(filtre(self.bridge))
+
+ filtre = filters.byProbingResistance(methodname="vanilla", ipVersion=4)
+ self.assertFalse(filtre(self.bridge))
+
+ filtre = filters.byProbingResistance(ipVersion=4)
+ self.assertFalse(filtre(self.bridge))
+
+ filtre = filters.byProbingResistance(methodname='scramblesuit', ipVersion=4)
+ self.assertTrue(filtre(self.bridge))
+
+ filtre = filters.byProbingResistance(methodname='obfs4', ipVersion=4)
+ self.assertTrue(filtre(self.bridge))
+
def test_byNotBlockedIn_ipv5(self):
"""Calling byNotBlockedIn([…], ipVersion=5) should default to IPv4."""
self.bridge.setBlockedIn('ru')
diff --git a/bridgedb/test/test_https_distributor.py b/bridgedb/test/test_https_distributor.py
index 791a940..c25c598 100644
--- a/bridgedb/test/test_https_distributor.py
+++ b/bridgedb/test/test_https_distributor.py
@@ -19,6 +19,7 @@ import random
from twisted.trial import unittest
+from bridgedb.bridges import PluggableTransport
from bridgedb.Bridges import BridgeRing
from bridgedb.Bridges import BridgeRingParameters
from bridgedb.filters import byIPv4
@@ -43,6 +44,7 @@ class HTTPSDistributorTests(unittest.TestCase):
def setUp(self):
self.key = 'aQpeOFIj8q20s98awfoiq23rpOIjFaqpEWFoij1X'
self.bridges = BRIDGES
+ PluggableTransport.probing_resistant_transports = ['scramblesuit', 'obfs4']
def tearDown(self):
"""Reset all bridge blocks in between test method runs."""
@@ -181,6 +183,35 @@ class HTTPSDistributorTests(unittest.TestCase):
b = dist.getBridges(clientRequest2, 1)
self.assertEqual(len(b), 3)
+ def test_HTTPSDistributor_getBridges_probing_vulnerable(self):
+ dist = distributor.HTTPSDistributor(1, self.key)
+ bridges = self.bridges[:]
+ [dist.insert(bridge) for bridge in bridges]
+
+ def requestTransports(bridges, transport, vulnerable):
+ for _ in range(len(bridges)):
+ request = HTTPSBridgeRequest(addClientCountryCode=False)
+ request.client = randomValidIPv4String()
+ request.isValid(True)
+ if transport is not None:
+ request.transports.append(transport)
+ request.generateFilters()
+
+ obtained_bridges = dist.getBridges(request, 1)
+ for bridge in obtained_bridges:
+ if vulnerable:
+ self.assertFalse(bridge.hasProbingResistantPT())
+ for t in bridge.transports:
+ self.assertTrue(t.methodname != 'obfs4' and
+ t.methodname != 'scramblesuit')
+ else:
+ self.assertTrue(bridge.hasProbingResistantPT())
+
+ requestTransports(bridges, None, True)
+ requestTransports(bridges, 'obfs2', True)
+ requestTransports(bridges, 'obfs3', True)
+ requestTransports(bridges, 'obfs4', False)
+
def test_HTTPSDistributor_getBridges_with_some_blocked_bridges(self):
dist = distributor.HTTPSDistributor(1, self.key)
bridges = self.bridges[:]
diff --git a/bridgedb/test/test_main.py b/bridgedb/test/test_main.py
index b6eae2b..52d98a2 100644
--- a/bridgedb/test/test_main.py
+++ b/bridgedb/test/test_main.py
@@ -403,6 +403,7 @@ BRIDGE_PURPOSE = "bridge"
TASKS = {'GET_TOR_EXIT_LIST': 3 * 60 * 60,}
SERVER_PUBLIC_FQDN = 'bridges.torproject.org'
SERVER_PUBLIC_EXTERNAL_IP = '38.229.72.19'
+PROBING_RESISTANT_TRANSPORTS = ['scramblesuit', 'obfs4']
HTTPS_DIST = True
HTTPS_BIND_IP = None
HTTPS_PORT = None
diff --git a/bridgedb/test/util.py b/bridgedb/test/util.py
index 042c92d..9fc16b8 100644
--- a/bridgedb/test/util.py
+++ b/bridgedb/test/util.py
@@ -196,13 +196,28 @@ def generateFakeBridges(n=500):
addrs = [(randomValidIPv6(), randomHighPort(), 6)]
fpr = "".join(random.choice('abcdef0123456789') for _ in xrange(40))
- # We only support the ones without PT args, because they're easier to fake.
supported = ["obfs2", "obfs3", "fte"]
transports = []
for j, method in zip(range(1, len(supported) + 1), supported):
pt = PluggableTransport(fpr, method, addr, port - j, {})
transports.append(pt)
+ # Every tenth bridge supports obfs4.
+ if i % 10 == 0:
+ obfs4Args = {'iat-mode': '1',
+ 'node-id': '2a79f14120945873482b7823caabe2fcde848722',
+ 'public-key': '0a5b046d07f6f971b7776de682f57c5b9cdc8fa060db7ef59de82e721c8098f4'}
+ pt = PluggableTransport(fpr, "obfs4", addr, port - j,
+ obfs4Args)
+ transports.append(pt)
+
+ # Every fifteenth bridge supports scramblesuit.
+ if i % 15 == 0:
+ scramblesuitArgs = {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'}
+ pt = PluggableTransport(fpr, "scramblesuit", addr, port - j,
+ scramblesuitArgs)
+ transports.append(pt)
+
bridge = Bridge(nick, addr, port, fpr)
bridge.flags.update("Running Stable")
bridge.transports = transports
diff --git a/doc/HACKING.md b/doc/HACKING.md
index 22f9b2b..43e7fa1 100644
--- a/doc/HACKING.md
+++ b/doc/HACKING.md
@@ -13,7 +13,7 @@ with password ```writecode```.
Developers wishing to test BridgeDB will need to generate mock bridge
descriptors. This is accomplished through the [leekspin
-script](https://gitweb.torproject.org/user/isis/leekspin.git). To generate 20
+script](https://gitweb.torproject.org/user/phw/leekspin.git). To generate 20
bridge descriptors, change to the bridgedb running directory and do:
$ leekspin -n 20
diff --git a/scripts/setup-tests b/scripts/setup-tests
index 18298f4..ccfd6cd 100755
--- a/scripts/setup-tests
+++ b/scripts/setup-tests
@@ -29,7 +29,7 @@ sed -r -i -e "s/(SERVER_PUBLIC_FQDN = )(.*)/\1'127.0.0.1:6788'/" run/bridgedb.co
sed -r -i -e "s/(MOAT_HTTP_IP = )(None)/\1'127.0.0.1'/" run/bridgedb.conf
sed -r -i -e "s/(MOAT_HTTP_PORT = )(None)/\16790/" run/bridgedb.conf
# Create descriptors
-leekspin -n 100
+leekspin -n 100 -xp 50
cp -t run/from-authority networkstatus-bridges cached-extrainfo* bridge-descriptors
cp -t run/from-bifroest networkstatus-bridges cached-extrainfo* bridge-descriptors
# Create TLS certificates
More information about the tor-commits
mailing list