Skip to content

home: add tls.admin_listen_addr for separate https admin listener#8356

Open
Raviu56 wants to merge 1 commit intoAdguardTeam:masterfrom
Raviu56:devin/1777002459-admin-https-listen-addr
Open

home: add tls.admin_listen_addr for separate https admin listener#8356
Raviu56 wants to merge 1 commit intoAdguardTeam:masterfrom
Raviu56:devin/1777002459-admin-https-listen-addr

Conversation

@Raviu56
Copy link
Copy Markdown

@Raviu56 Raviu56 commented Apr 24, 2026

Summary

Addresses #7424 and #7598 by adding a single optional tls.admin_listen_addr YAML setting (a netip.AddrPort, e.g. 127.0.0.1:4443) that decouples the HTTPS admin UI/API from DoH while sharing the same TLS certificate.

  • Unset / zero (default) — no change. One HTTPS server on tls.port_https serves both admin UI and DoH, exactly as today. Existing installs are unaffected and DNS clients using DoH see no change.
  • Set — two HTTPS servers sharing the same TLS certificate:
    • tls.port_https serves DoH only (/dns-query, etc.). Admin UI + API are not reachable here.
    • tls.admin_listen_addr serves admin UI + API only. DoH routes are not registered on this listener.

This covers both open feature requests without risking DoH clients:

HTTPSListenAddrs (DDR SVCB advertisement) and mobileconfig / DNS-encryption display intentionally stay tied to port_https, so DNS clients see no change. HTTP/3 on port_https mirrors the HTTPS mux split, so DoH-over-HTTP/3 is not blocked by the admin auth middleware and admin routes are not exposed there.

Port conflicts involving admin_listen_addr are validated at config parse time (including --check-config), not just as a runtime bind failure. checkPortAvailability validates the admin port on the admin listener's own IP (which may differ from the web API bind host). tlsConfigChanged uses a fresh shutdown context per server so the admin server is not handed an already-cancelled context.

Scope: backend + YAML only. No frontend / UI changes. AdminListenAddr is intentionally not exposed in the JSON API (json:"-"); the field is preserved across frontend TLS-settings saves via setPrivateFieldsAndCompare. The internal/next experimental code path is not touched. Default plain-HTTP admin behavior (http.address, default :3000) is unchanged; only the force_https redirect target is updated to point at the admin port when the feature is enabled.

Example config:

tls:
  enabled: true
  port_https: 443
  admin_listen_addr: 127.0.0.1:4443
  certificate_path: /etc/adguardhome/cert.pem
  private_key_path: /etc/adguardhome/key.pem

Changes

  • New field tlsConfigSettings.AdminListenAddr netip.AddrPort (YAML: admin_listen_addr, json:"-").
  • Split the shared mux into adminMux (web UI + API + install + control handlers) and dohMux (DoH routes) when the feature is enabled; single shared mux when not.
  • Second http.Server (adminHTTPSServer) with its own lifecycle, started when admin_listen_addr is valid and non-zero; shares the cert with the DoH HTTPS server.
  • HTTP/3 (mustStartHTTP3) mirrors serveTLS mux selection + auth decision.
  • validateConfig, checkPorts, validatePorts, and checkPortAvailability all cover the admin port (and the admin port uses its own IP in binding availability checks).
  • validateTLSSettings reads AdminListenAddr from the server-side tlsConf snapshot (not the frontend payload) so UI port_https changes still trigger the admin-port conflict check.
  • force_https plain-HTTP redirect target switches to the admin port when configured.
  • CHANGELOG entry under Added.
  • New unit tests in internal/home/adminlistenaddr_internal_test.go covering the helper, port-conflict detection, config preservation across frontend updates, and mux-split routing.
  • Audited against CodeGuidelines/Go (line length ≤ 100, no naked returns, no init, lowercase log/error messages, godoc [bracket] links, explicit zero values on error returns).

Test results

Three scenarios, 14 assertions, all pass against a binary built from this branch with a shared self-signed cert.

Scenario A — feature off (admin_listen_addr: "")

# Check Expected Got
A1 GET https://:8443/ (admin UI) 302 302
A2 GET https://:8443/dns-query?… (DoH) 200 200
A3 GET https://:4444/ (admin port not bound) refused refused

Scenario B — feature on (admin_listen_addr: 127.0.0.1:4444, port_https: 8443, force_https: true)

# Check Expected Got
B1 GET https://127.0.0.1:4444/ (admin UI) 302 302
B2 GET https://127.0.0.1:4444/dns-query?… (DoH not served here) 4xx 401
B3 GET https://:8443/control/status (admin API not served here) 404 404
B4 GET https://:8443/dns-query?… (DoH still works) 200 200
B5 TLS cert fingerprint on :8443 vs :4444 byte-identical match
B6 GET http://:3000/login.html Location: header https://…:4444/login.html https://127.0.0.1:4444/login.html

Scenario C — port conflict (admin_listen_addr: 127.0.0.1:8443, port_https: 8443)

# Check Expected Got
C1 AdGuardHome --check-config exit non-zero, mentions duplicated port exit 1, validating tcp ports: duplicated values: [8443]

go test ./..., go vet ./..., and bash scripts/make/go-lint.sh (gofumpt, govulncheck, gocyclo, gocognit, ineffassign, unparam, misspell, gosec, errcheck, staticcheck) are green.

Closes #7424
Closes #7598

Add an optional tls.admin_listen_addr (netip.AddrPort) that, when set,
runs a dedicated HTTPS server for the admin UI/API on that address,
while the HTTPS server on tls.port_https serves DoH only.  Both servers
share the same TLS certificate.  When unset the previous single-server
behavior is preserved, so existing installs are unaffected and DNS
clients using DoH see no change.

Port conflicts involving admin_listen_addr are validated at config
parse time (including --check-config), not only as a runtime bind
failure.  HTTP/3 on tls.port_https mirrors the HTTPS mux split so
DoH-over-HTTP/3 clients are not forced through the admin auth
middleware and admin routes are not exposed on the DoH port.
validateTLSSettings reads AdminListenAddr from the server-side config
snapshot, not the frontend payload (the field is json:"-"), so UI
port_https changes still trigger the admin-port conflict check.
checkPortAvailability validates the admin HTTPS port on its own IP
(which may differ from the web API bind host).  tlsConfigChanged uses
a fresh shutdown context for each HTTPS server so the admin server is
not handed an already-cancelled context.  registerDoHHandlers reads
the admin listen address under config.RLock.

Backend + YAML only; no UI changes in this commit.  See the CHANGELOG
entry for user-facing details.

Addresses AdguardTeam#7424 and AdguardTeam#7598.

Co-Authored-By: raviu <raviu@protonmail.com>
@rphillips
Copy link
Copy Markdown

This PR might fix #7773 but 7773 is still a regression.

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.

https admin interface setting independent of enabling DoH Why use same https port for DNS and Web Portal

2 participants