Skip to content

Commit 380fcdb

Browse files
authored
Add worker reading extra_records_path from file (#2271)
* consolidate scheduled tasks into one goroutine Signed-off-by: Kristoffer Dalby <[email protected]> * rename Tailcfg dns struct Signed-off-by: Kristoffer Dalby <[email protected]> * add dns.extra_records_path option Signed-off-by: Kristoffer Dalby <[email protected]> * prettier lint Signed-off-by: Kristoffer Dalby <[email protected]> * go-fmt Signed-off-by: Kristoffer Dalby <[email protected]> --------- Signed-off-by: Kristoffer Dalby <[email protected]>
1 parent 89a648c commit 380fcdb

22 files changed

+388
-81
lines changed

.github/workflows/docs-deploy.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ on:
1212
# Stable release tags
1313
- v[0-9]+.[0-9]+.[0-9]+
1414
paths:
15-
- 'docs/**'
16-
- 'mkdocs.yml'
15+
- "docs/**"
16+
- "mkdocs.yml"
1717
workflow_dispatch:
1818

1919
jobs:

.github/workflows/test-integration.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
- TestPolicyBrokenConfigCommand
4444
- TestDERPVerifyEndpoint
4545
- TestResolveMagicDNS
46+
- TestResolveMagicDNSExtraRecordsPath
4647
- TestValidateResolvConf
4748
- TestDERPServerScenario
4849
- TestDERPServerWebsocketScenario

CHANGELOG.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ When automatic migration is enabled (`map_legacy_users: true`), Headscale will f
3333
- If `strip_email_domain: true` (the default): the Headscale username matches the "username" part of their email address.
3434
- If `strip_email_domain: false`: the Headscale username matches the _whole_ email address.
3535

36-
On migration, Headscale will change the account's username to their `preferred_username`. **This could break any ACLs or policies which are configured to match by username.**
36+
On migration, Headscale will change the account's username to their `preferred_username`. **This could break any ACLs or policies which are configured to match by username.**
3737

38-
Like with Headscale v0.23.0 and earlier, this migration only works for users who haven't changed their email address since their last Headscale login.
38+
Like with Headscale v0.23.0 and earlier, this migration only works for users who haven't changed their email address since their last Headscale login.
3939

40-
A _successful_ automated migration should otherwise be transparent to users.
40+
A _successful_ automated migration should otherwise be transparent to users.
4141

42-
Once a Headscale account has been migrated, it will be _unavailable_ to be matched by the legacy process. An OIDC login with a matching username, but _non-matching_ `iss` and `sub` will instead get a _new_ Headscale account.
42+
Once a Headscale account has been migrated, it will be _unavailable_ to be matched by the legacy process. An OIDC login with a matching username, but _non-matching_ `iss` and `sub` will instead get a _new_ Headscale account.
4343

44-
Because of the way OIDC works, Headscale's automated migration process can _only_ work when a user tries to log in after the update. Mass updates would require Headscale implement a protocol like SCIM, which is **extremely** complicated and not available in all identity providers.
44+
Because of the way OIDC works, Headscale's automated migration process can _only_ work when a user tries to log in after the update. Mass updates would require Headscale implement a protocol like SCIM, which is **extremely** complicated and not available in all identity providers.
4545

46-
Administrators could also attempt to migrate users manually by editing the database, using their own mapping rules with known-good data sources.
46+
Administrators could also attempt to migrate users manually by editing the database, using their own mapping rules with known-good data sources.
4747

48-
Legacy account migration should have no effect on new installations where all users have a recorded `sub` and `iss`.
48+
Legacy account migration should have no effect on new installations where all users have a recorded `sub` and `iss`.
4949

5050
##### What happens when automatic migration is disabled?
5151

@@ -95,6 +95,7 @@ This will also affect the way you [reference users in policies](https://github.c
9595
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pr/2232)
9696
- Loosened up `server_url` and `base_domain` check. It was overly strict in some cases. [#2248](https://github.com/juanfont/headscale/pull/2248)
9797
- CLI for managing users now accepts `--identifier` in addition to `--name`, usage of `--identifier` is recommended [#2261](https://github.com/juanfont/headscale/pull/2261)
98+
- Add `dns.extra_records_path` configuration option [#2262](https://github.com/juanfont/headscale/issues/2262)
9899

99100
## 0.23.0 (2024-09-18)
100101

Dockerfile.integration

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ENV GOPATH /go
88
WORKDIR /go/src/headscale
99

1010
RUN apt-get update \
11-
&& apt-get install --no-install-recommends --yes less jq sqlite3 \
11+
&& apt-get install --no-install-recommends --yes less jq sqlite3 dnsutils \
1212
&& rm -rf /var/lib/apt/lists/* \
1313
&& apt-get clean
1414
RUN mkdir -p /var/run/headscale

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ fmt-prettier:
4444
prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
4545

4646
fmt-go:
47-
golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES)
47+
# TODO(kradalby): Reeval if we want to use 88 in the future.
48+
# golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES)
49+
gofumpt -l -w .
50+
golangci-lint run --fix
4851

4952
fmt-proto:
5053
clang-format -i $(PROTO_SOURCES)

flake.nix

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
# When updating go.mod or go.sum, a new sha will need to be calculated,
3434
# update this if you have a mismatch after doing a change to thos files.
35-
vendorHash = "sha256-OPgL2q13Hus6o9Npcp2bFiDiBZvbi/x8YVH6dU5q5fg=";
35+
vendorHash = "sha256-NyXMSIVcmPlUhE3LmEsYZQxJdz+e435r+GZC8umQKqQ=";
3636

3737
subPackages = ["cmd/headscale"];
3838

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ require (
117117
github.com/docker/go-units v0.5.0 // indirect
118118
github.com/dustin/go-humanize v1.0.1 // indirect
119119
github.com/felixge/fgprof v0.9.5 // indirect
120-
github.com/fsnotify/fsnotify v1.7.0 // indirect
120+
github.com/fsnotify/fsnotify v1.8.0 // indirect
121121
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
122122
github.com/gaissmai/bart v0.11.1 // indirect
123123
github.com/glebarez/go-sqlite v1.22.0 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
157157
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
158158
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
159159
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
160+
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
161+
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
160162
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
161163
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
162164
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=

hscontrol/app.go

+63-43
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/juanfont/headscale/hscontrol/db"
2828
"github.com/juanfont/headscale/hscontrol/derp"
2929
derpServer "github.com/juanfont/headscale/hscontrol/derp/server"
30+
"github.com/juanfont/headscale/hscontrol/dns"
3031
"github.com/juanfont/headscale/hscontrol/mapper"
3132
"github.com/juanfont/headscale/hscontrol/notifier"
3233
"github.com/juanfont/headscale/hscontrol/policy"
@@ -88,8 +89,9 @@ type Headscale struct {
8889
DERPMap *tailcfg.DERPMap
8990
DERPServer *derpServer.DERPServer
9091

91-
polManOnce sync.Once
92-
polMan policy.PolicyManager
92+
polManOnce sync.Once
93+
polMan policy.PolicyManager
94+
extraRecordMan *dns.ExtraRecordsMan
9395

9496
mapper *mapper.Mapper
9597
nodeNotifier *notifier.Notifier
@@ -184,7 +186,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
184186
}
185187
app.authProvider = authProvider
186188

187-
if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
189+
if app.cfg.TailcfgDNSConfig != nil && app.cfg.TailcfgDNSConfig.Proxied { // if MagicDNS
188190
// TODO(kradalby): revisit why this takes a list.
189191

190192
var magicDNSDomains []dnsname.FQDN
@@ -196,11 +198,11 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
196198
}
197199

198200
// we might have routes already from Split DNS
199-
if app.cfg.DNSConfig.Routes == nil {
200-
app.cfg.DNSConfig.Routes = make(map[string][]*dnstype.Resolver)
201+
if app.cfg.TailcfgDNSConfig.Routes == nil {
202+
app.cfg.TailcfgDNSConfig.Routes = make(map[string][]*dnstype.Resolver)
201203
}
202204
for _, d := range magicDNSDomains {
203-
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
205+
app.cfg.TailcfgDNSConfig.Routes[d.WithoutTrailingDot()] = nil
204206
}
205207
}
206208

@@ -237,23 +239,38 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
237239
http.Redirect(w, req, target, http.StatusFound)
238240
}
239241

240-
// expireExpiredNodes expires nodes that have an explicit expiry set
241-
// after that expiry time has passed.
242-
func (h *Headscale) expireExpiredNodes(ctx context.Context, every time.Duration) {
243-
ticker := time.NewTicker(every)
242+
func (h *Headscale) scheduledTasks(ctx context.Context) {
243+
expireTicker := time.NewTicker(updateInterval)
244+
defer expireTicker.Stop()
244245

245-
lastCheck := time.Unix(0, 0)
246-
var update types.StateUpdate
247-
var changed bool
246+
lastExpiryCheck := time.Unix(0, 0)
247+
248+
derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
249+
defer derpTicker.Stop()
250+
// If we dont want auto update, just stop the ticker
251+
if !h.cfg.DERP.AutoUpdate {
252+
derpTicker.Stop()
253+
}
254+
255+
var extraRecordsUpdate <-chan []tailcfg.DNSRecord
256+
if h.extraRecordMan != nil {
257+
extraRecordsUpdate = h.extraRecordMan.UpdateCh()
258+
} else {
259+
extraRecordsUpdate = make(chan []tailcfg.DNSRecord)
260+
}
248261

249262
for {
250263
select {
251264
case <-ctx.Done():
252-
ticker.Stop()
265+
log.Info().Caller().Msg("scheduled task worker is shutting down.")
253266
return
254-
case <-ticker.C:
267+
268+
case <-expireTicker.C:
269+
var update types.StateUpdate
270+
var changed bool
271+
255272
if err := h.db.Write(func(tx *gorm.DB) error {
256-
lastCheck, update, changed = db.ExpireExpiredNodes(tx, lastCheck)
273+
lastExpiryCheck, update, changed = db.ExpireExpiredNodes(tx, lastExpiryCheck)
257274

258275
return nil
259276
}); err != nil {
@@ -267,24 +284,8 @@ func (h *Headscale) expireExpiredNodes(ctx context.Context, every time.Duration)
267284
ctx := types.NotifyCtx(context.Background(), "expire-expired", "na")
268285
h.nodeNotifier.NotifyAll(ctx, update)
269286
}
270-
}
271-
}
272-
}
273-
274-
// scheduledDERPMapUpdateWorker refreshes the DERPMap stored on the global object
275-
// at a set interval.
276-
func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
277-
log.Info().
278-
Dur("frequency", h.cfg.DERP.UpdateFrequency).
279-
Msg("Setting up a DERPMap update worker")
280-
ticker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
281-
282-
for {
283-
select {
284-
case <-cancelChan:
285-
return
286287

287-
case <-ticker.C:
288+
case <-derpTicker.C:
288289
log.Info().Msg("Fetching DERPMap updates")
289290
h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
290291
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
@@ -297,6 +298,19 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
297298
Type: types.StateDERPUpdated,
298299
DERPMap: h.DERPMap,
299300
})
301+
302+
case records, ok := <-extraRecordsUpdate:
303+
if !ok {
304+
continue
305+
}
306+
h.cfg.TailcfgDNSConfig.ExtraRecords = records
307+
308+
ctx := types.NotifyCtx(context.Background(), "dns-extrarecord", "all")
309+
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
310+
// TODO(kradalby): We can probably do better than sending a full update here,
311+
// but for now this will ensure that all of the nodes get the new records.
312+
Type: types.StateFullUpdate,
313+
})
300314
}
301315
}
302316
}
@@ -568,12 +582,6 @@ func (h *Headscale) Serve() error {
568582
go h.DERPServer.ServeSTUN()
569583
}
570584

571-
if h.cfg.DERP.AutoUpdate {
572-
derpMapCancelChannel := make(chan struct{})
573-
defer func() { derpMapCancelChannel <- struct{}{} }()
574-
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
575-
}
576-
577585
if len(h.DERPMap.Regions) == 0 {
578586
return errEmptyInitialDERPMap
579587
}
@@ -591,9 +599,21 @@ func (h *Headscale) Serve() error {
591599
h.ephemeralGC.Schedule(node.ID, h.cfg.EphemeralNodeInactivityTimeout)
592600
}
593601

594-
expireNodeCtx, expireNodeCancel := context.WithCancel(context.Background())
595-
defer expireNodeCancel()
596-
go h.expireExpiredNodes(expireNodeCtx, updateInterval)
602+
if h.cfg.DNSConfig.ExtraRecordsPath != "" {
603+
h.extraRecordMan, err = dns.NewExtraRecordsManager(h.cfg.DNSConfig.ExtraRecordsPath)
604+
if err != nil {
605+
return fmt.Errorf("setting up extrarecord manager: %w", err)
606+
}
607+
h.cfg.TailcfgDNSConfig.ExtraRecords = h.extraRecordMan.Records()
608+
go h.extraRecordMan.Run()
609+
defer h.extraRecordMan.Close()
610+
}
611+
612+
// Start all scheduled tasks, e.g. expiring nodes, derp updates and
613+
// records updates
614+
scheduleCtx, scheduleCancel := context.WithCancel(context.Background())
615+
defer scheduleCancel()
616+
go h.scheduledTasks(scheduleCtx)
597617

598618
if zl.GlobalLevel() == zl.TraceLevel {
599619
zerolog.RespLog = true
@@ -847,7 +867,7 @@ func (h *Headscale) Serve() error {
847867
Str("signal", sig.String()).
848868
Msg("Received signal to stop, shutting down gracefully")
849869

850-
expireNodeCancel()
870+
scheduleCancel()
851871
h.ephemeralGC.Close()
852872

853873
// Gracefully shut down servers

hscontrol/auth.go

-1
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@ func (h *Headscale) handleAuthKey(
390390
http.Error(writer, "Internal server error", http.StatusInternalServerError)
391391
return
392392
}
393-
394393
}
395394

396395
err = h.db.Write(func(tx *gorm.DB) error {

hscontrol/db/db_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,5 @@ func TestConstraints(t *testing.T) {
373373

374374
tt.run(t, db.DB.Debug())
375375
})
376-
377376
}
378377
}

0 commit comments

Comments
 (0)