Skip to content

fix: proxying to ipv6/localhost backends#49

Merged
peterldowns merged 1 commit intomainfrom
pd-bugfix-for-localhost-binding
Jul 29, 2025
Merged

fix: proxying to ipv6/localhost backends#49
peterldowns merged 1 commit intomainfrom
pd-bugfix-for-localhost-binding

Conversation

@peterldowns
Copy link
Owner

@peterldowns peterldowns commented Feb 24, 2025

Fixes #48 by explicitly using localhost:PORT as the target upstream in each reverse_proxy directive. Previously, the directive was just :PORT.

How does this work?

From testing based on the repro in that linked issue, it seems like the vite server binds to localhost:5173, which is interpreted as ipv6:

$ lsof -Pn | grep -E ':5173'
node      10213   pd   39u     IPv6 0x2fc373b77e416fe2           0t0                 TCP [::1]:5173 (LISTEN)

For whatever reason, when the caddy upstream is :5173, this means that requests fail with a 502 upstream error:

$ curl -Lvvv 'http://frontend.local'
* Host frontend.local:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:80...
* Connected to frontend.local (::1) port 80
> GET / HTTP/1.1
> Host: frontend.local
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://frontend.local/
< Server: Caddy
< Date: Tue, 29 Jul 2025 18:01:24 GMT
< Content-Length: 0
<
* Closing connection
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://frontend.local/'
* Host frontend.local:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to frontend.local (::1) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jul 29 16:55:51 2025 GMT
*  expire date: Jul 30 04:55:51 2025 GMT
*  subjectAltName: host "frontend.local" matched cert's "frontend.local"
*  issuer: CN=Localias Intermediate
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://frontend.local/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: frontend.local]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: frontend.local
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 502
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Tue, 29 Jul 2025 18:01:24 GMT
<
* Connection #1 to host frontend.local left intact

With this change, the request successfully makes it to the backend server — which then rejects it because the vite devserver requires you to add your aliases to the allowedHosts config. But at this point, localias is working correctly:

$ curl -Lvvv 'http://frontend.local'
* Host frontend.local:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:80...
* Connected to frontend.local (::1) port 80
> GET / HTTP/1.1
> Host: frontend.local
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://frontend.local/
< Server: Caddy
< Date: Tue, 29 Jul 2025 18:02:59 GMT
< Content-Length: 0
<
* Closing connection
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://frontend.local/'
* Host frontend.local:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to frontend.local (::1) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jul 29 16:55:51 2025 GMT
*  expire date: Jul 30 04:55:51 2025 GMT
*  subjectAltName: host "frontend.local" matched cert's "frontend.local"
*  issuer: CN=Localias Intermediate
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://frontend.local/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: frontend.local]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: frontend.local
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 403
< alt-svc: h3=":443"; ma=2592000
< content-type: text/plain
< date: Tue, 29 Jul 2025 18:02:59 GMT
< server: Caddy
< vary: Origin
<
Blocked request. This host ("frontend.local") is not allowed.
* Connection #1 to host frontend.local left intact
To allow this host, add "frontend.local" to `server.allowedHosts` in vite.config.js.%

Caddy docs

The caddy documentation only ever shows binding to localhost:PORT, see https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#examples. So this change seems to make sense given the upstream docs.

Caddy's documentation on Network Addresses mentions that a single port specified as :PORT is valid, but doesn't really explain the difference between localhost:PORT and :PORT

Testing

  • I tested that using localias with this change allows proxying frontend.local to the bun/vite dev server as explained above.
  • I tested that when binding to localhost, 127.0.0.1, 0.0.0.0, and ::, localias allows proxying frontend.local to the dev server.

Starting with an index.html that just says ok, and a singe alias frontend.local -> 5173:

$ localias list
frontend.local -> 5173
$ cat index.html
ok
$ python3 -m http.server 5173 --bind '::'
Serving HTTP on :: port 5173 (http://[::]:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    14288   pd    3u     IPv6 0x6829c297d33036df           0t0                 TCP *:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok
$ python3 -m http.server 5173 --bind '0.0.0.0'
Serving HTTP on 0.0.0.0 port 5173 (http://0.0.0.0:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    14744   pd    3u     IPv4 0xea957acc3c2bf73e           0t0                 TCP *:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok
$ python3 -m http.server 5173 --bind '127.0.0.1'
Serving HTTP on 127.0.0.1 port 5173 (http://127.0.0.1:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    14818   pd    3u     IPv4 0xdf4dbc378b0d9325           0t0                 TCP 127.0.0.1:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok
$ python3 -m http.server 5173 --bind 'localhost'
Serving HTTP on ::1 port 5173 (http://[::1]:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    14869   pd    5u     IPv6 0x42254720557ecbb7           0t0                 TCP [::1]:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok
$ python3 -m http.server 5173 --bind '::1'
Serving HTTP on ::1 port 5173 (http://[::1]:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    14940   pd    3u     IPv6 0x42254720557ecbb7           0t0                 TCP [::1]:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok
$ python3 -m http.server 5173
Serving HTTP on :: port 5173 (http://[::]:5173/) ...
$ lsof -Pn | grep -E ':5173'
Python    15062   pd    4u     IPv6 0x42254720557ecbb7           0t0                 TCP *:5173 (LISTEN)
$ curl -L 'https://frontend.local'
ok

```
python3 -m http.server 8080 --bind 127.0.0.1
python3 -m http.server 8080 --bind localhost
python3 -m http.server 8080 --bind ::1
python3 -m http.server 8080 --bind ::1
```

and check

```
lsof -Pn | grep -E ':8080'

localias add frontend.local 8080
localias add frontend.testing 8080
```

```
curl 'http://[::]:8080/'
curl 'http://[::1]:8080/'
curl 'http://127.0.0.1:8080/'
curl 'http://localhost:8080/'
curl 'https://frontend.local'
curl 'https://frontend.testing'
```
@peterldowns peterldowns force-pushed the pd-bugfix-for-localhost-binding branch from 0b3e4fa to d281a8a Compare July 29, 2025 17:53
@peterldowns peterldowns marked this pull request as ready for review July 29, 2025 18:19
@peterldowns peterldowns changed the title tmp: reword this later. details on how this was tested below fix: proxying to ipv6/localhost backends Jul 29, 2025
@peterldowns peterldowns merged commit c82fe0f into main Jul 29, 2025
3 checks passed
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.

Doesn't work with the vite dev server

1 participant