This is an automated email from the git hooks/post-receive script.
meskio pushed a commit to branch main in repository bridgedb.
The following commit(s) were added to refs/heads/main by this push: new 3ab5afe Respond with dummy bridges if the request is not comming from the shim 3ab5afe is described below
commit 3ab5afee2bc37e87499b5f704d715a0b7b38783f Author: meskio meskio@torproject.org AuthorDate: Wed Jul 13 15:57:57 2022 +0200
Respond with dummy bridges if the request is not comming from the shim
Check if the shim-token header is present and valid on moat requests, to make sure the request is comming from the shim. If not respond with dummy bridges comming from a configured file. --- bridgedb.conf | 8 +++++ bridgedb/bridges.py | 37 +++++++++++++++++++++ bridgedb/configure.py | 9 ++++++ bridgedb/distributors/moat/distributor.py | 53 +++++++++++++++++++++++++++++++ bridgedb/distributors/moat/server.py | 37 ++++++++++++++++++--- bridgedb/main.py | 2 ++ bridgedb/test/moat_helpers.py | 3 +- bridgedb/test/test_bridges.py | 22 +++++++++++++ bridgedb/test/test_main.py | 1 + 9 files changed, 167 insertions(+), 5 deletions(-)
diff --git a/bridgedb.conf b/bridgedb.conf index fc56db8..829ff76 100644 --- a/bridgedb.conf +++ b/bridgedb.conf @@ -424,6 +424,14 @@ MOAT_ROTATION_PERIOD = "3 hours" MOAT_GIMP_CAPTCHA_HMAC_KEYFILE = 'moat_captcha_hmac_key' MOAT_GIMP_CAPTCHA_RSA_KEYFILE = 'moat_captcha_rsa_key'
+# The file containing the token that should be included on the header +# 'shim-token' on each request or dummy bridges will be provided. +MOAT_SHIM_TOKEN_FILE = "" + +# The file containing bridges to be distributed for requests without a valid +# shim-token. The file should contain one bridge per line. +MOAT_DUMMY_BRIDGES_FILE = "" + #------------------------------- # HTTP(S) Distribution Options \ #------------------------------------------------------------------------------ diff --git a/bridgedb/bridges.py b/bridgedb/bridges.py index 6cd3057..5f95040 100644 --- a/bridgedb/bridges.py +++ b/bridgedb/bridges.py @@ -1583,6 +1583,43 @@ class Bridge(BridgeBackwardsCompatibility): for country in resource["blocked_in"]: self.setBlockedIn(country)
+ def updateFromBridgeLine(self, bridge_line): + """Update this bridge's attributes from a bridge line + + :type bridge_line: str + """ + parts = bridge_line.split(" ") + if len(parts) < 2: + raise MalformedBridgeInfo("Not a valid bridge line: %s" % (bridge_line,)) + elif len(parts) == 2: + tpe = "" + addrstr = parts[0] + self.fingerprint = parts[1] + arguments = {} + else: + tpe = parts[0] + self.fingerprint = parts[2] + addrstr = parts[1] + arguments = dict([arg.split("=") for arg in parts[3:]]) + + addr = addrstr.split(":") + self.address = addr[0] + if not self.address or len(addr) != 2: + raise MalformedBridgeInfo("Invalid address for a bridge (%s): %s" % (self.fingerprint, addrstr)) + port = int(addr[1]) + + if not tpe: + self.orPort = port + else: + transport = PluggableTransport( + fingerprint=self.fingerprint, + methodname=tpe, + address=addr[0], + port=port, + arguments=arguments + ) + self.transports = [transport] + def updateFromNetworkStatus(self, descriptor, ignoreNetworkstatus=False): """Update this bridge's attributes from a parsed networkstatus document. diff --git a/bridgedb/configure.py b/bridgedb/configure.py index 8befc78..fd997e0 100644 --- a/bridgedb/configure.py +++ b/bridgedb/configure.py @@ -163,6 +163,15 @@ def loadConfig(configFile=None, configCls=None): else: config.RDSYS_TOKEN = ""
+ if os.path.isfile(config.MOAT_SHIM_TOKEN_FILE): + with open(config.MOAT_SHIM_TOKEN_FILE) as f: + setattr(config, "MOAT_SHIM_TOKEN", f.read()) + if not os.path.isfile(config.MOAT_DUMMY_BRIDGES_FILE): + logging.warning("The dummy bridges file '%s' doesn't exist" % (config.MOAT_DUMMY_BRIDGES_FILE,)) + else: + config.MOAT_SHIM_TOKEN = None + logging.info("No shim-token provided, moat will answer each request with bridge authority bridges.") + return config
diff --git a/bridgedb/distributors/moat/distributor.py b/bridgedb/distributors/moat/distributor.py index c4687eb..82a6d3a 100644 --- a/bridgedb/distributors/moat/distributor.py +++ b/bridgedb/distributors/moat/distributor.py @@ -19,7 +19,11 @@ A Distributor that hands out bridges through a web interface. .. inheritance-diagram:: MoatDistributor :parts: 1 """ +import logging
+from bridgedb.bridgerings import BridgeRing +from bridgedb.bridges import Bridge, MalformedBridgeInfo +from bridgedb.crypto import getHMAC from bridgedb.distributors.https.distributor import HTTPSDistributor
class MoatDistributor(HTTPSDistributor): @@ -63,3 +67,52 @@ class MoatDistributor(HTTPSDistributor): """ super(MoatDistributor, self).__init__(totalSubrings, key, proxies, answerParameters) + + def prepopulateRings(self): + """Prepopulate this distributor's hashrings and subhashrings with + bridges. + """ + super(MoatDistributor, self).prepopulateRings() + dummyKey = getHMAC(self.key, "dummy-bridges") + self.dummyHashring = BridgeRing(dummyKey, self.answerParameters) + + def loadDummyBridges(self, dummyBridgesFile): + """Load dummy bridges from a file + """ + with open(dummyBridgesFile) as f: + bridge_line = f.readline() + bridge = Bridge() + try: + bridge.updateFromBridgeLine(bridge_line) + except MalformedBridgeInfo as e: + logging.warning("Got a malformed dummy bridge: %s" % e) + self.dummyHashring.insert(bridge) + + def getBridges(self, bridgeRequest, interval, dummyBridges=False): + """Return a list of bridges to give to a user. + + :type bridgeRequest: :class:`bridgedb.distributors.https.request.HTTPSBridgeRequest` + :param bridgeRequest: A :class:`~bridgedb.bridgerequest.BridgeRequestBase` + with the :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` + attribute set to a string containing the client's IP address. + :param str interval: The time period when we got this request. This + can be any string, so long as it changes with every period. + :param bool dummyBridges: if it should provide dummyBridges or actual bridges from + from the bridge authority. + :rtype: list + :return: A list of :class:`~bridgedb.bridges.Bridge`s to include in + the response. See + :meth:`bridgedb.distributors.https.server.WebResourceBridges.getBridgeRequestAnswer` + for an example of how this is used. + """ + if not dummyBridges: + return super(MoatDistributor, self).getBridges(bridgeRequest, interval) + + if not len(self.dummyHashring): + logging.warn("Bailing! Hashring has zero bridges!") + return [] + + usingProxy = False + subnet = self.getSubnet(bridgeRequest.client, usingProxy) + position = self.mapClientToHashringPosition(interval, subnet) + return self.dummyHashring.getBridges(position, self._bridgesPerResponseMax, filterBySubnet=True) diff --git a/bridgedb/distributors/moat/server.py b/bridgedb/distributors/moat/server.py index fe03fc4..fe59b99 100644 --- a/bridgedb/distributors/moat/server.py +++ b/bridgedb/distributors/moat/server.py @@ -492,7 +492,8 @@ class CaptchaCheckResource(CaptchaResource):
def __init__(self, distributor, schedule, N=1, hmacKey=None, publicKey=None, secretKey=None, - useForwardedHeader=True, skipInvalid=False): + useForwardedHeader=True, skipInvalid=False, + shim_token=None): """Create a new resource for checking CAPTCHA solutions and returning bridges to a client.
@@ -507,6 +508,8 @@ class CaptchaCheckResource(CaptchaResource): X-Forwarded-For header instead of the source IP address. :param bool skipInvalid: Skip invalid (e.g., loopback, private) addresses when parsing the X-Forwarded-For header. + :param bytes shim_token: the token that should be included on the header + 'shim-token' on each request or dummy bridges will be provided. """ CaptchaResource.__init__(self, hmacKey, publicKey, secretKey, useForwardedHeader) @@ -514,6 +517,7 @@ class CaptchaCheckResource(CaptchaResource): self.schedule = schedule self.nBridgesToGive = N self.useForwardedHeader = useForwardedHeader + self.shim_token = shim_token
def createBridgeRequest(self, ip, data): """Create an appropriate :class:`MoatBridgeRequest` from the ``data`` @@ -539,9 +543,30 @@ class CaptchaCheckResource(CaptchaResource):
return bridgeRequest
- def getBridges(self, bridgeRequest): + def getBridges(self, bridgeRequest, dummyBridges=False): """Get bridges for a client's HTTP request.
+ :type bridgeRequest: :class:`MoatBridgeRequest` + :param bridgeRequest: A valid bridge request object with pre-generated + filters (as returned by :meth:`createBridgeRequest`). + :param bool dummyBridges: if it should provide dummyBridges or actual bridges from + from the bridge authority. + :rtype: list + :return: A list of :class:`~bridgedb.bridges.Bridge`s. + """ + bridges = list() + interval = self.schedule.intervalStart(time.time()) + + logging.debug("Replying to JSON API request from %s." % bridgeRequest.client) + + if bridgeRequest.isValid(): + bridges = self.distributor.getBridges(bridgeRequest, interval, dummyBridges) + + return bridges + + def getDummyBridges(self, bridgeRequest): + """Get dummy bridges for a client's HTTP request. + :type bridgeRequest: :class:`MoatBridgeRequest` :param bridgeRequest: A valid bridge request object with pre-generated filters (as returned by :meth:`createBridgeRequest`). @@ -737,7 +762,9 @@ class CaptchaCheckResource(CaptchaResource): if valid: qrcode = None bridgeRequest = self.createBridgeRequest(clientIP, client_data) - bridges = self.getBridges(bridgeRequest) + bridges = [] + dummyBridges = self.shim_token and request.getHeader('shim-token') == self.shim_token + bridges = self.getBridges(bridgeRequest, dummyBridges) bridgeLines = self.getBridgeLines(bridgeRequest, bridges) moatMetrix.recordValidMoatRequest(request)
@@ -810,6 +837,7 @@ def addMoatServer(config, distributor): fwdHeaders = config.MOAT_USE_IP_FROM_FORWARDED_HEADER numBridges = config.MOAT_BRIDGES_PER_ANSWER skipInvalid = config.MOAT_SKIP_LOOPBACK_ADDRESSES + shim_token = config.MOAT_SHIM_TOKEN
logging.info("Starting moat servers...")
@@ -839,7 +867,8 @@ def addMoatServer(config, distributor): fwdHeaders, skipInvalid) check = CaptchaCheckResource(distributor, sched, numBridges, hmacKey, publicKey, secretKey, - fwdHeaders, skipInvalid) + fwdHeaders, skipInvalid, + shim_token)
moat.putChild(b"fetch", fetch) moat.putChild(b"check", check) diff --git a/bridgedb/main.py b/bridgedb/main.py index 72c1f0e..1691c96 100644 --- a/bridgedb/main.py +++ b/bridgedb/main.py @@ -101,6 +101,8 @@ def load(cfg, proxyList, key): proxyList, answerParameters=ringParams) moatDistributor.prepopulateRings() + if cfg.MOAT_DUMMY_BRIDGES_FILE: + moatDistributor.loadDummyBridges(cfg.MOAT_DUMMY_BRIDGES_FILE) rdsys.start_stream("moat", cfg.RDSYS_TOKEN, cfg.RDSYS_ADDRESS, moatDistributor.hashring)
# As appropriate, create an IP-based distributor. diff --git a/bridgedb/test/moat_helpers.py b/bridgedb/test/moat_helpers.py index 236b529..452c207 100644 --- a/bridgedb/test/moat_helpers.py +++ b/bridgedb/test/moat_helpers.py @@ -69,6 +69,7 @@ MOAT_N_IP_CLUSTERS = %r MOAT_ROTATION_PERIOD = %r MOAT_GIMP_CAPTCHA_HMAC_KEYFILE = %r MOAT_GIMP_CAPTCHA_RSA_KEYFILE = %r +MOAT_SHIM_TOKEN = "" """ % (GIMP_CAPTCHA_DIR, SERVER_PUBLIC_FQDN, SUPPORTED_TRANSPORTS, @@ -107,7 +108,7 @@ class DummyMoatDistributor(object): _bridge_class = util.DummyBridge _bridgesPerResponseMin = 3
- def getBridges(self, bridgeRequest=None, epoch=None): + def getBridges(self, bridgeRequest=None, epoch=None, dummyBridges=False): """Needed because it's called in :meth:`BridgesResource.getBridgeRequestAnswer`.""" return [self._bridge_class() for _ in range(self._bridgesPerResponseMin)] diff --git a/bridgedb/test/test_bridges.py b/bridgedb/test/test_bridges.py index 82d9584..61a11ec 100644 --- a/bridgedb/test/test_bridges.py +++ b/bridgedb/test/test_bridges.py @@ -1840,6 +1840,28 @@ class BridgeTests(unittest.TestCase): self.assertNotIn('scramblesuit', [pt.methodname for pt in self.bridge.transports])
+ def test_Bridge_updateFromBridgeLine_vanilla(self): + self.bridge.updateFromBridgeLine("1.1.1.1:1111 2C3225C4805331025E211F4B6E5BF45C333FDD2C") + + self.assertEqual(self.bridge.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") + self.assertEqual(str(self.bridge.address), "1.1.1.1") + self.assertEqual(self.bridge.orPort, 1111) + self.assertEqual(len(self.bridge.transports), 0) + + def test_Bridge_updateFromBridgeLine_obfs4(self): + self.bridge.updateFromBridgeLine("obfs4 1.1.1.1:1111 2C3225C4805331025E211F4B6E5BF45C333FDD2C cert=UXj/cWm0qolGrROYpkl0UyD/7PEhzkoZkZXrOpjRKwImvkpQZwmF0nSzBXfyfbT9afBZEw iat-mode=1") + + self.assertEqual(self.bridge.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") + self.assertEqual(len(self.bridge.transports), 1) + + pt = self.bridge.transports[0] + self.assertEqual(pt.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") + self.assertEqual(pt.methodname, "obfs4") + self.assertEqual(str(pt.address), "1.1.1.1") + self.assertEqual(pt.port, 1111) + self.assertEqual(pt.arguments["iat-mode"], "1") + self.assertEqual(pt.arguments["cert"], "UXj/cWm0qolGrROYpkl0UyD/7PEhzkoZkZXrOpjRKwImvkpQZwmF0nSzBXfyfbT9afBZEw") + def test_runsVersions(self): """Calling runsVersions() should tell us if a bridge is running any of the given versions. diff --git a/bridgedb/test/test_main.py b/bridgedb/test/test_main.py index 71738b4..e3d319e 100644 --- a/bridgedb/test/test_main.py +++ b/bridgedb/test/test_main.py @@ -337,6 +337,7 @@ HTTPS_SHARE = 10 EMAIL_SHARE = 5 RESERVED_SHARE = 2 RDSYS_TOKEN_FILE = "rdsys-token" +MOAT_SHIM_TOKEN_FILE = "shim-token" """ configFile = self._writeConfig(config)
tor-commits@lists.torproject.org