# SPDX-FileCopyrightText: 2022 The Tor Project, Inc.
#
# SPDX-License-Identifier: BSD-3-Clause
import logging
import random
from asgiref.sync import sync_to_async
from django.db import models
from onbasca.base.models.bridge import BridgeBase, BridgeManagerBase
from onbasca.onbasca.models import Consensus
from onbrisca import config
logger = logging.getLogger(__name__)
[docs]
class BridgeManager(BridgeManagerBase):
"""BridgeManager model for operations with all the bridges."""
[docs]
def bridgelines(self) -> list:
bridge_list = [
bridge.bridgeline
for bridge in Bridge.objects.all()
if bridge.bridgeline
]
return bridge_list
[docs]
def bridgelines_from_bridges(self, bridges) -> list:
"""Return a bridgelines list for the given set of bridges."""
# logger.debug("bridges %s", bridges)
bridge_list = [
bridge.bridgeline for bridge in bridges if bridge.bridgeline
]
logger.debug("bridge list %s", bridge_list)
return bridge_list
[docs]
def bridgelines_config(self) -> dict:
bridge_dict = {
"Bridge": self.bridgelines(),
}
return bridge_dict
[docs]
def mu(self):
return self.annotate(
bridge_bw_mean=models.Avg("measurements__bandwidth")
).aggregate(models.Avg("bridge_bw_mean"))["bridge_bw_mean__avg"]
[docs]
async def acount(self):
return sync_to_async(self.count())
[docs]
def ordered(self):
logger.debug("Prioritizing to measure bridges without measurements.")
# The bridges are ordered by default (`get_latest_by`) by
# `_last_measured` ascending. Because here we are selecting the
# bridges without any measurement, then they will be ordered by
# `_obj_updated_at` descending, ie. the ones more recently updated
# (or created) first.
bridges_no_measurements = self.filter(
measurements__isnull=True,
).distinct()
bridges = bridges_no_measurements[: config.NUM_BRIDGES_LOOP]
bridges_count = bridges.count()
logger.debug("Adding %s bridges never measured yet.", bridges_count)
if bridges_count < config.NUM_BRIDGES_LOOP:
bridges_to_add = config.NUM_BRIDGES_LOOP - bridges_count
logger.debug("Adding %s bridges already measured.", bridges_to_add)
# Then select the bridges that have been already measured, the ones
# with older measurements first. If 2 bridges were measured at the
# same time, the one most recently updated (or created) is measured
# first.
bridges_measurements = self.filter(
measurements__isnull=False
).distinct()
bridges = list(bridges)
bridges.extend(list(bridges_measurements[:bridges_to_add]))
# Convert it to list when it's still a queryset, so that it's always
# a list
return list(bridges)
[docs]
def muf(self):
return (
self.annotate(bridge_bw_mean=models.Avg("measurements__bandwidth"))
.filter(measurements__bandwidth__gte=models.F("bridge_bw_mean"))
.aggregate(models.Avg("bridge_bw_mean"))["bridge_bw_mean__avg"]
)
[docs]
def delete_invalid(self):
"""
Delete the bridges in the database that have invalid bridgelines,
so that they don't fail to be set to tor configuration.
"""
from onbasca.bridgeline import parse_bridge_line
logger.debug("Deleting bridges with invalid bridgelines.")
for bridge in self.all():
if not parse_bridge_line(bridge.bridgeline):
logger.warning(
"Deleting bridge with invalid bridgeline %s.",
bridge.bridgeline,
)
bridge.delete()
[docs]
class Bridge(BridgeBase):
"""Bridge model that calculates and saves the stream ratio."""
class Meta:
# Latest by ascending `_last_measured` and descending `_obj_updated_at`
get_latest_by = ["_last_measured", "-_obj_updated_at"]
objects = BridgeManager()
_bw_mean = models.PositiveIntegerField(null=True, blank=True)
_bw_filt = models.PositiveIntegerField(null=True, blank=True)
_ratio_stream = models.FloatField(null=True, blank=True)
_last_measured = models.DateTimeField(blank=True, null=True)
_ratio_filt = models.FloatField(null=True, blank=True)
_ratio = models.FloatField(null=True, blank=True)
[docs]
async def asave(self, *args, **kwargs):
await sync_to_async(super().save)(*args, **kwargs)
[docs]
async def helper_path(self, consensus: Consensus) -> list:
""" """
logger.debug("Creating path for bridge %s.", self)
middle_candidates_fingerprints = (
await consensus.aget_fast_stable_uptime_non_exits_fingerprints()
)
middle_fp = await sync_to_async(random.choice)(
middle_candidates_fingerprints
)
exit_candidates_fingerprints = (
await consensus.aget_fast_exits_min_bandwidth()
)
exit_fp = await sync_to_async(random.choice)(
exit_candidates_fingerprints
)
path = [self.fingerprint, middle_fp, exit_fp]
logger.debug("Created path: %s", path)
return path
[docs]
def set_bw_mean(self):
self._bw_mean = (
self.measurements.aggregate(models.Avg("bandwidth"))[
"bandwidth__avg"
]
or 0
)
# logger.debug("Bridge %s bw mean: %s", self, self._bw_mean)
self.save()
return self._bw_mean
[docs]
def set_bw_filt(self):
# This should be the same as:
# self.measurements.annotate(bw_mean=models.Avg("bandwidth"))
# .filter(bandwidth__gte=models.F("bw_mean"))
# .aggregate(models.Avg("bandwidth"))["bandwidth__avg"]
self._bw_filt = (
self.measurements.filter(bandwidth__gte=self._bw_mean).aggregate(
models.Avg("bandwidth")
)["bandwidth__avg"]
or self._bw_mean
)
# logger.debug("Bridge %s bw mean filtered: %s", self, self._bw_mean)
self.save()
return self._bw_filt
[docs]
def set_ratio_stream(self, mu):
if not mu:
# logger.warning("Unexpected mu %s", mu)
# If there aren't measurements (mu is None), set ratio to 0 too.
self._ratio_stream = 0
else:
self._ratio_stream = self._bw_mean / mu
# logger.debug("Bridge %s stream ratio: %s", self, self._ratio_stream)
self.save()
return self._ratio_stream
[docs]
def set_ratio_filt(self, muf):
if not muf:
# logger.warning("Unexpected muf %s", muf)
self._ratio_filt = 0
else:
self._ratio_filt = self._bw_filt / muf
# logger.debug("Bridge filtered stream ratio: %s", self._ratio_filt)
self.save()
return self._ratio_filt
[docs]
def set_ratio(self):
# if self._ratio_stream > self._ratio_filt:
# logger.info("Stream ratio greater than filtered.")
self._ratio = max(self._ratio_stream, self._ratio_filt)
logger.debug("Bridge ratio: %s", self._ratio)
self.save()
return self._ratio
[docs]
def set_ratios(self, mu, muf):
self.set_bw_mean()
self.set_bw_filt()
self.set_ratio_stream(mu)
self.set_ratio_filt(muf)
self.set_ratio()
return self._ratio
[docs]
def is_valid(self, ratio, ratio_threshold=config.BRIDGE_RATIO_THRESHOLD):
if not ratio:
# logger.debug("No ratio for bridge %s", self.fingerprint)
# If it has not been measured yet, it's valid
if not self.latest_measurement():
logger.debug(
"No measurements for bridge %s yet.", self.fingerprint
)
return True, None
# If none of the measurements was successful
error = self.latest_measurement().error
logger.debug(
"No successful measurements for bridge %s: %s",
self.fingerprint,
error,
)
return False, error
if ratio < ratio_threshold:
return False, None
return True, None
[docs]
def latest_measurement(self):
try:
latest_measurement = self.measurements.latest()
except models.ObjectDoesNotExist:
return None
return latest_measurement