Skip to content

Commit bbc4b2f

Browse files
author
waffen29
committed
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. Close the packet connection owned by the HTTP/3 serving helper when the server is shut down, since http3.Server.Serve does not close connections provided by the caller, and an unclosed connection would make rebinding the same address fail with EADDRINUSE on a TLS reconfiguration.
1 parent 5f6cb57 commit bbc4b2f

6 files changed

Lines changed: 372 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: 88 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)
405+
406+
ln, lErr := net.Listen(network, addrStr)
407+
if lErr != nil {
408+
errs <- lErr
409+
410+
return
411+
}
384412

385-
errs <- web.httpServer.ListenAndServe()
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,38 @@ 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+
err := serveHTTP3(ctx, logger, web.httpsServer.server3, network, addrStr)
529564
if !errors.Is(err, http.ErrServerClosed) {
530565
cleanupAlways(ctx, logger, web.pidFilePath)
531566

532567
panic(fmt.Errorf("http3: %w", err))
533568
}
534569
}
535570

536-
// startPprof launches the debug and profiling server on the provided port.
537-
func startPprof(baseLogger *slog.Logger, port uint16) {
538-
addr := netip.AddrPortFrom(netutil.IPv4Localhost(), port)
571+
// serveHTTP3 listens for UDP packets on the given network and address, and
572+
// serves HTTP/3 requests on srv until it is closed. The created packet
573+
// connection is closed before returning, since [http3.Server.Serve] does not
574+
// close connections provided by the caller. logger and srv must not be nil.
575+
func serveHTTP3(
576+
ctx context.Context,
577+
logger *slog.Logger,
578+
srv *http3.Server,
579+
network string,
580+
addrStr string,
581+
) (err error) {
582+
conn, err := net.ListenPacket(network, addrStr)
583+
if err != nil {
584+
// Don't wrap the error because it's informative enough as is.
585+
return err
586+
}
587+
defer slogutil.CloseAndLog(ctx, logger, conn, slog.LevelDebug)
539588

589+
return srv.Serve(conn)
590+
}
591+
592+
// startPprof launches the debug and profiling server on the provided port on
593+
// both IPv4 and IPv6 loopback addresses.
594+
func startPprof(baseLogger *slog.Logger, port uint16) {
540595
runtime.SetBlockProfileRate(1)
541596
runtime.SetMutexProfileFraction(1)
542597

@@ -546,15 +601,26 @@ func startPprof(baseLogger *slog.Logger, port uint16) {
546601
ctx := context.Background()
547602
logger := baseLogger.With(slogutil.KeyPrefix, "pprof")
548603

549-
go func() {
550-
defer slogutil.RecoverAndLog(ctx, logger)
604+
go servePprof(ctx, logger, mux, netutil.IPv4Localhost(), port)
605+
go servePprof(ctx, logger, mux, netutil.IPv6Localhost(), port)
606+
}
551607

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-
}()
608+
// servePprof serves the pprof HTTP endpoints on the given host and port.
609+
func servePprof(
610+
ctx context.Context,
611+
logger *slog.Logger,
612+
mux *http.ServeMux,
613+
host netip.Addr,
614+
port uint16,
615+
) {
616+
defer slogutil.RecoverAndLog(ctx, logger)
617+
618+
addrStr := netip.AddrPortFrom(host, port).String()
619+
logger.InfoContext(ctx, "listening", "addr", addrStr)
620+
err := http.ListenAndServe(addrStr, mux)
621+
if !errors.Is(err, http.ErrServerClosed) {
622+
logger.ErrorContext(ctx, "shutting down", slogutil.KeyError, err)
623+
}
558624
}
559625

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

0 commit comments

Comments
 (0)