-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain_runtime_helpers.go
More file actions
349 lines (320 loc) · 10.5 KB
/
Copy pathmain_runtime_helpers.go
File metadata and controls
349 lines (320 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"crypto/tls"
"github.com/labyrinthdns/labyrinth/blocklist"
"github.com/labyrinthdns/labyrinth/cache"
"github.com/labyrinthdns/labyrinth/certmanager"
"github.com/labyrinthdns/labyrinth/config"
"github.com/labyrinthdns/labyrinth/daemon"
"github.com/labyrinthdns/labyrinth/metrics"
"github.com/labyrinthdns/labyrinth/resolver"
"github.com/labyrinthdns/labyrinth/server"
"github.com/labyrinthdns/labyrinth/web"
)
const dnsServerErrorBuffer = 4
var (
waitSignalNotify = signal.Notify
waitSignalStop = signal.Stop
)
func startHTTPServices(
ctx context.Context,
cfg *config.Config,
c *cache.Cache,
m *metrics.Metrics,
res *resolver.Resolver,
handler *server.MainHandler,
logger *slog.Logger,
blocklistMgr *blocklist.Manager,
configPath string,
) error {
// Start web dashboard (replaces standalone metrics server when enabled)
if cfg.Web.Enabled {
adminServer, err := web.NewAdminServer(cfg, c, m, res, logger, blocklistMgr)
if err != nil {
logger.Error("failed to create admin server", "error", err)
return err
}
adminServer.SetConfigPath(configPath)
// Hot-reload hook: settings that can be applied without restart.
// Anything not listed here still requires a process restart to take effect.
adminServer.SetRuntimeApplier(func(newCfg *config.Config) {
handler.SetPrivateFilter(newCfg.Security.PrivateAddressFilter)
handler.SetECSPrefixes(newCfg.Resolver.ECSEnabled, newCfg.Resolver.ECSMaxPrefix, newCfg.Resolver.ECSMaxPrefixV6)
logger.Info("config hot-applied",
"private_address_filter", newCfg.Security.PrivateAddressFilter,
"ecs_enabled", newCfg.Resolver.ECSEnabled,
"ecs_max_prefix", newCfg.Resolver.ECSMaxPrefix,
"ecs_max_prefix_v6", newCfg.Resolver.ECSMaxPrefixV6,
)
})
// Auto-TLS: create certificate manager if enabled
if cfg.Web.AutoTLS {
cm := certmanager.New(
cfg.Web.AutoTLSDomain,
cfg.Web.AutoTLSEmail,
cfg.Web.AutoTLSCacheDir,
cfg.Web.AutoTLSStaging,
logger,
)
adminServer.SetCertManager(cm)
logger.Info("auto-tls enabled",
"domain", cfg.Web.AutoTLSDomain,
"cache_dir", cfg.Web.AutoTLSCacheDir,
"staging", cfg.Web.AutoTLSStaging,
)
}
// Enable DoH endpoint if any DoH transport is configured.
if cfg.Web.DoHEnabled || cfg.Web.DoH3Enabled {
adminServer.SetDoHHandler(handler)
adminServer.SetDoHEnabled(true)
logger.Info("DoH endpoint enabled on web dashboard",
"path", "/dns-query",
"http", cfg.Web.DoHEnabled,
"http3", cfg.Web.DoH3Enabled,
)
if !cfg.Web.TLSEnabled {
logger.Warn("DoH is enabled without web TLS; terminate TLS at reverse proxy or enable web.tls_* settings")
}
if cfg.Web.DoH3Enabled {
logger.Info("DoH/HTTP3 requested; web server will advertise Alt-Svc and accept QUIC connections")
}
}
// Wire query log hook
handler.OnQuery = func(client, qname, qtype, rcode string, cached bool, durationMs float64) {
adminServer.RecordQuery(client, qname, qtype, rcode, cached, durationMs)
}
go func() {
logger.Info("web dashboard starting", "addr", cfg.Web.Addr)
if err := adminServer.Start(ctx); err != nil && ctx.Err() == nil {
logger.Error("web dashboard error", "error", err)
}
}()
// Background update checker
go adminServer.StartUpdateChecker(ctx)
// Start Zabbix agent if enabled
if cfg.Zabbix.Enabled && cfg.Zabbix.Addr != "" {
go func() {
logger.Info("zabbix agent starting", "addr", cfg.Zabbix.Addr)
web.StartZabbixAgent(ctx, cfg.Zabbix.Addr, m, c, logger)
}()
}
return nil
}
// Standalone metrics server (legacy mode)
go func() {
mux := http.NewServeMux()
mux.Handle("/metrics", m)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
stats := c.Stats()
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"healthy","cache_entries":%d,"uptime":"%s"}`,
stats.Entries, time.Since(m.StartTime()).Round(time.Second))
})
mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json")
if res.IsReady() {
fmt.Fprint(w, `{"status":"ready"}`)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprint(w, `{"status":"not ready"}`)
}
})
logger.Info("metrics server starting", "addr", cfg.Server.MetricsAddr)
// Standalone metrics server: must carry the same slowloris
// timeout regime as the admin HTTP servers, otherwise an
// attacker reaching the metrics port (which is often left
// exposed for Prometheus scrapers) can hold thousands of
// half-open connections sending one byte every few seconds
// and exhaust the resolver's file descriptors. http.ListenAndServe
// with default timeouts (=zero) is the documented Go footgun
// this defends against.
metricsSrv := &http.Server{
Addr: cfg.Server.MetricsAddr,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
// Cap header bytes to 16 KiB. Go's default is 1 MiB, which
// for a scrape/health endpoint is gratuitous: a Prometheus
// scraper sends a few hundred bytes of headers. Without the
// cap an attacker can sit just under the slowloris deadline,
// flush 900 KiB of headers per connection, and across many
// connections inflate resolver memory by orders of magnitude.
MaxHeaderBytes: 16 << 10,
}
if err := metricsSrv.ListenAndServe(); err != nil {
logger.Error("metrics server error", "error", err)
}
}()
return nil
}
func startDNSServers(
ctx context.Context,
cfg *config.Config,
handler *server.MainHandler,
logger *slog.Logger,
sharedTLSConfig ...*tls.Config,
) (chan error, error) {
errCh := make(chan error, dnsServerErrorBuffer)
udpServer, err := server.NewUDPServer(cfg.Server.ListenAddr, handler, cfg.Server.MaxUDPWorkers, logger)
if err != nil {
logger.Error("failed to start UDP server", "error", err)
return nil, err
}
go func() { errCh <- udpServer.Serve(ctx) }()
tcpServer, err := server.NewTCPServer(cfg.Server.ListenAddr, handler, cfg.Server.TCPTimeout, cfg.Server.MaxTCPConns, logger,
server.WithMaxConnsPerClient(cfg.Server.MaxTCPConnsPerClient),
server.WithPipelineMax(cfg.Server.TCPPipelineMax),
server.WithIdleTimeout(cfg.Server.TCPIdleTimeout),
)
if err != nil {
logger.Error("failed to start TCP server", "error", err)
return nil, err
}
go func() { errCh <- tcpServer.Serve(ctx) }()
// Start DoT server if enabled
if cfg.Server.DoTEnabled {
var sharedCfg *tls.Config
if len(sharedTLSConfig) > 0 && sharedTLSConfig[0] != nil {
sharedCfg = sharedTLSConfig[0]
}
switch {
case sharedCfg != nil:
// Use auto-TLS shared config
dotServer, dotErr := server.NewDoTServerWithTLSConfig(
cfg.Server.DoTListenAddr,
handler,
sharedCfg,
cfg.Server.TCPTimeout,
cfg.Server.MaxTCPConns,
cfg.Server.MaxDoTConnsPerClient,
logger,
)
if dotErr != nil {
logger.Error("failed to start DoT server with auto-TLS", "error", dotErr)
return nil, dotErr
}
go func() { errCh <- dotServer.Serve(ctx) }()
logger.Info("DoT server started (auto-TLS)", "addr", cfg.Server.DoTListenAddr)
case cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "":
dotServer, dotErr := server.NewDoTServer(
cfg.Server.DoTListenAddr,
handler,
cfg.Server.TLSCertFile,
cfg.Server.TLSKeyFile,
cfg.Server.TCPTimeout,
cfg.Server.MaxTCPConns,
cfg.Server.MaxDoTConnsPerClient,
logger,
)
if dotErr != nil {
logger.Error("failed to start DoT server", "error", dotErr)
return nil, dotErr
}
go func() { errCh <- dotServer.Serve(ctx) }()
logger.Info("DoT server started", "addr", cfg.Server.DoTListenAddr)
default:
err := fmt.Errorf("DoT enabled but no TLS certificate available (set tls_cert_file/tls_key_file or enable web.auto_tls)")
logger.Error(err.Error())
return nil, err
}
}
// Start DoQ server if enabled (RFC 9250). Shares TLS certs with DoT.
if cfg.Server.DoQEnabled {
switch {
case len(sharedTLSConfig) > 0 && sharedTLSConfig[0] != nil:
doqServer, doqErr := server.NewDoQServerWithTLSConfig(
cfg.Server.DoQListenAddr,
handler,
sharedTLSConfig[0],
cfg.Server.TCPTimeout,
cfg.Server.MaxTCPConns,
logger,
)
if doqErr != nil {
logger.Error("failed to start DoQ server with auto-TLS", "error", doqErr)
return nil, doqErr
}
go func() { errCh <- doqServer.Serve(ctx) }()
logger.Info("DoQ server started (auto-TLS)", "addr", cfg.Server.DoQListenAddr)
case cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "":
doqServer, doqErr := server.NewDoQServer(
cfg.Server.DoQListenAddr,
handler,
cfg.Server.TLSCertFile,
cfg.Server.TLSKeyFile,
cfg.Server.TCPTimeout,
cfg.Server.MaxTCPConns,
logger,
)
if doqErr != nil {
logger.Error("failed to start DoQ server", "error", doqErr)
return nil, doqErr
}
go func() { errCh <- doqServer.Serve(ctx) }()
logger.Info("DoQ server started", "addr", cfg.Server.DoQListenAddr)
default:
logger.Warn("DoQ enabled but no TLS certificate available — set tls_cert_file/tls_key_file or enable web.auto_tls")
}
}
return errCh, nil
}
func waitForShutdown(
ctx context.Context,
cancel context.CancelFunc,
cfg *config.Config,
c *cache.Cache,
daemonMode bool,
errCh <-chan error,
logger *slog.Logger,
) int {
sigCh := make(chan os.Signal, 1)
waitSignalNotify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer waitSignalStop(sigCh)
for {
select {
case sig := <-sigCh:
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
logger.Info("shutting down", "signal", sig)
cancel()
time.Sleep(cfg.Server.GracefulPeriod)
stats := c.Stats()
logger.Info("final stats", "cache_entries", stats.Entries)
// Clean up PID file if running as daemon
if daemonMode && cfg.Daemon.PIDFile != "" {
daemon.RemovePID(cfg.Daemon.PIDFile)
}
return 0
}
case err := <-errCh:
if ctx.Err() != nil {
continue
}
logger.Error("server error", "error", err)
cancel()
return 1
}
}
}