Skip to content

Commit 822b00d

Browse files
waffen29claude
andcommitted
home: add IPv6 support for encrypted listeners
Preserve the configured address family of the web bind address: an explicitly configured unspecified IPv4 address now produces an IPv4-only listener, while the unspecified IPv6 address enables dual-stack listening. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5f6cb57 commit 822b00d

6 files changed

Lines changed: 266 additions & 25 deletions

File tree

internal/home/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ var config = &configuration{
465465
AuthAttempts: 5,
466466
AuthBlockMin: 15,
467467
HTTPConfig: httpConfig{
468-
Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),
468+
Address: netip.AddrPortFrom(netip.IPv6Unspecified(), 3000),
469469
SessionTTL: timeutil.Duration(30 * timeutil.Day),
470470
Pprof: &httpPprofConfig{
471471
Enabled: false,

internal/home/config_internal_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package home
22

33
import (
4+
"net/netip"
45
"os"
56
"path/filepath"
67
"testing"
78

9+
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
10+
"github.com/AdguardTeam/golibs/netutil"
811
"github.com/AdguardTeam/golibs/testutil"
912
"github.com/stretchr/testify/assert"
1013
"github.com/stretchr/testify/require"
@@ -89,3 +92,73 @@ func TestConfigFilePath(t *testing.T) {
8992
})
9093
}
9194
}
95+
96+
func TestNewServerConfig_DefaultHosts(t *testing.T) {
97+
dnsConf := &dnsConfig{
98+
BindHosts: nil,
99+
Port: 53,
100+
PendingRequests: &pendingRequests{
101+
Enabled: false,
102+
},
103+
}
104+
tlsConf := &tlsConfigSettings{}
105+
dohConf := &doHConfig{}
106+
107+
conf, err := newServerConfig(
108+
dnsConf,
109+
&clientSourcesConfig{},
110+
tlsConf,
111+
dohConf,
112+
&tlsManager{},
113+
&aghtest.Registrar{},
114+
nil, // clientsContainer
115+
&aghtest.ConfigModifier{},
116+
)
117+
require.NoError(t, err)
118+
require.Len(t, conf.UDPListenAddrs, 2)
119+
120+
assert.Equal(t, netutil.IPv4Localhost().String(), conf.UDPListenAddrs[0].IP.String())
121+
assert.Equal(t, netutil.IPv6Localhost().String(), conf.UDPListenAddrs[1].IP.String())
122+
}
123+
124+
func TestNewServerConfig_Issue8363BindHosts(t *testing.T) {
125+
bindHosts := []netip.Addr{
126+
netip.IPv4Unspecified(),
127+
netip.IPv6Unspecified(),
128+
netutil.IPv4Localhost(),
129+
netutil.IPv6Localhost(),
130+
}
131+
dnsConf := &dnsConfig{
132+
BindHosts: bindHosts,
133+
Port: 53,
134+
PendingRequests: &pendingRequests{
135+
Enabled: false,
136+
},
137+
}
138+
tlsConf := &tlsConfigSettings{
139+
Enabled: true,
140+
PortDNSOverTLS: 853,
141+
PortDNSOverQUIC: 853,
142+
CertificateChainData: requireReadFile(t, testCertificatePath),
143+
PrivateKeyData: requireReadFile(t, testPrivateKeyPath),
144+
}
145+
146+
conf, err := newServerConfig(
147+
dnsConf,
148+
&clientSourcesConfig{},
149+
tlsConf,
150+
&doHConfig{},
151+
&tlsManager{},
152+
&aghtest.Registrar{},
153+
nil, // clientsContainer
154+
&aghtest.ConfigModifier{},
155+
)
156+
require.NoError(t, err)
157+
require.Len(t, conf.TLSConf.TLSListenAddrs, len(bindHosts))
158+
require.Len(t, conf.TLSConf.QUICListenAddrs, len(bindHosts))
159+
160+
for i, host := range bindHosts {
161+
assert.Equal(t, host.String(), conf.TLSConf.TLSListenAddrs[i].IP.String())
162+
assert.Equal(t, host.String(), conf.TLSConf.QUICListenAddrs[i].IP.String())
163+
}
164+
}

internal/home/control.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err
7171
// tlsMgr must not be nil.
7272
func collectDNSAddresses(tlsMgr *tlsManager) (addrs []string, err error) {
7373
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
74-
addrs = appendDNSAddrs(addrs, netutil.IPv4Localhost())
74+
addrs = appendDNSAddrs(addrs, netutil.IPv4Localhost(), netutil.IPv6Localhost())
7575
} else {
7676
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
7777
if err != nil {

internal/home/dns.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ func newServerConfig(
264264
clientsContainer dnsforward.ClientsContainer,
265265
confModifier agh.ConfigModifier,
266266
) (newConf *dnsforward.ServerConfig, err error) {
267-
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
267+
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{
268+
netutil.IPv4Localhost(),
269+
netutil.IPv6Localhost(),
270+
})
268271

269272
fwdConf := dnsConf.Config
270273
fwdConf.ClientsContainer = clientsContainer

internal/home/web.go

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io/fs"
88
"log/slog"
9+
"net"
910
"net/http"
1011
"net/netip"
1112
"runtime"
@@ -345,6 +346,24 @@ func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *tlsConfigSetti
345346
// loggerKeyServer is the key used by [webAPI] to identify servers.
346347
const loggerKeyServer = "server"
347348

349+
// getBindAddr returns the network and address strings to use when creating a
350+
// listener on addr and port. network must be either "tcp" or "udp". The
351+
// address family of addr is preserved: for the unspecified IPv4 address the
352+
// IPv4-only network is returned, since Go's wildcard listeners otherwise
353+
// accept connections of both address families on platforms that support
354+
// IPv4-mapped IPv6 addresses. For the unspecified IPv6 address the returned
355+
// address is in the ":port" form, which enables dual-stack listening.
356+
func getBindAddr(network string, addr netip.Addr, port uint16) (listenNetwork, addrStr string) {
357+
switch {
358+
case !addr.IsUnspecified():
359+
return network, netip.AddrPortFrom(addr, port).String()
360+
case addr.Is4():
361+
return network + "4", netip.AddrPortFrom(addr, port).String()
362+
default:
363+
return network, netutil.JoinHostPort("", port)
364+
}
365+
}
366+
348367
// start starts serving HTTP requests.
349368
func (web *webAPI) start(ctx context.Context) {
350369
defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)
@@ -367,9 +386,11 @@ func (web *webAPI) start(ctx context.Context) {
367386
protocols.SetUnencryptedHTTP2(true)
368387
protocols.SetHTTP1(true)
369388

389+
network, addrStr := getBindAddr("tcp", web.conf.BindAddr.Addr(), web.conf.BindAddr.Port())
390+
370391
// Create a new instance, because the Web is not usable after Shutdown.
371392
web.httpServer = &http.Server{
372-
Addr: web.conf.BindAddr.String(),
393+
Addr: addrStr,
373394
Handler: hdlr,
374395
ReadTimeout: web.conf.ReadTimeout,
375396
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
@@ -380,9 +401,16 @@ func (web *webAPI) start(ctx context.Context) {
380401
go func() {
381402
defer slogutil.RecoverAndLog(ctx, logger)
382403

383-
logger.InfoContext(ctx, "starting plain server", "addr", web.httpServer.Addr)
404+
logger.InfoContext(ctx, "starting plain server", "addr", addrStr)
384405

385-
errs <- web.httpServer.ListenAndServe()
406+
ln, lErr := net.Listen(network, addrStr)
407+
if lErr != nil {
408+
errs <- lErr
409+
410+
return
411+
}
412+
413+
errs <- web.httpServer.Serve(ln)
386414
}()
387415

388416
err := <-errs
@@ -463,13 +491,13 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
463491
portHTTPS = config.TLS.PortHTTPS
464492
}()
465493

466-
addr := netip.AddrPortFrom(web.conf.BindAddr.Addr(), portHTTPS).String()
494+
network, addrStr := getBindAddr("tcp", web.conf.BindAddr.Addr(), portHTTPS)
467495
logger := web.baseLogger.With(loggerKeyServer, "https")
468496

469497
hdlr := web.wrapMux(logger)
470498

471499
web.httpsServer.server = &http.Server{
472-
Addr: addr,
500+
Addr: addrStr,
473501
Handler: hdlr,
474502
// TODO(m.kazantsev): Do not create TLS config manually, but use
475503
// [aghtls.TLSConfigProvider].
@@ -488,11 +516,15 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
488516
printHTTPAddresses(ctx, web.logger, urlutil.SchemeHTTPS, web.tlsManager)
489517

490518
if web.conf.serveHTTP3 {
491-
go web.mustStartHTTP3(ctx, addr)
519+
go web.mustStartHTTP3(ctx, portHTTPS)
492520
}
493521

494522
logger.InfoContext(ctx, "starting https server")
495-
err := web.httpsServer.server.ListenAndServeTLS("", "")
523+
ln, err := net.Listen(network, addrStr)
524+
if err == nil {
525+
err = web.httpsServer.server.ServeTLS(ln, "", "")
526+
}
527+
496528
if !errors.Is(err, http.ErrServerClosed) {
497529
cleanupAlways(ctx, logger, web.pidFilePath)
498530

@@ -502,17 +534,20 @@ func (web *webAPI) serveTLS(ctx context.Context) (next bool) {
502534
return true
503535
}
504536

505-
// mustStartHTTP3 initializes and starts HTTP3 server.
506-
func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
537+
// mustStartHTTP3 initializes and starts HTTP3 server on the configured bind
538+
// address with the given port.
539+
func (web *webAPI) mustStartHTTP3(ctx context.Context, port uint16) {
507540
defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)
508541

509542
logger := web.baseLogger.With(loggerKeyServer, "http3")
510543
hdlr := web.wrapMux(logger)
511544

545+
network, addrStr := getBindAddr("udp", web.conf.BindAddr.Addr(), port)
546+
512547
web.httpsServer.server3 = &http3.Server{
513548
// TODO(a.garipov): See if there is a way to use the error log as
514549
// well as timeouts here.
515-
Addr: address,
550+
Addr: addrStr,
516551
// TODO(m.kazantsev): Do not create TLS config manually, but use
517552
// [aghtls.TLSConfigProvider].
518553
TLSConfig: &tls.Config{
@@ -525,18 +560,21 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
525560
}
526561

527562
web.logger.DebugContext(ctx, "starting http/3 server")
528-
err := web.httpsServer.server3.ListenAndServe()
563+
conn, err := net.ListenPacket(network, addrStr)
564+
if err == nil {
565+
err = web.httpsServer.server3.Serve(conn)
566+
}
567+
529568
if !errors.Is(err, http.ErrServerClosed) {
530569
cleanupAlways(ctx, logger, web.pidFilePath)
531570

532571
panic(fmt.Errorf("http3: %w", err))
533572
}
534573
}
535574

536-
// startPprof launches the debug and profiling server on the provided port.
575+
// startPprof launches the debug and profiling server on the provided port on
576+
// both IPv4 and IPv6 loopback addresses.
537577
func startPprof(baseLogger *slog.Logger, port uint16) {
538-
addr := netip.AddrPortFrom(netutil.IPv4Localhost(), port)
539-
540578
runtime.SetBlockProfileRate(1)
541579
runtime.SetMutexProfileFraction(1)
542580

@@ -546,15 +584,26 @@ func startPprof(baseLogger *slog.Logger, port uint16) {
546584
ctx := context.Background()
547585
logger := baseLogger.With(slogutil.KeyPrefix, "pprof")
548586

549-
go func() {
550-
defer slogutil.RecoverAndLog(ctx, logger)
587+
go servePprof(ctx, logger, mux, netutil.IPv4Localhost(), port)
588+
go servePprof(ctx, logger, mux, netutil.IPv6Localhost(), port)
589+
}
551590

552-
logger.InfoContext(ctx, "listening", "addr", addr)
553-
err := http.ListenAndServe(addr.String(), mux)
554-
if !errors.Is(err, http.ErrServerClosed) {
555-
logger.ErrorContext(ctx, "shutting down", slogutil.KeyError, err)
556-
}
557-
}()
591+
// servePprof serves the pprof HTTP endpoints on the given host and port.
592+
func servePprof(
593+
ctx context.Context,
594+
logger *slog.Logger,
595+
mux *http.ServeMux,
596+
host netip.Addr,
597+
port uint16,
598+
) {
599+
defer slogutil.RecoverAndLog(ctx, logger)
600+
601+
addrStr := netip.AddrPortFrom(host, port).String()
602+
logger.InfoContext(ctx, "listening", "addr", addrStr)
603+
err := http.ListenAndServe(addrStr, mux)
604+
if !errors.Is(err, http.ErrServerClosed) {
605+
logger.ErrorContext(ctx, "shutting down", slogutil.KeyError, err)
606+
}
558607
}
559608

560609
// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.

0 commit comments

Comments
 (0)