Skip to content

fix(adapters): refuse a unix-socket upstream in the nginx importer - #412

Open
shuvamk wants to merge 1 commit into
oblien:mainfrom
shuvamk:fix/nginx-import-unix-socket-upstream
Open

fix(adapters): refuse a unix-socket upstream in the nginx importer#412
shuvamk wants to merge 1 commit into
oblien:mainfrom
shuvamk:fix/nginx-import-unix-socket-upstream

Conversation

@shuvamk

@shuvamk shuvamk commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The nginx importer refuses proxy_pass http://unix:/run/app.sock:/ outright, but migrates the same socket when it is reached through an upstream block — as the route target http://unix:/run/app.sock.

Motivation

upstream app { server unix:/run/app.sock; }
server { server_name sock.example.com; location / { proxy_pass http://app; } }

resolveProxyTarget substitutes the block's server value into the scheme without looking at it. To nginx the two spellings are one config — measured on nginx:1.27-alpine, proxy_pass http://unix:/run/app.sock and …:/ log the identical upstream: "http://unix:/run/app.sock:/foo" — so only which one the operator typed decided whether the site migrated.

Nothing downstream catches it: assertValidUpstream accepts http://unix:/… as an http URL, and openresty -t on the edge's own base image accepts proxy_pass http://unix:/run/app.sock;. But that socket is unreachable from the edge now serving it — EDGE_CONTAINER_MOUNTS mounts nothing under /run — so the host answers 502. On the takeover path the operator's nginx is already stopped, and the domain is reported as migrated.

Warn and skip is what the direct form already does, and what the caddy importer does for both of its unix-socket shapes.

Related issue

None.

Changes

  • packages/adapters — the upstream branch of resolveProxyTarget applies the existing unix-socket test to the value it resolved.
  • Regression test in proxy-import.test.ts.

Verification

# scanNginx -> registerImportedSites, real NginxProvider over a fake executor
main: registered ["sock.example.com"], warnings [], logs
      `Migrated sock.example.com → http://unix:/run/app.sock`,
      vhost gets `proxy_pass http://unix:/run/app.sock;`
PR:   registered [], warning
      `nginx: sock.example.com / — proxy_pass "http://app" resolves to upstream "app", a unix socket (skipped)`

$ npx vitest run ... -t "refuses a unix-socket upstream"
  fix reverted: FAIL (expected true to be false - the site still migrated)
  restored:     19 passed
$ bun run test -- --continue --force
  7/7 tasks, 3415 passed / 15 skipped / 0 failed (main: 3414, +1 is this test)

A 21-config corpus through scanNginx differs on exactly the two unix-socket-via-upstream cases.

Happy to migrate these instead of skipping them, if you'd rather — that needs the edge to see the socket path.

Checklist

  • One change per PR — one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped — no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it (or I explained above why there isn't one)
  • bun run test and bun run --cwd packages/adapters lint pass; both files carry pre-existing prettier drift on main, so I hand-formatted my own lines instead of running --write over them
  • I understand every line of this diff and can explain it in review

`resolveProxyTarget` refuses `proxy_pass http://unix:/run/app.sock:/` outright,
but the same socket reached through an `upstream` block was migrated. The
upstream branch substituted the block's `server` value into the scheme without
looking at it, so `unix:/run/app.sock` became the route target
`http://unix:/run/app.sock`.

Both spellings are the same config to nginx. Measured on `nginx:1.27-alpine`,
`nginx -t` is `Syntax OK` for the upstream form the operator wrote, and a
request through either `proxy_pass http://unix:/run/app.sock` or
`proxy_pass http://unix:/run/app.sock:/` logs the identical
`upstream: "http://unix:/run/app.sock:/foo"`. Only which one the operator typed
decided whether the site migrated.

    upstream app { server unix:/run/app.sock; }
    server { server_name sock.example.com; location / { proxy_pass http://app; } }

  before: sites [{ serverNames: ["sock.example.com"],
                   target: { kind: "proxy", url: "http://unix:/run/app.sock" } }]
          warnings []
  after:  sites [], warning
          `nginx: sock.example.com / — proxy_pass "http://app" resolves to upstream "app", a unix socket (skipped)`

Nothing downstream catches it. Run end to end through `scanNginx` ->
`registerImportedSites` with a real `NginxProvider` over a fake executor, `main`
returns `["sock.example.com"]`, collects no warning, writes a vhost containing
`proxy_pass http://unix:/run/app.sock;`, and logs

    Migrated sock.example.com → http://unix:/run/app.sock

`assertValidUpstream` accepts that URL as an http URL, and `openresty -t` on the
edge's own base image (`openresty/openresty:1.27.1.1-alpine`) accepts the
directive. Of the three shapes this function rejects, the unix socket is the
only one that passes `nginx -t` at all - `http://ghost` is `host not found in
upstream` and `http://$backend` is `unknown "backend" variable` - so the
function's doc comment is corrected alongside the fix.

The socket is not reachable from the edge that now serves it:
EDGE_CONTAINER_MOUNTS mounts nothing under /run, so the host answers 502. This
is the takeover path, where the operator's nginx is already stopped, and the
domain is reported as successfully migrated.

Warn and skip is what the direct form already does, and what the caddy importer
does for both of its unix-socket shapes. The upstream branch now applies the
same test to the value it resolved.

A 21-config corpus through `scanNginx` differs on exactly the two
unix-socket-via-upstream cases. The other 19 - including the direct unix form,
which `main` already refuses - are byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant