fix(servers): support IPv6 server addresses everywhere

Store server IPs in one form (no brackets, short lower-case IPv6) and
write each address in the form its context needs:

- URLs and host:port (Sentinel, database URLs, SFTP backup download,
  DNS servers, SMTP, webhook base URLs, links, health checks) get
  brackets through formatHostForUrl().
- Traefik Host() rules use the address without brackets, so IPv6
  domains no longer return 404; saved labels are fixed at deploy.
  Caddy keeps the brackets.
- SSH keeps the plain address.
- Hetzner gives an IPv6 /64 network; IPv6-only servers now get the
  server address (prefix::1) instead of the unusable prefix::64.
- sslip labels, registry image names with an IPv6 host, the API domain
  list, allowlist matching and duplicate checks handle IPv6.

A migration normalizes stored IPv6 server addresses, repairs Hetzner
servers saved as ::64 that are not reachable, and adds brackets to
saved Sentinel URLs.
This commit is contained in:
Andras Bacsai
2026-10-09 13:50:28 +02:00
parent 17957fbb4c
commit f39c884cc7
65 changed files with 1458 additions and 107 deletions
@@ -15,6 +15,9 @@ it('accepts valid docker registry image names', function (string $imageName) {
'ghcr image' => 'ghcr.io/coollabsio/coolify',
'repository component with repeated hyphens' => 'ghcr.io/acme/my--service',
'registry with port' => 'registry.example.com:5000/team/app',
'IPv6 registry' => '[2a01:4f8::1]/team/app',
'IPv6 registry with port' => '[2a01:4f8::1]:5000/app',
'IPv4-mapped IPv6 registry' => '[::ffff:192.0.2.1]:5000/app',
'digest marker used by existing dockerimage records' => 'nginx@sha256',
]);
@@ -29,6 +32,10 @@ it('rejects docker registry image names with shell metacharacters', function (st
'newline' => "coolify/poc\nid",
'space' => 'coolify/poc image',
'tag in image-name-only field' => 'coolify/poc:latest',
'IPv6 registry with command substitution' => '[2a01:4f8::1$(id)]:5000/app',
'IPv6 registry with letters outside hex' => '[2a01:4f8::zz]/app',
'unclosed IPv6 registry' => '[2a01:4f8::1:5000/app',
'bare IPv6 registry' => '2a01:4f8::1/app',
]);
it('accepts valid docker registry image tags', function (string $tag) {
@@ -66,6 +73,7 @@ it('accepts supported full docker image reference formats', function (string $im
'image with sha256 digest' => 'nginx@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
'registry image with sha256 digest' => 'ghcr.io/user/app@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
'registry port image with tag' => 'localhost:5000/app:latest',
'IPv6 registry port image with tag' => '[2a01:4f8::1]:5000/app:latest',
]);
it('rejects unsupported full docker image reference formats', function (string $imageReference) {