Description
When docker-agent serve api loads an agent from an https URL source, urlSource.Read() in pkg/config/sources.go constructs the Desktop proxy-aware client via httpclient.NewHTTPClient(ctx) but then (unless the source is marked unsafe or is localhost) replaces it with httpclient.NewSSRFSafeTransport(), whose Proxy is http.ProxyFromEnvironment (pkg/httpclient/ssrf.go). Identified by source inspection at v1.115.0 (sources.go:329-344), v1.124.0, v1.125.0, and current HEAD; not yet independently reproduced in a live rig. In environments where outbound HTTPS is only possible via a PAC-file proxy (no static HTTPS_PROXY env var — common in corporate networks), the fetch connects direct and fails at DNS/connect. Because Docker Desktop's managed Gordon agent is loaded from a URL source, the server starts with zero loaded agents: source_loader.go loads synchronously once, getAgents() silently skips the failed source and returns [], the Desktop UI falls back to requesting docker_gordon.yaml, and resolveSource() (pkg/server/session_manager.go) cannot match it → HTTP 500 "agent not found". The OCI source path does not have this bug (pkg/remote/pull.go uses the Desktop-aware transport via crane.WithTransport). Additionally, the URL cache key is the sha256 of the full raw URL (hashURL); since the shipped URL embeds desktopVersion and gordonTag, every Desktop upgrade cold-starts the cache and the failure recurs.
Expected Behavior
URL sources for Docker-hosted endpoints load successfully behind a PAC-only proxy, using the same Desktop proxy-aware transport as OCI pulls; failures should surface as a meaningful error, not a 500 "agent not found".
Actual Behavior
Behind a PAC-only proxy, the initial source fetch fails (direct connect), the agent list is empty, and Desktop's Gordon UI receives HTTP 500 on POST /api/sessions/<id>/agent/docker_gordon.yaml. Because of the version-bearing cache key, the failure recurs after every Desktop upgrade even if a previous cache existed.
Steps to Reproduce
Expected reproduction, derived from source inspection — untested:
- On a host whose only outbound path is a PAC-file proxy (or simulate: block direct egress to the source host with a firewall rule; provide a working proxy configured only via PAC/Desktop proxy settings, with no
HTTP(S)_PROXY env vars).
- Configure Docker Desktop's proxy settings with the PAC (Desktop's own httpproxy will CONNECT to the source host successfully).
- Cold cache: remove
<data-dir>/url_cache.
- Run
docker-agent serve api --listen <sock> --data-dir <dir> https://..../proxy/gordon-agent?... (the argv Desktop uses) or any https URL source resolvable only via the proxy.
- Expected: the source fetch dials direct and fails;
GET /api/agents returns []; any session POST for the agent returns 500.
Docker Agent version
Identified by source inspection at v1.115.0; code present unchanged at v1.124.0, v1.125.0, and HEAD (pkg/config/sources.go, urlSource.Read).
OS & terminal
Not OS-specific — network topology (PAC-only egress) is the trigger.
Error output
Generic: the source load error is swallowed — `getAgents()` returns empty; the client sees `HTTP 500` / `agent not found` from `resolveSource()`.
Additional context / Proposed remediation
Description
When
docker-agent serve apiloads an agent from an https URL source,urlSource.Read()inpkg/config/sources.goconstructs the Desktop proxy-aware client viahttpclient.NewHTTPClient(ctx)but then (unless the source is marked unsafe or is localhost) replaces it withhttpclient.NewSSRFSafeTransport(), whoseProxyishttp.ProxyFromEnvironment(pkg/httpclient/ssrf.go). Identified by source inspection at v1.115.0 (sources.go:329-344), v1.124.0, v1.125.0, and current HEAD; not yet independently reproduced in a live rig. In environments where outbound HTTPS is only possible via a PAC-file proxy (no staticHTTPS_PROXYenv var — common in corporate networks), the fetch connects direct and fails at DNS/connect. Because Docker Desktop's managed Gordon agent is loaded from a URL source, the server starts with zero loaded agents:source_loader.goloads synchronously once,getAgents()silently skips the failed source and returns[], the Desktop UI falls back to requestingdocker_gordon.yaml, andresolveSource()(pkg/server/session_manager.go) cannot match it → HTTP 500 "agent not found". The OCI source path does not have this bug (pkg/remote/pull.gouses the Desktop-aware transport viacrane.WithTransport). Additionally, the URL cache key is the sha256 of the full raw URL (hashURL); since the shipped URL embedsdesktopVersionandgordonTag, every Desktop upgrade cold-starts the cache and the failure recurs.Expected Behavior
URL sources for Docker-hosted endpoints load successfully behind a PAC-only proxy, using the same Desktop proxy-aware transport as OCI pulls; failures should surface as a meaningful error, not a 500 "agent not found".
Actual Behavior
Behind a PAC-only proxy, the initial source fetch fails (direct connect), the agent list is empty, and Desktop's Gordon UI receives HTTP 500 on
POST /api/sessions/<id>/agent/docker_gordon.yaml. Because of the version-bearing cache key, the failure recurs after every Desktop upgrade even if a previous cache existed.Steps to Reproduce
Expected reproduction, derived from source inspection — untested:
HTTP(S)_PROXYenv vars).<data-dir>/url_cache.docker-agent serve api --listen <sock> --data-dir <dir> https://..../proxy/gordon-agent?...(the argv Desktop uses) or any https URL source resolvable only via the proxy.GET /api/agentsreturns[]; any session POST for the agent returns 500.Docker Agent version
Identified by source inspection at v1.115.0; code present unchanged at v1.124.0, v1.125.0, and HEAD (
pkg/config/sources.go,urlSource.Read).OS & terminal
Not OS-specific — network topology (PAC-only egress) is the trigger.
Error output
Additional context / Proposed remediation
docker.com/*.docker.comsource URLs, use the Desktop proxy-aware transport with an explicitTimeoutandCheckRedirect(NewHTTPClientsets neither); keepNewSSRFSafeTransportfor all other non-localhost URLs. Do NOT reuseenvironment.IsTrustedDockerURLas the predicate — it admits localhost/loopback over plain http and gates JWT injection, not SSRF. Alternative worth security review: teachNewSSRFSafeTransportthe Desktop proxy socket while keeping its dial-time allowlist (preserves anti-DNS-rebinding hardening for all hosts).source_loader.gowhen the initial synchronous load fails (today only the 60-min ticker retries, and only whenrefreshInterval > 0).