"""Which inference endpoints the product will and will not talk to. The endpoint is where the whole campaign goes: prompts, narration, retrieved memories, embedding inputs. `app/endpoints.py` is the rule that keeps that on the user's own infrastructure, and these tests are what stops the rule quietly widening. Nothing here touches the network. Hostnames are resolved through a stub, so the tests are about the *policy*, run identically offline, and cannot be made to pass or fail by whatever DNS the machine happens to have. python -m pytest tests/test_endpoint_policy.py -v """ import ipaddress import socket import pytest from fastapi import Depends from fastapi.testclient import TestClient from app import auth, endpoints, models from app.database import Base, SessionLocal, engine, get_db from app.main import app from app.providers import OpenAICompatibleProvider, ProviderError @pytest.fixture() def resolves(monkeypatch): """Points every hostname at addresses the test names.""" table: dict[str, list[str]] = {} def fake_getaddrinfo(host, port, *a, **k): # An address literal resolves to itself, as the real resolver does. try: ipaddress.ip_address(host) except ValueError: if host not in table: raise socket.gaierror(-2, "Name or service not known") answers = table[host] else: answers = [host] return [ ( socket.AF_INET6 if ":" in ip else socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port or 0), ) for ip in answers ] monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo) return table # --- allowed: the user's own machine, and the user's own network ---------- @pytest.mark.parametrize("url", [ "http://127.0.0.1:11434/v1", "http://[::1]:11434/v1", "http://192.168.1.50:11434/v1", # RFC1918 "http://10.0.0.7:11434/v1", # RFC1918 "http://172.16.4.4:11434/v1", # RFC1918 "https://192.168.1.50:8443/v1", # TLS on the LAN "http://100.100.5.6:11434/v1", # CGNAT, which is what a mesh VPN hands out "http://[fd00::1]:11434/v1", # IPv6 unique-local ]) def test_local_and_lan_addresses_are_allowed(url, resolves): assert endpoints.rejection_reason(url) is None def test_a_hostname_resolving_to_the_lan_is_allowed(resolves): resolves["ollama.home.arpa"] = ["192.168.1.50"] assert endpoints.rejection_reason("https://ollama.home.arpa:8443/v1") is None # --- refused: anywhere else ---------------------------------------------- @pytest.mark.parametrize("url", [ "http://8.8.8.8:11434/v1", "http://1.1.1.1:11434/v1", "http://[2001:4860:4860::8888]:11434/v1", ]) def test_public_addresses_are_refused(url, resolves): reason = endpoints.rejection_reason(url) assert reason is not None assert "public Internet address" in reason @pytest.mark.parametrize("url", [ "https://198.51.100.9/v1", # TEST-NET-2, a documentation range "http://0.0.0.0:11434/v1", # "this host", not an address to dial ]) def test_addresses_that_are_neither_local_nor_public_are_refused(url, resolves): """Deny by default. `ipaddress` calls both of these "private", which is why the policy names the networks it allows instead of asking that question.""" assert endpoints.rejection_reason(url) is not None def test_a_hostname_resolving_to_the_public_internet_is_refused(resolves): resolves["ollama.example.com"] = ["93.184.216.34"] reason = endpoints.rejection_reason("https://ollama.example.com/v1") assert reason is not None and "public Internet address" in reason def test_a_split_horizon_answer_is_refused(resolves): """One public address among the answers is enough. A name that resolves to both a LAN address and a public one must not be usable: which one the request actually reaches is not ours to decide.""" resolves["sneaky.example"] = ["192.168.1.50", "93.184.216.34"] assert endpoints.rejection_reason("http://sneaky.example:11434/v1") is not None @pytest.mark.parametrize("host", [ "openrouter.ai", "api.openai.com", "api.anthropic.com", "api.groq.com", ]) def test_known_cloud_providers_are_named_in_the_refusal(host, resolves): """They would be refused by address anyway — every one resolves publicly. Naming them turns 'your DNS might be broken' into 'this build has no cloud provider support'.""" resolves[host] = ["192.168.1.50"] # even if DNS said otherwise reason = endpoints.rejection_reason(f"https://{host}/v1") assert reason is not None assert "cloud inference service" in reason def test_a_subdomain_of_a_cloud_provider_is_refused(resolves): resolves["eu.api.openai.com"] = ["192.168.1.50"] assert endpoints.rejection_reason("https://eu.api.openai.com/v1") is not None @pytest.mark.parametrize("url", [ "ftp://127.0.0.1/v1", "file:///etc/passwd", "127.0.0.1:11434", # no scheme "", "http://", ]) def test_things_that_are_not_usable_urls_are_refused(url, resolves): assert endpoints.rejection_reason(url) is not None def test_an_unresolvable_host_is_refused_with_advice(resolves): reason = endpoints.rejection_reason("http://nope.invalid:11434/v1") assert reason is not None and "could not be resolved" in reason # --- the rule is applied, not merely available --------------------------- @pytest.fixture() def client(monkeypatch): Base.metadata.create_all(bind=engine) setup = SessionLocal() user = models.User(is_guest=False) setup.add(user) setup.flush() setup.add(models.Settings(user_id=user.id, model="test-model")) setup.commit() user_id = user.id setup.close() def _current_user(db=Depends(get_db)): return db.get(models.User, user_id) app.dependency_overrides[auth.get_current_user] = _current_user c = TestClient(app) try: yield c finally: app.dependency_overrides.clear() Base.metadata.drop_all(bind=engine) def test_saving_a_public_endpoint_is_refused(client, resolves): resolves["openrouter.ai"] = ["93.184.216.34"] r = client.put("/api/settings", json={"endpoint_url": "https://openrouter.ai/api/v1"}) assert r.status_code == 400, r.text assert "can't be used" in r.json()["detail"] # And it was not written. assert client.get("/api/settings").json()["endpoint_url"] != "https://openrouter.ai/api/v1" def test_saving_a_lan_endpoint_is_allowed(client, resolves): r = client.put("/api/settings", json={"endpoint_url": "http://192.168.1.50:11434/v1"}) assert r.status_code == 200, r.text assert client.get("/api/settings").json()["endpoint_url"] == "http://192.168.1.50:11434/v1" def test_the_connection_test_reports_a_refused_endpoint_as_such(client, resolves): """A row edited by hand, or a name that has started resolving elsewhere, must not simply look 'unreachable'.""" db = SessionLocal() try: db.query(models.Settings).first().endpoint_url = "https://openrouter.ai/api/v1" db.commit() finally: db.close() resolves["openrouter.ai"] = ["93.184.216.34"] body = client.post("/api/settings/test").json() assert body["ok"] is False assert body["kind"] == "rejected" @pytest.mark.anyio async def test_the_provider_refuses_before_it_sends_anything(resolves): """The check that actually matters. Whatever is stored, no request leaves for an address outside the policy — so a database edited behind the app's back cannot turn into an exfiltration path.""" resolves["openrouter.ai"] = ["93.184.216.34"] provider = OpenAICompatibleProvider("https://openrouter.ai/api/v1", "m") with pytest.raises(ProviderError) as exc: async for _ in provider.generate( _parts(), temperature=0.8, max_tokens=10 ): pass assert "can't be used" in str(exc.value) def _parts(): from app.providers.base import PromptParts return PromptParts(system="s", story="t") @pytest.fixture def anyio_backend(): return "asyncio"