diff --git a/decnet/web/router/swarm_mgmt/api_enroll_bundle.py b/decnet/web/router/swarm_mgmt/api_enroll_bundle.py index 31683c2..8b72b25 100644 --- a/decnet/web/router/swarm_mgmt/api_enroll_bundle.py +++ b/decnet/web/router/swarm_mgmt/api_enroll_bundle.py @@ -288,7 +288,13 @@ async def create_enroll_bundle( sh_path = BUNDLE_DIR / f"{token}.sh" tgz_path = BUNDLE_DIR / f"{token}.tgz" - base = str(request.base_url).rstrip("/") + # Build URLs against the operator-supplied master_host (reachable from the + # new agent) rather than request.base_url, which reflects how the dashboard + # user reached us — often 127.0.0.1 behind a proxy or loopback-bound API. + scheme = request.url.scheme + port = request.url.port + netloc = req.master_host if port is None else f"{req.master_host}:{port}" + base = f"{scheme}://{netloc}" tarball_url = f"{base}/api/v1/swarm/enroll-bundle/{token}.tgz" bootstrap_url = f"{base}/api/v1/swarm/enroll-bundle/{token}.sh" script = _render_bootstrap(req.agent_name, req.master_host, tarball_url, expires_at) diff --git a/tests/api/swarm_mgmt/test_enroll_bundle.py b/tests/api/swarm_mgmt/test_enroll_bundle.py index 6e9065d..788a280 100644 --- a/tests/api/swarm_mgmt/test_enroll_bundle.py +++ b/tests/api/swarm_mgmt/test_enroll_bundle.py @@ -55,6 +55,25 @@ async def test_create_bundle_returns_one_liner(client, auth_token): assert timedelta(minutes=4) < expires - now <= timedelta(minutes=5) +@pytest.mark.anyio +async def test_bundle_urls_use_master_host_not_request_base(client, auth_token): + """URLs baked into the bootstrap must target the operator-supplied + master_host, not the dashboard's request.base_url (which may be loopback + behind a proxy).""" + resp = await _post(client, auth_token, master_host="10.20.30.40", agent_name="urltest") + assert resp.status_code == 201 + body = resp.json() + assert "10.20.30.40" in body["command"] + assert "127.0.0.1" not in body["command"] + assert "testserver" not in body["command"] + + token = body["token"] + sh = (await client.get(f"/api/v1/swarm/enroll-bundle/{token}.sh")).text + assert "10.20.30.40" in sh + assert "127.0.0.1" not in sh + assert "testserver" not in sh + + @pytest.mark.anyio async def test_duplicate_agent_name_409(client, auth_token): r1 = await _post(client, auth_token, agent_name="dup-node")