fix(proxy): keep service traffic on --proxy and warn about requests that bypass it

--proxy now overrides HTTP(S)_PROXY and the Windows system proxy while keeping the env CA bundle. Core fallback sessions use the run's proxy, and a warn-only audit hook names service code that sends a request or opens a raw socket outside the proxy; a session with proxy_exempt = True opts out.
This commit is contained in:
imSp4rky
2026-10-16 16:12:42 -06:00
parent 95ec73ece5
commit bfda31ce4a
25 changed files with 575 additions and 28 deletions
+1 -1
View File
@@ -139,7 +139,7 @@ will use throughout the service:
|---|---|
| `self.config` | This service's `config.yaml` contents (a dict), or `None` if absent. |
| `self.log` | A `logging.Logger` named after your class. |
| `self.session` | A prepared `requests.Session` with config headers and a retry adapter (5 retries, backoff 0.2, retries on 429/500/502/503/504). |
| `self.session` | A prepared `requests.Session` with config headers and a retry adapter (5 retries, backoff 0.2, retries on 429/500/502/503/504). Send every request through it: with a proxy active, a request from service code that does not go through the proxy logs a warning. A separate HTTP session that goes direct on purpose sets `proxy_exempt = True`. |
| `self.cache` | A `Cacher` for arbitrary key/value data, for example authentication tokens. |
| `self.title_cache` | A region/account-aware `TitleCacher` used by `get_titles_cached`. |
| `self.cache_dir` | `config.directories.cache / <ClassName>`. |
+1 -1
View File
@@ -114,7 +114,7 @@ up the objects your methods rely on:
|---|---|
| `self.config` | The per-service config slice (see [Request and HTTP session objects](#request-and-http-session-objects)). |
| `self.log` | A `logging.Logger` named after your class. |
| `self.session` | A ready-to-use HTTP session (headers, retries, and proxy applied). |
| `self.session` | A ready-to-use HTTP session (headers, retries, and proxy applied). Send every request through it: with a proxy active, a request from service code that does not go through the proxy logs a warning. A separate HTTP session that goes direct on purpose sets `proxy_exempt = True`. |
| `self.cache` | A generic key/value `Cacher` namespaced to the service. |
| `self.title_cache` | A region- and account-aware `TitleCacher`. |
| `self.cache_dir` | `config.directories.cache / <ServiceName>`. |
+39
View File
@@ -176,6 +176,32 @@ because geolocation data can be out of date.
Over the REST API, the server returns this failure to the client as `INVALID_PROXY`.
### System proxy and environment variables
`--proxy` takes priority over the `HTTP_PROXY` and `HTTPS_PROXY` environment variables
and over the Windows system proxy. unshackle still reads a `REQUESTS_CA_BUNDLE` or
`CURL_CA_BUNDLE` certificate bundle, so a local debugging proxy with its own certificate
authority continues to work. Without `--proxy`, unshackle uses the system proxy.
### Requests outside the proxy
With a proxy active, unshackle logs a warning when service code sends a request that does
not go through the proxy:
```text
A request to api.example.com did not go through the proxy, so api.example.com sees your own IP (EXAMPLE/__init__.py:120)
```
The host in the message sees your own IP address, and a service can treat that as proxy
detection. The request still goes: unshackle only warns. Report the message, with the
file and line, to the service maintainer.
A request to your own machine or to your local network gives no warning. This includes a
private IP address such as `192.168.1.50`, and a `.local`, `.lan` or `.home.arpa` host name.
A `socks5://` proxy makes the DNS lookup of each host on your machine and gives no warning.
Use `socks5h://` to make the proxy do the DNS lookups too.
## Basic (static proxies)
The `Basic` proxy provider is pure static configuration, with no accounts and no network
@@ -868,6 +894,19 @@ The returned proxy is HTTPS on **port 443**.
resolver for the server with `get_remote_proxy()`, and it removes Control D from a bare
query, because a loopback forwarder is of no use to a server on another machine.
!!! note "Keeping service requests on the proxy (developers)"
`unshackle/core/utils/run_proxy.py` holds the proxy rules for service traffic:
- `pin(session)` stops environment and system proxies from overriding the proxy of a
requests HTTP session, and keeps the environment CA bundle. Call it on each requests
HTTP session that gets a proxy.
- `fallback_session()` gives a core helper with no HTTP session (a manifest parser, a DRM
class) a session on the proxy of the service that called it.
- An audit hook on `socket.getaddrinfo` and on `socket.connect` to a host name, and a
check in `RnetSession.request`, warn about requests from service code that do not use
the proxy. A service sets
`proxy_exempt = True` on an HTTP session that goes direct on purpose.
!!! note "Implementing a new provider (developers)"
Every proxy provider subclasses `unshackle.core.proxies.proxy.Proxy` and has three
methods: