mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
fix(remote): send the typed --proxy to the server when it resolved to loopback
dl and search keep the typed --proxy value, so --proxy controld:ca --remote sends the server the Control D resolver, not this machine's forwarder. The Control D log names the location and masks the resolver ID in place of 127.0.0.1.
This commit is contained in:
@@ -288,7 +288,8 @@ run. Give `profile` and `resolver` only to use a profile that you made yourself
|
||||
[How unshackle chooses a profile](#how-unshackle-chooses-a-profile)).
|
||||
|
||||
The returned proxy is HTTP on loopback,
|
||||
`http://127.0.0.1:{port}`. The locations themselves are read from Control D at startup
|
||||
`http://127.0.0.1:{port}`. The log shows the location and a masked resolver ID, for example
|
||||
`Using ControlD Proxy (YUL - Montreal): abcd***@dns.controld.com`. The locations themselves are read from Control D at startup
|
||||
(107 of them across 68 countries, at the time of writing), so there is nothing to list:
|
||||
|
||||
| Query | Meaning |
|
||||
|
||||
@@ -267,12 +267,14 @@ def account(catalogue: None, monkeypatch: pytest.MonkeyPatch) -> Iterator[Accoun
|
||||
monkeypatch.setattr("unshackle.core.proxies.controld.requests.request", made.request)
|
||||
controld.FORWARDERS.clear()
|
||||
controld.REGIONS.clear()
|
||||
controld.LOCATIONS.clear()
|
||||
yield made
|
||||
for server in controld.FORWARDERS.values():
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
controld.FORWARDERS.clear()
|
||||
controld.REGIONS.clear()
|
||||
controld.LOCATIONS.clear()
|
||||
|
||||
|
||||
def test_locations_come_from_control_d(account: Account) -> None:
|
||||
@@ -378,6 +380,13 @@ def test_a_server_runs_a_forwarder_for_a_resolver_a_client_sends(account: Accoun
|
||||
local_proxy("controld://evil.example/x@dns.controld.com")
|
||||
|
||||
|
||||
def test_the_log_names_the_location_and_masks_the_resolver(account: Account) -> None:
|
||||
provider = ControlD(token="secret", profile="abcd123", resolver="efgh456")
|
||||
provider.get_proxy("yul")
|
||||
provider.get_proxy("yul") # a profile already on the region keeps its location
|
||||
assert provider.last_connection_display() == "(YUL - Montreal): efgh***@dns.controld.com"
|
||||
|
||||
|
||||
def test_remote_sends_the_resolver_and_a_bare_region_skips_control_d(
|
||||
account: Account, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
@@ -391,6 +400,15 @@ def test_remote_sends_the_resolver_and_a_bare_region_skips_control_d(
|
||||
remote_service.resolve_remote_proxy_arg("us") # a loopback forwarder is useless to a server
|
||||
|
||||
|
||||
def test_a_server_gets_the_typed_proxy_only_when_this_machine_resolved_it_to_loopback() -> None:
|
||||
from unshackle.core.remote_service import server_proxy_arg
|
||||
|
||||
assert server_proxy_arg({"proxy": "http://127.0.0.1:4000", "proxy_arg": "controld:ca"}) == "controld:ca"
|
||||
assert server_proxy_arg({"proxy": "https://u:p@ca1.example:89", "proxy_arg": "nordvpn:ca"}) == (
|
||||
"https://u:p@ca1.example:89"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("query", ["ca", "us:seattle", "us-ny", "uk1", "yul", "jfk", "res_yyz", "nordvpn:ca"])
|
||||
def test_a_region_query_matches(query: str) -> None:
|
||||
assert re.fullmatch(rf"(?:[a-z]+:)?{REGION}", query, re.IGNORECASE)
|
||||
|
||||
@@ -1613,6 +1613,7 @@ class dl:
|
||||
with console.status("Loading Proxy Providers...", spinner="dots"):
|
||||
self.proxy_providers = load_proxy_providers()
|
||||
|
||||
ctx.params["proxy_arg"] = proxy
|
||||
if proxy and proxy.startswith("controld://"):
|
||||
proxy = ctx.params["proxy"] = resolve_proxy(proxy, [])
|
||||
if proxy:
|
||||
@@ -1647,9 +1648,6 @@ class dl:
|
||||
ctx.params["proxy_query"] = None
|
||||
ctx.params["proxy_provider"] = None
|
||||
|
||||
if self.remote_needs_public_proxy and ctx.params.get("proxy") and is_loopback(ctx.params["proxy"]):
|
||||
raise click.UsageError("That proxy is on your machine, so --remote cannot use it.")
|
||||
|
||||
ctx.obj = ContextData(
|
||||
config=self.service_config,
|
||||
cdm=self.cdm,
|
||||
|
||||
@@ -88,6 +88,7 @@ def search(
|
||||
with console.status("Loading Proxy Providers...", spinner="dots"):
|
||||
proxy_providers = load_proxy_providers()
|
||||
|
||||
ctx.params["proxy_arg"] = proxy
|
||||
if proxy:
|
||||
try:
|
||||
requested_provider, query = split_proxy_query(proxy, proxy_providers)
|
||||
|
||||
@@ -16,6 +16,7 @@ import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from unshackle.core.proxies.proxy import Proxy
|
||||
from unshackle.core.utils.redact import REDACTED
|
||||
|
||||
log = logging.getLogger("ControlD")
|
||||
|
||||
@@ -261,6 +262,7 @@ class LocalProxy(socketserver.ThreadingTCPServer):
|
||||
# process-wide across provider instances; separate processes share only the account's unshackle-<region> names
|
||||
FORWARDERS: dict[str, LocalProxy] = {}
|
||||
REGIONS: dict[str, str] = {} # profile -> region, oldest claim first
|
||||
LOCATIONS: dict[str, str] = {} # profile -> the location code it redirects through
|
||||
STATE = threading.Lock()
|
||||
|
||||
|
||||
@@ -337,6 +339,7 @@ class ControlD(Proxy):
|
||||
self.configured = {x["profile"]: resolver_id(x["resolver"]) for x in pairs}
|
||||
self.max_profiles = max_profiles
|
||||
self.resolvers = dict(self.configured)
|
||||
self.last_profile: Optional[str] = None
|
||||
|
||||
self.catalogue = self.get_locations()
|
||||
|
||||
@@ -459,6 +462,8 @@ class ControlD(Proxy):
|
||||
server.resolver.cache.clear()
|
||||
REGIONS.pop(profile, None)
|
||||
REGIONS[profile] = query
|
||||
LOCATIONS[profile] = location
|
||||
self.last_profile = profile
|
||||
return resolver
|
||||
|
||||
def get_proxy(self, query: str) -> Optional[str]:
|
||||
@@ -467,6 +472,15 @@ class ControlD(Proxy):
|
||||
resolver = self.claim(query.lower())
|
||||
return forwarder(resolver).uri if resolver else None
|
||||
|
||||
def last_connection_display(self) -> Optional[str]:
|
||||
"""Name the location of the last claim, with the resolver ID masked, in place of the loopback URI."""
|
||||
location = LOCATIONS.get(self.last_profile or "")
|
||||
if not location:
|
||||
return None
|
||||
city = next((x.get("city") for x in self.catalogue if x["PK"] == location), None)
|
||||
label = f"{location} - {city}" if city else location
|
||||
return f"({label}): {self.resolvers[self.last_profile][:4]}{REDACTED}@dns.controld.com"
|
||||
|
||||
def get_remote_proxy(self, query: str) -> Optional[str]:
|
||||
"""Get the `controld://` proxy URI for this region, for a server on another machine."""
|
||||
with STATE:
|
||||
|
||||
@@ -920,6 +920,12 @@ def resolve_remote_proxy_arg(proxy_arg: str, providers: Optional[list] = None) -
|
||||
raise click.ClickException(str(e))
|
||||
|
||||
|
||||
def server_proxy_arg(params: Dict[str, Any]) -> str:
|
||||
"""The --proxy value to resolve for a server: the one typed when it resolved to a proxy on this machine."""
|
||||
proxy = params["proxy"]
|
||||
return (params.get("proxy_arg") or proxy) if is_loopback(proxy) else proxy
|
||||
|
||||
|
||||
_CACHE_HEX_ID_RE = re.compile(r"[0-9a-f]{32,}", re.IGNORECASE)
|
||||
|
||||
|
||||
@@ -1159,7 +1165,8 @@ class RemoteService:
|
||||
if params.get("no_proxy"):
|
||||
data["no_proxy"] = True
|
||||
elif params.get("proxy"):
|
||||
data["proxy"] = resolve_remote_proxy_arg(params["proxy"], getattr(self.ctx.obj, "proxy_providers", None))
|
||||
providers = getattr(self.ctx.obj, "proxy_providers", None)
|
||||
data["proxy"] = resolve_remote_proxy_arg(server_proxy_arg(params), providers)
|
||||
self.require_reachable_proxy(data["proxy"])
|
||||
response = self.client.post("/api/search", data)
|
||||
self.save_returned_cache(response.get("cache") or {}, profile)
|
||||
@@ -1226,7 +1233,7 @@ class RemoteService:
|
||||
"rejects the remote session"
|
||||
)
|
||||
elif not no_proxy and proxy:
|
||||
resolved_proxy = resolve_remote_proxy_arg(proxy, providers)
|
||||
resolved_proxy = resolve_remote_proxy_arg(server_proxy_arg(self.ctx.parent.params), providers)
|
||||
if resolved_proxy:
|
||||
create_data["proxy"] = resolved_proxy
|
||||
query = (self.ctx.parent.params.get("proxy_query") if self.ctx.parent else None) or proxy
|
||||
|
||||
Reference in New Issue
Block a user