From 6b9905b7f14b76ed1d1a66466d85373f203e3c70 Mon Sep 17 00:00:00 2001 From: Simon Holesch Date: Thu, 8 Feb 2024 12:10:17 +0100 Subject: [PATCH] Agent: Support exporters with loopback host If an Exporter and the Hub run on the same host and the Exporter connects to the hub via localhost, then the host of the Place is the loopback address for every agent. This prevents agents from connecting if they are on a remote host. Fixed by replacing the host of the Place with the host of the Hub, if the host of the Place is the loopback address. I considered replacing the host in `/api/v1/places` on the Hub, but then clients would get different responses based on their source IP address, which doesn't work well with HTTP caching, if it will ever be used. --- not_my_board/_agent.py | 48 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/not_my_board/_agent.py b/not_my_board/_agent.py index 1b78dbb..d869429 100644 --- a/not_my_board/_agent.py +++ b/not_my_board/_agent.py @@ -7,6 +7,7 @@ import pathlib import traceback import urllib.parse +import ipaddress import websockets @@ -29,17 +30,18 @@ def __init__(self, hub_url): self._hub_url = hub_url self._reserved_places = {} self._pending = set() + url = urllib.parse.urlsplit(hub_url) + ws_scheme = "ws" if url.scheme == "http" else "wss" + self._ws_uri = f"{ws_scheme}://{url.netloc}/ws-agent" + self._hub_host = url.netloc.split(":")[0] async def __aenter__(self): runtime_dir = pathlib.Path(os.environ["XDG_RUNTIME_DIR"]) async with contextlib.AsyncExitStack() as stack: - url = urllib.parse.urlsplit(self._hub_url) - ws_scheme = "ws" if url.scheme == "http" else "wss" - uri = f"{ws_scheme}://{url.netloc}/ws-agent" headers = {"Authorization": "Bearer dummy-token-1"} ws = await stack.enter_async_context( - websockets.connect(uri, extra_headers=headers) + websockets.connect(self._ws_uri, extra_headers=headers) ) async def receive_iter(): @@ -102,7 +104,7 @@ async def reserve(self, name, import_description_file): response = await http.get_json(f"{self._hub_url}/api/v1/places") places = [models.Place(**p) for p in response["places"]] - candidates = _filter_places(import_description, places) + candidates = self._filter_places(import_description, places) candidate_ids = list(candidates) if not candidate_ids: raise RuntimeError("No matching place found") @@ -160,23 +162,29 @@ async def status(self): for status in place.status ] + def _filter_places(self, import_description, places): + reserved_places = {} -def _filter_places(import_description, places): - reserved_places = {} + imported_part_sets = [ + (name, _part_to_set(imported_part)) + for name, imported_part in import_description.parts.items() + ] - imported_part_sets = [ - (name, _part_to_set(imported_part)) - for name, imported_part in import_description.parts.items() - ] + for place in places: + matching = _find_matching(imported_part_sets, place) + if matching: + real_host = self._real_host(place.host) + reserved_places[place.id] = ReservedPlace( + import_description, place, real_host, matching + ) - for place in places: - matching = _find_matching(imported_part_sets, place) - if matching: - reserved_places[place.id] = ReservedPlace( - import_description, place, matching - ) + return reserved_places - return reserved_places + def _real_host(self, host): + if ipaddress.ip_address(host).is_loopback: + logger.info("Replacing %s with %s", host, self._hub_host) + return self._hub_host + return host def _find_matching(imported_part_sets, place): @@ -215,13 +223,13 @@ def _part_to_set(part): class ReservedPlace: - def __init__(self, import_description, place, matching): + def __init__(self, import_description, place, real_host, matching): self._import_description = import_description self._place = place self._tunnels = [] self._stack = None self.lock = asyncio.Lock() - proxy = str(place.host), place.port + proxy = real_host, place.port for name, place_part_idx in matching: imported_part = import_description.parts[name]