Skip to content

Commit afaf417

Browse files
committed
webserver: serve every configured TLS port, and DoH behind a reverse proxy
Three gaps in how the front terminator exposes TLS: 1. `split_terminator_ports()` only ever captured the *first* secure entry of `webserver.port`. Every further "...s" entry was still dropped from the list handed to CivetWeb, so nothing served it and nothing reported it. A documented configuration such as "80or,443os,8080,4443s" - the example in the `webserver.port` help text itself - silently lost 4443. The terminator now takes an array of listeners and its accept thread polls all of them, so every configured TLS port is served and reported in the API port list. Entries that would bind the same socket are collapsed first: the default "443os,[::]:443os" names one dual-stack socket and then its IPv6 half, and binding both is simply `EADDRINUSE`. A port outside 1-65535 is rejected here, as a secure entry never reaches CivetWeb's own syntax check. HTTP/3 stays on one port, the first that actually bound - Alt-Svc names the HTTP/3 port explicitly, so advertising it from the others remains correct. 2. The IPv4 and IPv6 halves of one port could not coexist. `fill_bind_addr()` maps "0.0.0.0" to the v4-mapped `::ffff:0.0.0.0`, which accepts IPv4 only, while every socket was bound dual-stack - so "0.0.0.0:443s,[::]:443s" served IPv4 only, with no way to express the split. `IPV6_V6ONLY` is now set for an explicit IPv6 literal and cleared otherwise, so the two halves bind as separate sockets. A configuration naming *only* a bracketed TLS entry, e.g. "[::]:443s", no longer answers over IPv4 through v4-mapped addresses. That is what `webserver.port` documents ("[::]:80" is IPv6 only) and what CivetWeb does for plaintext ports, and the terminator has never been in a release, so no released behavior changes. The default port list is unaffected, as it names both a bare and a bracketed entry. 3. Plaintext `/dns-query` was always refused with 426, leaving no way to put Pi-hole's DoH behind an external TLS-terminating reverse proxy. Two new settings cover that deployment: - `webserver.proxySecret` is a shared secret authenticating such a proxy. It is passed to CivetWeb as `proxy_protocol_secret`, so a PROXY protocol v2 header carrying it is believed and the client address and TLS status it announces replace the transport peer. Our own terminator uses the same secret instead of its per-boot token. Empty by default, in which case the token stays per-boot and internal. Installing it does not depend on a local TLS port, as the deployment it serves has none. - `dns.dohReverseProxy` (off by default) serves DoH on such a connection. Trust therefore rests on the proxy authenticating itself, not on the hop being assumed safe: an unauthenticated plaintext `/dns-query` is still refused with 426, so enabling this cannot expose cleartext DNS on a port reachable by anything else. An unauthenticated `X-Forwarded-For` is never consulted, as it is trivially spoofable and would let any host on that port attribute its queries to a victim, taking over their rate-limit budget and group policy. Because the announced address is authenticated, queries are attributed to the real client rather than to the proxy. Signed-off-by: DL6ER <dl6er@dl6er.de>
1 parent 0af0d74 commit afaf417

7 files changed

Lines changed: 498 additions & 118 deletions

File tree

src/api/docs/content/specs/config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ components:
299299
type: string
300300
doh:
301301
type: boolean
302+
dohReverseProxy:
303+
type: boolean
302304
dot:
303305
type: boolean
304306
blocking:
@@ -465,6 +467,8 @@ components:
465467
properties:
466468
domain:
467469
type: string
470+
proxySecret:
471+
type: string
468472
acl:
469473
type: string
470474
port:
@@ -774,6 +778,7 @@ components:
774778
upstreams: [ "127.0.0.1#5353", "8.8.8.8" ]
775779
upstreamCA: ""
776780
doh: true
781+
dohReverseProxy: false
777782
dot: true
778783
CNAMEdeepInspect: true
779784
blockESNI: true
@@ -879,6 +884,7 @@ components:
879884
expire: 365
880885
webserver:
881886
domain: pi.hole
887+
proxySecret: ''
882888
acl: "+0.0.0.0/0,::/0"
883889
port: 80,[::]:80
884890
threads: 0

src/config/config.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,13 @@ void initConfig(struct config *conf)
618618
conf->dns.doh.f = FLAG_RESTART_FTL;
619619
conf->dns.doh.c = validate_stub;
620620

621+
conf->dns.dohReverseProxy.k = "dns.dohReverseProxy";
622+
conf->dns.dohReverseProxy.h = "Serve DoH to a trusted reverse proxy that terminates TLS in front of Pi-hole (nginx, Traefik, Caddy, HAProxy, ...). The client-facing connection stays HTTPS; the proxy handles the encryption and forwards plain HTTP to Pi-hole. This requires webserver.proxySecret to be set and the proxy to send a PROXY protocol v2 header carrying that secret: only an authenticated proxy is believed, and the client address it announces is used to attribute the query. A plain HTTP request that is not authenticated this way is still refused, so enabling this alone never exposes DNS queries in cleartext.";
623+
conf->dns.dohReverseProxy.t = CONF_BOOL;
624+
conf->dns.dohReverseProxy.d.b = false;
625+
conf->dns.dohReverseProxy.f = FLAG_RESTART_FTL;
626+
conf->dns.dohReverseProxy.c = validate_stub;
627+
621628
conf->dns.dot.k = "dns.dot";
622629
conf->dns.dot.h = "Enable the inbound DNS-over-TLS (DoT) server on port 853. When enabled, FTL terminates DoT connections directly (RFC 7858) so downstream clients can use this Pi-hole as their encrypted resolver. Requires a valid TLS certificate (the same one configured for the webserver).";
623630
conf->dns.dot.t = CONF_BOOL;
@@ -1083,6 +1090,14 @@ void initConfig(struct config *conf)
10831090
conf->webserver.domain.d.s = (char*)"pi.hole";
10841091
conf->webserver.domain.c = validate_domain;
10851092

1093+
conf->webserver.proxySecret.k = "webserver.proxySecret";
1094+
conf->webserver.proxySecret.h = "Shared secret authenticating a trusted reverse proxy in front of Pi-hole. When set, FTL accepts a PROXY protocol v2 header carrying this secret and takes the client address (and TLS status) it announces in place of the transport peer, so requests forwarded by the proxy are attributed to the real client instead of to the proxy. Only a proxy that knows the secret is believed, which is why an unauthenticated X-Forwarded-For header is never trusted. Leave empty to disable; FTL then uses an internal per-boot secret for its own TLS terminator only.";
1095+
conf->webserver.proxySecret.a = cJSON_CreateStringReference("A shared secret of 32 hexadecimal characters, or an empty string");
1096+
conf->webserver.proxySecret.t = CONF_STRING;
1097+
conf->webserver.proxySecret.f = FLAG_RESTART_FTL | FLAG_WRITE_ONLY;
1098+
conf->webserver.proxySecret.d.s = (char*)"";
1099+
conf->webserver.proxySecret.c = validate_stub;
1100+
10861101
conf->webserver.acl.k = "webserver.acl";
10871102
conf->webserver.acl.h = "Webserver access control list (ACL) allowing for restrictions to be put on the list of IP addresses which have access to the web server. The ACL is a comma separated list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus sign means allow, where a minus sign means deny.\n\n If a subnet mask is omitted, such as -1.2.3.4, this means to deny only that single IP address. If this value is not set (empty string), all accesses are allowed. Otherwise, the default setting is to deny all accesses. On each request the full list is traversed, and the last (!) match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.\n\n Example 1: \"+127.0.0.1,+[::1]\" ---> deny all access, except from 127.0.0.1 and ::1\n\n Example 2: \"+192.168.0.0/16\" ---> deny all accesses, except from the 192.168.0.0/16 subnet\n\n Example 3: \"+[::]/0\" ---> allow only IPv6 access.";
10881103
conf->webserver.acl.a = cJSON_CreateStringReference("A valid ACL");

src/config/config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ struct config {
157157
struct conf_item revServers;
158158
struct conf_item upstreamCA;
159159
struct conf_item doh;
160+
struct conf_item dohReverseProxy;
160161
struct conf_item dot;
161162
struct {
162163
struct conf_item name;
@@ -263,6 +264,7 @@ struct config {
263264
struct conf_item headers;
264265
struct conf_item serve_all;
265266
struct conf_item advancedOpts;
267+
struct conf_item proxySecret;
266268
struct {
267269
struct conf_item timeout;
268270
struct conf_item restore;

0 commit comments

Comments
 (0)