2023-05-10 09:24:05 +02:00
|
|
|
package hscontrol
|
2020-06-21 12:32:08 +02:00
|
|
|
|
|
|
|
|
import (
|
2021-10-26 20:42:56 +00:00
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
2021-04-23 22:54:15 -04:00
|
|
|
"errors"
|
2020-06-21 12:32:08 +02:00
|
|
|
"fmt"
|
2025-12-08 11:39:30 -05:00
|
|
|
"io"
|
2021-10-26 20:42:56 +00:00
|
|
|
"net"
|
2021-04-23 22:54:15 -04:00
|
|
|
"net/http"
|
2024-07-18 11:08:25 +05:30
|
|
|
_ "net/http/pprof" // nolint
|
2021-02-21 23:54:15 +01:00
|
|
|
"os"
|
2021-11-02 21:46:15 +00:00
|
|
|
"os/signal"
|
2024-02-17 13:36:19 +01:00
|
|
|
"path/filepath"
|
2023-09-11 06:04:58 -05:00
|
|
|
"runtime"
|
2021-04-23 16:54:35 -04:00
|
|
|
"strings"
|
2021-02-23 21:07:52 +01:00
|
|
|
"sync"
|
2021-11-02 21:46:15 +00:00
|
|
|
"syscall"
|
hscontrol: add servertest harness for in-process control plane testing
Add a new hscontrol/servertest package that provides a test harness
for exercising the full Headscale control protocol in-process, using
Tailscale's controlclient.Direct as the client.
The harness consists of:
- TestServer: wraps a Headscale instance with an httptest.Server
- TestClient: wraps controlclient.Direct with NetworkMap tracking
- TestHarness: orchestrates N clients against a single server
- Assertion helpers for mesh completeness, visibility, and consistency
Export minimal accessor methods on Headscale (HTTPHandler, NoisePublicKey,
GetState, SetServerURL, StartBatcher, StartEphemeralGC) so the servertest
package can construct a working server from outside the hscontrol package.
This enables fast, deterministic tests of connection lifecycle, update
propagation, and network weather scenarios without Docker.
2026-03-16 09:16:43 +00:00
|
|
|
"testing"
|
2021-05-22 20:15:29 -04:00
|
|
|
"time"
|
2020-06-21 12:32:08 +02:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
"github.com/cenkalti/backoff/v5"
|
2024-05-24 09:15:34 +01:00
|
|
|
"github.com/davecgh/go-spew/spew"
|
2026-02-24 18:47:40 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
|
"github.com/go-chi/metrics"
|
2023-09-11 06:04:58 -05:00
|
|
|
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/juanfont/headscale"
|
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
2025-01-30 21:49:09 +00:00
|
|
|
"github.com/juanfont/headscale/hscontrol/capver"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/db"
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/derp"
|
|
|
|
|
derpServer "github.com/juanfont/headscale/hscontrol/derp/server"
|
2024-12-13 07:52:40 +00:00
|
|
|
"github.com/juanfont/headscale/hscontrol/dns"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/mapper"
|
2025-05-27 16:27:16 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/state"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
2025-07-28 11:15:53 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/types/change"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
2021-11-13 08:39:04 +00:00
|
|
|
zerolog "github.com/philip-bui/grpc-zerolog"
|
2024-02-08 17:28:19 +01:00
|
|
|
"github.com/pkg/profile"
|
2021-11-08 22:06:25 +00:00
|
|
|
zl "github.com/rs/zerolog"
|
2021-10-26 20:42:56 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2025-07-28 11:15:53 +02:00
|
|
|
"github.com/sasha-s/go-deadlock"
|
2021-10-03 12:26:38 -06:00
|
|
|
"golang.org/x/crypto/acme"
|
2021-04-23 22:54:15 -04:00
|
|
|
"golang.org/x/crypto/acme/autocert"
|
2021-10-26 20:42:56 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
|
"google.golang.org/grpc"
|
2021-10-29 16:45:06 +00:00
|
|
|
"google.golang.org/grpc/codes"
|
2022-02-12 17:05:30 +00:00
|
|
|
"google.golang.org/grpc/credentials"
|
2022-02-12 19:48:05 +00:00
|
|
|
"google.golang.org/grpc/credentials/insecure"
|
2021-10-29 16:45:06 +00:00
|
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
|
"google.golang.org/grpc/peer"
|
|
|
|
|
"google.golang.org/grpc/reflection"
|
|
|
|
|
"google.golang.org/grpc/status"
|
2023-12-20 21:47:48 +01:00
|
|
|
"tailscale.com/envknob"
|
2021-02-20 23:57:06 +01:00
|
|
|
"tailscale.com/tailcfg"
|
2021-10-02 12:13:05 +02:00
|
|
|
"tailscale.com/types/dnstype"
|
2021-11-26 23:28:06 +00:00
|
|
|
"tailscale.com/types/key"
|
2024-04-17 07:03:06 +02:00
|
|
|
"tailscale.com/util/dnsname"
|
2020-06-21 12:32:08 +02:00
|
|
|
)
|
|
|
|
|
|
2023-05-11 09:09:18 +02:00
|
|
|
var (
|
|
|
|
|
errSTUNAddressNotSet = errors.New("STUN address not set")
|
|
|
|
|
errUnsupportedLetsEncryptChallengeType = errors.New(
|
2022-03-16 19:46:59 +01:00
|
|
|
"unknown value for Lets Encrypt challenge type",
|
|
|
|
|
)
|
2023-12-20 21:47:48 +01:00
|
|
|
errEmptyInitialDERPMap = errors.New(
|
2024-02-08 17:28:19 +01:00
|
|
|
"initial DERPMap is empty, Headscale requires at least one entry",
|
2023-12-20 21:47:48 +01:00
|
|
|
)
|
2022-03-16 19:46:59 +01:00
|
|
|
)
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
var (
|
|
|
|
|
debugDeadlock = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK")
|
|
|
|
|
debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
deadlock.Opts.Disable = !debugDeadlock
|
|
|
|
|
if debugDeadlock {
|
|
|
|
|
deadlock.Opts.DeadlockTimeout = debugDeadlockTimeout()
|
|
|
|
|
deadlock.Opts.PrintAllCurrentGoroutines = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-29 16:45:06 +00:00
|
|
|
const (
|
2023-06-06 10:41:30 +02:00
|
|
|
AuthPrefix = "Bearer "
|
2024-05-02 17:57:53 +02:00
|
|
|
updateInterval = 5 * time.Second
|
2023-06-06 10:41:30 +02:00
|
|
|
privateKeyFileMode = 0o600
|
2024-02-17 13:36:19 +01:00
|
|
|
headscaleDirPerm = 0o700
|
2021-10-29 16:45:06 +00:00
|
|
|
)
|
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Headscale represents the base app of the service.
|
2020-06-21 12:32:08 +02:00
|
|
|
type Headscale struct {
|
2023-06-06 10:23:39 +02:00
|
|
|
cfg *types.Config
|
2025-05-27 16:27:16 +02:00
|
|
|
state *state.State
|
2022-08-13 11:14:38 +02:00
|
|
|
noisePrivateKey *key.MachinePrivate
|
2024-07-18 10:01:59 +02:00
|
|
|
ephemeralGC *db.EphemeralGarbageCollector
|
2021-02-23 21:07:52 +01:00
|
|
|
|
2023-06-06 11:09:48 +02:00
|
|
|
DERPServer *derpServer.DERPServer
|
2021-10-22 16:55:14 +00:00
|
|
|
|
2026-05-18 09:21:32 +00:00
|
|
|
// realIPMiddleware is nil when cfg.TrustedProxies is empty; the
|
|
|
|
|
// router skips the mount and r.RemoteAddr stays as the TCP peer.
|
|
|
|
|
realIPMiddleware func(http.Handler) http.Handler
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
// Things that generate changes
|
2024-12-13 07:52:40 +00:00
|
|
|
extraRecordMan *dns.ExtraRecordsMan
|
2025-05-27 16:27:16 +02:00
|
|
|
authProvider AuthProvider
|
2026-03-13 13:42:42 +00:00
|
|
|
mapBatcher *mapper.Batcher
|
2022-02-28 08:06:39 +00:00
|
|
|
|
2025-09-10 15:34:16 +02:00
|
|
|
clientStreamsOpen sync.WaitGroup
|
2020-06-21 12:32:08 +02:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
var (
|
2024-05-24 09:15:34 +01:00
|
|
|
profilingEnabled = envknob.Bool("HEADSCALE_DEBUG_PROFILING_ENABLED")
|
|
|
|
|
profilingPath = envknob.String("HEADSCALE_DEBUG_PROFILING_PATH")
|
2023-12-20 21:47:48 +01:00
|
|
|
tailsqlEnabled = envknob.Bool("HEADSCALE_DEBUG_TAILSQL_ENABLED")
|
|
|
|
|
tailsqlStateDir = envknob.String("HEADSCALE_DEBUG_TAILSQL_STATE_DIR")
|
|
|
|
|
tailsqlTSKey = envknob.String("TS_AUTHKEY")
|
2024-05-24 09:15:34 +01:00
|
|
|
dumpConfig = envknob.Bool("HEADSCALE_DEBUG_DUMP_CONFIG")
|
2023-12-20 21:47:48 +01:00
|
|
|
)
|
|
|
|
|
|
2023-06-06 10:23:39 +02:00
|
|
|
func NewHeadscale(cfg *types.Config) (*Headscale, error) {
|
2024-02-18 19:31:29 +01:00
|
|
|
var err error
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
if profilingEnabled {
|
2023-09-11 06:04:58 -05:00
|
|
|
runtime.SetBlockProfileRate(1)
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-13 11:14:38 +02:00
|
|
|
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return nil, fmt.Errorf("reading or creating Noise protocol private key: %w", err)
|
2022-08-13 11:14:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
s, err := state.NewState(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init state: %w", err)
|
|
|
|
|
}
|
2022-02-28 08:06:39 +00:00
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
app := Headscale{
|
2025-09-10 15:34:16 +02:00
|
|
|
cfg: cfg,
|
|
|
|
|
noisePrivateKey: noisePrivateKey,
|
|
|
|
|
clientStreamsOpen: sync.WaitGroup{},
|
|
|
|
|
state: s,
|
2020-06-21 12:32:08 +02:00
|
|
|
}
|
2021-07-04 13:24:05 +02:00
|
|
|
|
2026-05-18 09:21:32 +00:00
|
|
|
if len(cfg.TrustedProxies) > 0 {
|
|
|
|
|
app.realIPMiddleware, err = trustedProxyRealIP(cfg.TrustedProxies)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("building trusted_proxies middleware: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
// Initialize ephemeral garbage collector
|
|
|
|
|
ephemeralGC := db.NewEphemeralGarbageCollector(func(ni types.NodeID) {
|
2025-07-05 23:30:47 +02:00
|
|
|
node, ok := app.state.GetNodeByID(ni)
|
|
|
|
|
if !ok {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Error().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed")
|
|
|
|
|
log.Debug().Caller().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed because node not found in NodeStore")
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
return
|
|
|
|
|
}
|
2023-05-11 09:09:18 +02:00
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
policyChanged, err := app.state.DeleteNode(node)
|
|
|
|
|
if err != nil {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Error().Err(err).EmbedObject(node).Msg("ephemeral node deletion failed")
|
2025-05-27 16:27:16 +02:00
|
|
|
return
|
2024-07-18 10:01:59 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
app.Change(policyChanged)
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Debug().Caller().EmbedObject(node).Msg("ephemeral node deleted because garbage collection timeout reached")
|
2025-05-27 16:27:16 +02:00
|
|
|
})
|
|
|
|
|
app.ephemeralGC = ephemeralGC
|
2024-11-26 15:16:06 +01:00
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
var authProvider AuthProvider
|
2026-02-06 21:45:32 +01:00
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
authProvider = NewAuthProviderWeb(cfg.ServerURL)
|
2021-10-18 19:27:52 +00:00
|
|
|
if cfg.OIDC.Issuer != "" {
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
oidcProvider, err := NewAuthProviderOIDC(
|
|
|
|
|
ctx,
|
2025-07-28 11:15:53 +02:00
|
|
|
&app,
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
cfg.ServerURL,
|
|
|
|
|
&cfg.OIDC,
|
|
|
|
|
)
|
2022-09-27 11:51:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
if cfg.OIDC.OnlyStartIfOIDCIsAvailable {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else {
|
|
|
|
|
log.Warn().Err(err).Msg("failed to set up OIDC provider, falling back to CLI based authentication")
|
|
|
|
|
}
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
} else {
|
|
|
|
|
authProvider = oidcProvider
|
2021-10-08 17:43:52 +08:00
|
|
|
}
|
2021-10-18 19:27:52 +00:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
app.authProvider = authProvider
|
2021-10-16 22:31:37 +08:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
if app.cfg.TailcfgDNSConfig != nil && app.cfg.TailcfgDNSConfig.Proxied { // if MagicDNS
|
2024-02-18 19:31:29 +01:00
|
|
|
// TODO(kradalby): revisit why this takes a list.
|
2024-04-17 07:03:06 +02:00
|
|
|
var magicDNSDomains []dnsname.FQDN
|
|
|
|
|
if cfg.PrefixV4 != nil {
|
2025-03-10 16:20:29 +01:00
|
|
|
magicDNSDomains = append(
|
|
|
|
|
magicDNSDomains,
|
|
|
|
|
util.GenerateIPv4DNSRootDomain(*cfg.PrefixV4)...)
|
2024-04-17 07:03:06 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
if cfg.PrefixV6 != nil {
|
2025-03-10 16:20:29 +01:00
|
|
|
magicDNSDomains = append(
|
|
|
|
|
magicDNSDomains,
|
|
|
|
|
util.GenerateIPv6DNSRootDomain(*cfg.PrefixV6)...)
|
2024-04-17 07:03:06 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-20 09:35:56 +02:00
|
|
|
// we might have routes already from Split DNS
|
2024-12-13 07:52:40 +00:00
|
|
|
if app.cfg.TailcfgDNSConfig.Routes == nil {
|
|
|
|
|
app.cfg.TailcfgDNSConfig.Routes = make(map[string][]*dnstype.Resolver)
|
2021-10-19 20:51:43 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2021-10-10 12:43:41 +02:00
|
|
|
for _, d := range magicDNSDomains {
|
2026-05-13 13:20:04 +00:00
|
|
|
// Empty non-nil slice rather than nil: tailcfg.DNSConfig.Clone
|
|
|
|
|
// and dns.Config.Clone in tailscale drop map entries whose
|
|
|
|
|
// value is nil (see tailscale.com/tailcfg/tailcfg_clone.go and
|
|
|
|
|
// tailscale.com/net/dns/dns_clone.go: `if sv == nil { continue }`).
|
|
|
|
|
// Sending nil here caused the client's wgengine LinkChange:major
|
|
|
|
|
// handler to clobber /etc/resolv.conf on every tunnel-IP rebind
|
|
|
|
|
// — the handler reapplies a Clone of lastDNSConfig and the magic
|
|
|
|
|
// DNS routes vanish, taking the resolver with them for ~6 min
|
|
|
|
|
// until the next route-changing netmap. Empty slice survives
|
|
|
|
|
// Clone and carries the same "resolve locally" semantics
|
|
|
|
|
// (tailscale.com/ipn/ipnlocal/node_backend.go:869 documents the
|
|
|
|
|
// empty-resolver Routes form for Issue 2706).
|
|
|
|
|
app.cfg.TailcfgDNSConfig.Routes[d.WithoutTrailingDot()] = []*dnstype.Resolver{}
|
2021-10-02 12:13:05 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-05 16:22:02 +01:00
|
|
|
if cfg.DERP.ServerEnabled {
|
2023-11-23 08:31:33 +01:00
|
|
|
derpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return nil, fmt.Errorf("reading or creating DERP server private key: %w", err)
|
2023-11-23 08:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if derpServerKey.Equal(*noisePrivateKey) {
|
2023-12-09 18:09:24 +01:00
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"DERP server private key and noise private key are the same: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
2023-11-23 08:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
if cfg.DERP.ServerVerifyClients {
|
|
|
|
|
t := http.DefaultTransport.(*http.Transport) //nolint:forcetypeassert
|
|
|
|
|
t.RegisterProtocol(
|
|
|
|
|
derpServer.DerpVerifyScheme,
|
|
|
|
|
derpServer.NewDERPVerifyTransport(app.handleVerifyRequest),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-21 11:29:52 +02:00
|
|
|
embeddedDERPServer, err := derpServer.NewDERPServer(
|
|
|
|
|
cfg.ServerURL,
|
2023-11-23 08:31:33 +01:00
|
|
|
key.NodePrivate(*derpServerKey),
|
2023-06-21 11:29:52 +02:00
|
|
|
&cfg.DERP,
|
|
|
|
|
)
|
2022-03-04 00:01:31 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-03-05 16:22:02 +01:00
|
|
|
app.DERPServer = embeddedDERPServer
|
2022-03-04 00:01:31 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
return &app, nil
|
2020-06-21 12:32:08 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Redirect to our TLS url.
|
2021-04-23 22:54:15 -04:00
|
|
|
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
target := h.cfg.ServerURL + req.URL.RequestURI()
|
2026-05-18 18:34:58 +00:00
|
|
|
http.Redirect(w, req, target, http.StatusFound) //nolint:gosec // G710: target prefixed by trusted ServerURL
|
2021-04-23 22:54:15 -04:00
|
|
|
}
|
|
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
func (h *Headscale) scheduledTasks(ctx context.Context) {
|
|
|
|
|
expireTicker := time.NewTicker(updateInterval)
|
|
|
|
|
defer expireTicker.Stop()
|
2023-06-21 11:29:52 +02:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
lastExpiryCheck := time.Unix(0, 0)
|
|
|
|
|
|
2025-01-23 16:16:12 +01:00
|
|
|
derpTickerChan := make(<-chan time.Time)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-01-23 16:16:12 +01:00
|
|
|
if h.cfg.DERP.AutoUpdate && h.cfg.DERP.UpdateFrequency != 0 {
|
|
|
|
|
derpTicker := time.NewTicker(h.cfg.DERP.UpdateFrequency)
|
|
|
|
|
defer derpTicker.Stop()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-01-23 16:16:12 +01:00
|
|
|
derpTickerChan = derpTicker.C
|
2024-12-13 07:52:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var extraRecordsUpdate <-chan []tailcfg.DNSRecord
|
|
|
|
|
if h.extraRecordMan != nil {
|
|
|
|
|
extraRecordsUpdate = h.extraRecordMan.UpdateCh()
|
|
|
|
|
} else {
|
|
|
|
|
extraRecordsUpdate = make(chan []tailcfg.DNSRecord)
|
|
|
|
|
}
|
2023-06-21 11:29:52 +02:00
|
|
|
|
2026-04-15 13:41:30 +00:00
|
|
|
var (
|
|
|
|
|
haProber *state.HAHealthProber
|
|
|
|
|
haHealthChan <-chan time.Time
|
|
|
|
|
)
|
|
|
|
|
if h.cfg.Node.Routes.HA.ProbeInterval > 0 {
|
|
|
|
|
haProber = state.NewHAHealthProber(
|
|
|
|
|
h.state,
|
|
|
|
|
h.cfg.Node.Routes.HA,
|
|
|
|
|
h.cfg.ServerURL,
|
|
|
|
|
h.mapBatcher.IsConnected,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
haTicker := time.NewTicker(h.cfg.Node.Routes.HA.ProbeInterval)
|
|
|
|
|
defer haTicker.Stop()
|
|
|
|
|
|
|
|
|
|
haHealthChan = haTicker.C
|
|
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
Dur("interval", h.cfg.Node.Routes.HA.ProbeInterval).
|
|
|
|
|
Dur("timeout", h.cfg.Node.Routes.HA.ProbeTimeout).
|
|
|
|
|
Msg("HA subnet router health probing enabled")
|
|
|
|
|
} else {
|
|
|
|
|
haHealthChan = make(<-chan time.Time)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 17:57:53 +02:00
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
2024-12-13 07:52:40 +00:00
|
|
|
log.Info().Caller().Msg("scheduled task worker is shutting down.")
|
2024-05-02 17:57:53 +02:00
|
|
|
return
|
2024-12-13 07:52:40 +00:00
|
|
|
|
|
|
|
|
case <-expireTicker.C:
|
2026-02-06 21:45:32 +01:00
|
|
|
var (
|
|
|
|
|
expiredNodeChanges []change.Change
|
|
|
|
|
changed bool
|
|
|
|
|
)
|
2024-12-13 07:52:40 +00:00
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
lastExpiryCheck, expiredNodeChanges, changed = h.state.ExpireExpiredNodes(lastExpiryCheck)
|
2024-02-08 17:28:19 +01:00
|
|
|
|
2024-05-02 17:57:53 +02:00
|
|
|
if changed {
|
2025-07-28 11:15:53 +02:00
|
|
|
log.Trace().Interface("changes", expiredNodeChanges).Msgf("expiring nodes")
|
2024-02-23 10:59:24 +01:00
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
// Send the changes directly since they're already in the new format
|
|
|
|
|
for _, nodeChange := range expiredNodeChanges {
|
|
|
|
|
h.Change(nodeChange)
|
|
|
|
|
}
|
2024-05-02 17:57:53 +02:00
|
|
|
}
|
2023-06-06 10:41:30 +02:00
|
|
|
|
2025-01-23 16:16:12 +01:00
|
|
|
case <-derpTickerChan:
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Info().Msg("fetching DERPMap updates")
|
2026-02-06 21:45:32 +01:00
|
|
|
|
|
|
|
|
derpMap, err := backoff.Retry(ctx, func() (*tailcfg.DERPMap, error) { //nolint:contextcheck
|
2025-08-22 10:40:38 +02:00
|
|
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
|
|
|
|
region, _ := h.DERPServer.GenerateRegion()
|
|
|
|
|
derpMap.Regions[region.RegionID] = ®ion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return derpMap, nil
|
|
|
|
|
}, backoff.WithBackOff(backoff.NewExponentialBackOff()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to build new DERPMap, retrying later")
|
|
|
|
|
continue
|
2023-06-06 10:41:30 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
h.state.SetDERPMap(derpMap)
|
2023-06-06 10:41:30 +02:00
|
|
|
|
2025-12-15 14:36:21 +00:00
|
|
|
h.Change(change.DERPMap())
|
2024-12-13 07:52:40 +00:00
|
|
|
|
|
|
|
|
case records, ok := <-extraRecordsUpdate:
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
h.cfg.TailcfgDNSConfig.ExtraRecords = records
|
|
|
|
|
|
2025-12-15 14:36:21 +00:00
|
|
|
h.Change(change.ExtraRecords())
|
2026-04-15 13:41:30 +00:00
|
|
|
|
|
|
|
|
case <-haHealthChan:
|
|
|
|
|
haProber.ProbeOnce(ctx, h.Change)
|
2022-11-25 15:11:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-29 16:45:06 +00:00
|
|
|
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
|
2025-07-28 11:15:53 +02:00
|
|
|
req any,
|
2021-10-29 16:45:06 +00:00
|
|
|
info *grpc.UnaryServerInfo,
|
2022-04-30 23:48:28 +02:00
|
|
|
handler grpc.UnaryHandler,
|
2025-07-28 11:15:53 +02:00
|
|
|
) (any, error) {
|
2021-10-29 16:45:06 +00:00
|
|
|
// Check if the request is coming from the on-server client.
|
|
|
|
|
// This is not secure, but it is to maintain maintainability
|
|
|
|
|
// with the "legacy" database-based client
|
2024-05-19 23:49:27 +02:00
|
|
|
// It is also needed for grpc-gateway to be able to connect to
|
2021-10-29 16:45:06 +00:00
|
|
|
// the server
|
2021-11-14 20:32:03 +01:00
|
|
|
client, _ := peer.FromContext(ctx)
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2021-11-13 08:36:45 +00:00
|
|
|
log.Trace().
|
|
|
|
|
Caller().
|
2021-11-14 20:32:03 +01:00
|
|
|
Str("client_address", client.Addr.String()).
|
2021-11-13 08:36:45 +00:00
|
|
|
Msg("Client is trying to authenticate")
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
meta, ok := metadata.FromIncomingContext(ctx)
|
2021-10-29 16:45:06 +00:00
|
|
|
if !ok {
|
2021-11-13 08:36:45 +00:00
|
|
|
return ctx, status.Errorf(
|
|
|
|
|
codes.InvalidArgument,
|
2026-02-05 16:29:54 +00:00
|
|
|
"retrieving metadata",
|
2021-11-13 08:36:45 +00:00
|
|
|
)
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
authHeader, ok := meta["authorization"]
|
2021-10-29 16:45:06 +00:00
|
|
|
if !ok {
|
2021-11-13 08:36:45 +00:00
|
|
|
return ctx, status.Errorf(
|
|
|
|
|
codes.Unauthenticated,
|
2026-02-05 16:29:54 +00:00
|
|
|
"authorization token not supplied",
|
2021-11-13 08:36:45 +00:00
|
|
|
)
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := authHeader[0]
|
|
|
|
|
|
2021-11-15 17:24:24 +00:00
|
|
|
if !strings.HasPrefix(token, AuthPrefix) {
|
2021-11-13 08:36:45 +00:00
|
|
|
return ctx, status.Error(
|
|
|
|
|
codes.Unauthenticated,
|
|
|
|
|
`missing "Bearer " prefix in "Authorization" header`,
|
|
|
|
|
)
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(token, AuthPrefix))
|
2022-01-25 22:11:15 +00:00
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return ctx, status.Error(codes.Internal, "validating token")
|
2022-01-25 22:11:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !valid {
|
|
|
|
|
log.Info().
|
|
|
|
|
Str("client_address", client.Addr.String()).
|
|
|
|
|
Msg("invalid token")
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2022-01-25 22:11:15 +00:00
|
|
|
return ctx, status.Error(codes.Unauthenticated, "invalid token")
|
|
|
|
|
}
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2022-01-25 22:11:15 +00:00
|
|
|
return handler(ctx, req)
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
2022-06-18 18:41:42 +02:00
|
|
|
func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(
|
2022-06-26 11:55:37 +02:00
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
2022-06-18 18:41:42 +02:00
|
|
|
) {
|
2025-10-22 19:48:07 +08:00
|
|
|
log.Trace().
|
|
|
|
|
Caller().
|
|
|
|
|
Str("client_address", req.RemoteAddr).
|
|
|
|
|
Msg("HTTP authentication invoked")
|
2022-01-25 22:11:15 +00:00
|
|
|
|
2025-10-22 19:48:07 +08:00
|
|
|
authHeader := req.Header.Get("Authorization")
|
2022-01-25 22:11:15 +00:00
|
|
|
|
2025-10-22 19:48:07 +08:00
|
|
|
writeUnauthorized := func(statusCode int) {
|
|
|
|
|
writer.WriteHeader(statusCode)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
|
|
|
|
if _, err := writer.Write([]byte("Unauthorized")); err != nil { //nolint:noinlineerr
|
2025-10-22 19:48:07 +08:00
|
|
|
log.Error().Err(err).Msg("writing HTTP response failed")
|
2022-06-26 12:21:35 +02:00
|
|
|
}
|
2025-10-22 19:48:07 +08:00
|
|
|
}
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2025-10-22 19:48:07 +08:00
|
|
|
if !strings.HasPrefix(authHeader, AuthPrefix) {
|
|
|
|
|
log.Error().
|
|
|
|
|
Caller().
|
|
|
|
|
Str("client_address", req.RemoteAddr).
|
|
|
|
|
Msg(`missing "Bearer " prefix in "Authorization" header`)
|
|
|
|
|
writeUnauthorized(http.StatusUnauthorized)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-10-22 19:48:07 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
|
|
|
|
|
if err != nil {
|
2025-10-22 16:30:25 +02:00
|
|
|
log.Info().
|
2025-09-05 16:32:46 +02:00
|
|
|
Caller().
|
|
|
|
|
Err(err).
|
2025-10-22 19:48:07 +08:00
|
|
|
Str("client_address", req.RemoteAddr).
|
|
|
|
|
Msg("failed to validate token")
|
2025-10-22 16:30:25 +02:00
|
|
|
writeUnauthorized(http.StatusUnauthorized)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-10-22 19:48:07 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !valid {
|
|
|
|
|
log.Info().
|
|
|
|
|
Str("client_address", req.RemoteAddr).
|
|
|
|
|
Msg("invalid token")
|
|
|
|
|
writeUnauthorized(http.StatusUnauthorized)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-06-18 18:41:42 +02:00
|
|
|
return
|
|
|
|
|
}
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2022-06-26 11:55:37 +02:00
|
|
|
next.ServeHTTP(writer, req)
|
2022-06-18 18:41:42 +02:00
|
|
|
})
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
2021-11-07 09:55:32 +00:00
|
|
|
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
|
|
|
|
|
// and will remove it if it is not.
|
|
|
|
|
func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
|
|
|
|
// File does not exist, all fine
|
2026-02-06 21:45:32 +01:00
|
|
|
if _, err := os.Stat(h.cfg.UnixSocket); errors.Is(err, os.ErrNotExist) { //nolint:noinlineerr
|
2021-11-07 09:55:32 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2021-11-14 16:46:09 +01:00
|
|
|
|
2021-11-07 09:55:32 +00:00
|
|
|
return os.Remove(h.cfg.UnixSocket)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 05:50:09 +00:00
|
|
|
// securityHeaders sets baseline response headers on every HTTP response:
|
|
|
|
|
// deny framing (clickjacking), forbid MIME-type sniffing, drop the Referer
|
|
|
|
|
// header on outbound navigation. Cheap defense-in-depth for HTML surfaces.
|
|
|
|
|
func securityHeaders(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
h := w.Header()
|
|
|
|
|
h.Set("X-Frame-Options", "DENY")
|
|
|
|
|
h.Set("Content-Security-Policy", "frame-ancestors 'none'")
|
|
|
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
h.Set("Referrer-Policy", "no-referrer")
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
|
|
|
|
r := chi.NewRouter()
|
|
|
|
|
r.Use(metrics.Collector(metrics.CollectorOpts{
|
|
|
|
|
Host: false,
|
|
|
|
|
Proto: true,
|
|
|
|
|
Skip: func(r *http.Request) bool {
|
|
|
|
|
return r.Method != http.MethodOptions
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
r.Use(middleware.RequestID)
|
2026-05-18 09:21:32 +00:00
|
|
|
|
|
|
|
|
if h.realIPMiddleware != nil {
|
|
|
|
|
r.Use(h.realIPMiddleware)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Use(middleware.RequestLogger(&zerologRequestLogger{}))
|
|
|
|
|
r.Use(middleware.Recoverer)
|
2026-04-17 05:50:09 +00:00
|
|
|
r.Use(securityHeaders)
|
2026-02-24 18:47:40 +00:00
|
|
|
|
|
|
|
|
r.Post(ts2021UpgradePath, h.NoiseUpgradeHandler)
|
|
|
|
|
|
|
|
|
|
r.Get("/robots.txt", h.RobotsHandler)
|
|
|
|
|
r.Get("/health", h.HealthHandler)
|
|
|
|
|
r.Get("/version", h.VersionHandler)
|
|
|
|
|
r.Get("/key", h.KeyHandler)
|
2026-02-24 18:48:57 +00:00
|
|
|
r.Get("/register/{auth_id}", h.authProvider.RegisterHandler)
|
|
|
|
|
r.Get("/auth/{auth_id}", h.authProvider.AuthHandler)
|
2022-11-04 11:26:33 +01:00
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
if provider, ok := h.authProvider.(*AuthProviderOIDC); ok {
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Get("/oidc/callback", provider.OIDCCallbackHandler)
|
2026-04-09 18:18:44 +00:00
|
|
|
r.Post("/register/confirm/{auth_id}", provider.RegisterConfirmHandler)
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Get("/apple", h.AppleConfigMessage)
|
|
|
|
|
r.Get("/apple/{platform}", h.ApplePlatformConfig)
|
|
|
|
|
r.Get("/windows", h.WindowsConfigMessage)
|
2023-05-10 10:19:16 +02:00
|
|
|
|
|
|
|
|
// TODO(kristoffer): move swagger into a package
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Get("/swagger", headscale.SwaggerUI)
|
|
|
|
|
r.Get("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1)
|
2022-02-12 13:25:27 +00:00
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Post("/verify", h.VerifyHandler)
|
2024-11-22 20:23:05 +08:00
|
|
|
|
2022-03-05 16:22:02 +01:00
|
|
|
if h.cfg.DERP.ServerEnabled {
|
2026-02-24 18:47:40 +00:00
|
|
|
r.HandleFunc("/derp", h.DERPServer.DERPHandler)
|
|
|
|
|
r.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
|
|
|
|
|
r.HandleFunc("/derp/latency-check", derpServer.DERPProbeHandler)
|
|
|
|
|
r.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap()))
|
2022-03-04 00:01:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Route("/api", func(r chi.Router) {
|
|
|
|
|
r.Use(h.httpAuthenticationMiddleware)
|
|
|
|
|
r.HandleFunc("/v1/*", grpcMux.ServeHTTP)
|
|
|
|
|
})
|
2026-04-10 12:46:19 +00:00
|
|
|
// Ping response endpoint: receives HEAD from clients responding
|
2026-05-18 18:34:58 +00:00
|
|
|
// to a [tailcfg.PingRequest]. The unguessable ping ID serves as authentication.
|
2026-04-10 12:46:19 +00:00
|
|
|
r.Head("/machine/ping-response", h.PingResponseHandler)
|
|
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
r.Get("/favicon.ico", FaviconHandler)
|
|
|
|
|
r.Get("/", BlankHandler)
|
2022-02-12 13:25:27 +00:00
|
|
|
|
2026-02-24 18:47:40 +00:00
|
|
|
return r
|
2022-02-12 13:25:27 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-10 15:35:09 +02:00
|
|
|
// Serve launches the HTTP and gRPC server service Headscale and the API.
|
2026-02-06 21:45:32 +01:00
|
|
|
//
|
|
|
|
|
//nolint:gocyclo // complex server startup function
|
2020-06-21 12:32:08 +02:00
|
|
|
func (h *Headscale) Serve() error {
|
2025-07-05 23:30:47 +02:00
|
|
|
var err error
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-02-05 12:02:32 +01:00
|
|
|
capver.CanOldCodeBeCleanedUp()
|
|
|
|
|
|
2024-05-24 09:15:34 +01:00
|
|
|
if profilingEnabled {
|
|
|
|
|
if profilingPath != "" {
|
2025-07-05 23:30:47 +02:00
|
|
|
err = os.MkdirAll(profilingPath, os.ModePerm)
|
2024-02-08 17:28:19 +01:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal().Err(err).Msg("failed to create profiling directory")
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-24 09:15:34 +01:00
|
|
|
defer profile.Start(profile.ProfilePath(profilingPath)).Stop()
|
2024-02-08 17:28:19 +01:00
|
|
|
} else {
|
|
|
|
|
defer profile.Start().Stop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-24 09:15:34 +01:00
|
|
|
if dumpConfig {
|
|
|
|
|
spew.Dump(h.cfg)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 11:47:31 +02:00
|
|
|
versionInfo := types.GetVersionInfo()
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Info().Str("version", versionInfo.Version).Str("commit", versionInfo.Commit).Msg("starting headscale")
|
2025-01-30 21:49:09 +00:00
|
|
|
log.Info().
|
2025-02-05 12:02:32 +01:00
|
|
|
Str("minimum_version", capver.TailscaleVersion(capver.MinSupportedCapabilityVersion)).
|
2025-01-30 21:49:09 +00:00
|
|
|
Msg("Clients with a lower minimum version will be rejected")
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
h.mapBatcher = mapper.NewBatcherAndMapper(h.cfg, h.state)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
h.mapBatcher.Start()
|
|
|
|
|
defer h.mapBatcher.Close()
|
2022-03-05 20:04:31 +01:00
|
|
|
|
2022-03-05 16:22:02 +01:00
|
|
|
if h.cfg.DERP.ServerEnabled {
|
2022-03-18 13:10:35 +01:00
|
|
|
// When embedded DERP is enabled we always need a STUN server
|
2022-03-16 18:45:34 +01:00
|
|
|
if h.cfg.DERP.STUNAddr == "" {
|
2022-03-15 13:22:25 +01:00
|
|
|
return errSTUNAddressNotSet
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
go h.DERPServer.ServeSTUN()
|
|
|
|
|
}
|
2023-06-06 11:09:48 +02:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("getting DERPMap: %w", err)
|
2025-08-22 10:40:38 +02:00
|
|
|
}
|
2023-06-06 11:09:48 +02:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
|
|
|
|
region, _ := h.DERPServer.GenerateRegion()
|
|
|
|
|
derpMap.Regions[region.RegionID] = ®ion
|
2022-03-05 20:04:31 +01:00
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
if len(derpMap.Regions) == 0 {
|
2023-12-09 18:09:24 +01:00
|
|
|
return errEmptyInitialDERPMap
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 10:40:38 +02:00
|
|
|
h.state.SetDERPMap(derpMap)
|
|
|
|
|
|
2024-07-18 10:01:59 +02:00
|
|
|
// Start ephemeral node garbage collector and schedule all nodes
|
|
|
|
|
// that are already in the database and ephemeral. If they are still
|
|
|
|
|
// around between restarts, they will reconnect and the GC will
|
|
|
|
|
// be cancelled.
|
|
|
|
|
go h.ephemeralGC.Start()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
ephmNodes := h.state.ListEphemeralNodes()
|
|
|
|
|
for _, node := range ephmNodes.All() {
|
2026-03-01 22:53:26 +00:00
|
|
|
h.ephemeralGC.Schedule(node.ID(), h.cfg.Node.Ephemeral.InactivityTimeout)
|
2024-07-18 10:01:59 +02:00
|
|
|
}
|
2024-05-02 17:57:53 +02:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
if h.cfg.DNSConfig.ExtraRecordsPath != "" {
|
|
|
|
|
h.extraRecordMan, err = dns.NewExtraRecordsManager(h.cfg.DNSConfig.ExtraRecordsPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("setting up extrarecord manager: %w", err)
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
h.cfg.TailcfgDNSConfig.ExtraRecords = h.extraRecordMan.Records()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
go h.extraRecordMan.Run()
|
|
|
|
|
defer h.extraRecordMan.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start all scheduled tasks, e.g. expiring nodes, derp updates and
|
|
|
|
|
// records updates
|
|
|
|
|
scheduleCtx, scheduleCancel := context.WithCancel(context.Background())
|
|
|
|
|
defer scheduleCancel()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
go h.scheduledTasks(scheduleCtx)
|
2022-02-12 13:25:27 +00:00
|
|
|
|
|
|
|
|
if zl.GlobalLevel() == zl.TraceLevel {
|
|
|
|
|
zerolog.RespLog = true
|
|
|
|
|
} else {
|
|
|
|
|
zerolog.RespLog = false
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-12 17:05:30 +00:00
|
|
|
// Prepare group for running listeners
|
|
|
|
|
errorGroup := new(errgroup.Group)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-02-12 17:05:30 +00:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2022-02-12 13:25:27 +00:00
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// Set up LOCAL listeners
|
|
|
|
|
//
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-11-07 09:55:32 +00:00
|
|
|
err = h.ensureUnixSocketIsAbsent()
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("removing old socket file: %w", err)
|
2021-11-07 09:55:32 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-17 13:36:19 +01:00
|
|
|
socketDir := filepath.Dir(h.cfg.UnixSocket)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-02-17 13:36:19 +01:00
|
|
|
err = util.EnsureDir(socketDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("setting up unix socket: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
socketListener, err := new(net.ListenConfig).Listen(context.Background(), "unix", h.cfg.UnixSocket)
|
2021-10-30 14:08:16 +00:00
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("setting up gRPC socket: %w", err)
|
2021-10-30 14:08:16 +00:00
|
|
|
}
|
|
|
|
|
|
2022-01-28 18:58:22 +00:00
|
|
|
// Change socket permissions
|
2026-02-06 21:45:32 +01:00
|
|
|
if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil { //nolint:noinlineerr
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("changing gRPC socket permission: %w", err)
|
2022-01-28 18:58:22 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-11 06:04:58 -05:00
|
|
|
grpcGatewayMux := grpcRuntime.NewServeMux()
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-10-30 14:08:16 +00:00
|
|
|
// Make the grpc-gateway connect to grpc over socket
|
2026-02-06 21:45:32 +01:00
|
|
|
grpcGatewayConn, err := grpc.Dial( //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
2021-10-30 14:08:16 +00:00
|
|
|
h.cfg.UnixSocket,
|
|
|
|
|
[]grpc.DialOption{
|
2022-02-12 19:48:05 +00:00
|
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
2023-05-11 09:09:18 +02:00
|
|
|
grpc.WithContextDialer(util.GrpcSocketDialer),
|
2021-10-30 14:08:16 +00:00
|
|
|
}...,
|
|
|
|
|
)
|
2021-10-29 16:45:06 +00:00
|
|
|
if err != nil {
|
2024-04-10 15:35:09 +02:00
|
|
|
return fmt.Errorf("setting up gRPC gateway via socket: %w", err)
|
2021-10-29 16:45:06 +00:00
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-10-29 16:45:06 +00:00
|
|
|
// Connect to the gRPC server over localhost to skip
|
|
|
|
|
// the authentication.
|
2021-11-04 22:18:55 +00:00
|
|
|
err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
|
2021-10-26 20:42:56 +00:00
|
|
|
if err != nil {
|
2024-04-10 15:35:09 +02:00
|
|
|
return fmt.Errorf("registering Headscale API service to gRPC: %w", err)
|
2021-10-26 20:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-10-31 19:52:34 +00:00
|
|
|
// Start the local gRPC server without TLS and without authentication
|
2023-12-10 15:22:59 +01:00
|
|
|
grpcSocket := grpc.NewServer(
|
|
|
|
|
// Uncomment to debug grpc communication.
|
|
|
|
|
// zerolog.UnaryInterceptor(),
|
|
|
|
|
)
|
2021-10-31 19:52:34 +00:00
|
|
|
|
2021-11-04 22:18:55 +00:00
|
|
|
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
|
2021-10-31 19:52:34 +00:00
|
|
|
reflection.Register(grpcSocket)
|
2021-10-29 16:45:06 +00:00
|
|
|
|
2022-02-12 17:05:30 +00:00
|
|
|
errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// Set up REMOTE listeners
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
tlsConfig, err := h.getTLSSettings()
|
|
|
|
|
if err != nil {
|
2024-04-10 15:35:09 +02:00
|
|
|
return fmt.Errorf("configuring TLS settings: %w", err)
|
2022-02-12 17:05:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// gRPC setup
|
|
|
|
|
//
|
|
|
|
|
|
2022-02-12 19:48:05 +00:00
|
|
|
// We are sadly not able to run gRPC and HTTPS (2.0) on the same
|
|
|
|
|
// port because the connection mux does not support matching them
|
|
|
|
|
// since they are so similar. There is multiple issues open and we
|
|
|
|
|
// can revisit this if changes:
|
|
|
|
|
// https://github.com/soheilhy/cmux/issues/68
|
|
|
|
|
// https://github.com/soheilhy/cmux/issues/91
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
var (
|
|
|
|
|
grpcServer *grpc.Server
|
|
|
|
|
grpcListener net.Listener
|
|
|
|
|
)
|
|
|
|
|
|
2022-02-13 09:08:46 +00:00
|
|
|
if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Info().Msgf("enabling remote gRPC at %s", h.cfg.GRPCAddr)
|
2022-02-12 17:05:30 +00:00
|
|
|
|
|
|
|
|
grpcOptions := []grpc.ServerOption{
|
2025-06-23 16:57:20 +02:00
|
|
|
grpc.ChainUnaryInterceptor(
|
|
|
|
|
h.grpcAuthenticationInterceptor,
|
|
|
|
|
// Uncomment to debug grpc communication.
|
|
|
|
|
// zerolog.NewUnaryServerInterceptor(),
|
2022-02-12 17:05:30 +00:00
|
|
|
),
|
2022-02-13 09:08:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tlsConfig != nil {
|
|
|
|
|
grpcOptions = append(grpcOptions,
|
|
|
|
|
grpc.Creds(credentials.NewTLS(tlsConfig)),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
log.Warn().Msg("gRPC is running without security")
|
2022-02-12 17:05:30 +00:00
|
|
|
}
|
|
|
|
|
|
2022-06-30 23:35:22 +02:00
|
|
|
grpcServer = grpc.NewServer(grpcOptions...)
|
2022-02-12 17:05:30 +00:00
|
|
|
|
|
|
|
|
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
grpcListener, err = new(net.ListenConfig).Listen(context.Background(), "tcp", h.cfg.GRPCAddr)
|
2022-02-12 17:05:30 +00:00
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("binding to TCP address: %w", err)
|
2022-02-12 17:05:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
|
2022-02-12 19:30:25 +00:00
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
Msgf("listening and serving gRPC on: %s", h.cfg.GRPCAddr)
|
2022-02-12 16:15:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// HTTP setup
|
|
|
|
|
//
|
2022-08-13 20:55:37 +02:00
|
|
|
// This is the regular router that we expose
|
2024-04-21 22:08:59 +02:00
|
|
|
// over our main Addr
|
2022-02-12 16:15:26 +00:00
|
|
|
router := h.createRouter(grpcGatewayMux)
|
|
|
|
|
|
|
|
|
|
httpServer := &http.Server{
|
|
|
|
|
Addr: h.cfg.Addr,
|
|
|
|
|
Handler: router,
|
2024-04-10 15:35:09 +02:00
|
|
|
ReadTimeout: types.HTTPTimeout,
|
|
|
|
|
|
2024-07-22 08:56:00 +02:00
|
|
|
// Long polling should not have any timeout, this is overridden
|
2024-04-10 15:35:09 +02:00
|
|
|
// further down the chain
|
|
|
|
|
WriteTimeout: types.HTTPTimeout,
|
2022-02-12 16:15:26 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-12 16:33:18 +00:00
|
|
|
var httpListener net.Listener
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-02-12 16:15:26 +00:00
|
|
|
if tlsConfig != nil {
|
|
|
|
|
httpServer.TLSConfig = tlsConfig
|
2022-02-12 16:33:18 +00:00
|
|
|
httpListener, err = tls.Listen("tcp", h.cfg.Addr, tlsConfig)
|
|
|
|
|
} else {
|
2026-02-06 21:45:32 +01:00
|
|
|
httpListener, err = new(net.ListenConfig).Listen(context.Background(), "tcp", h.cfg.Addr)
|
2022-02-12 16:33:18 +00:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-02-12 16:33:18 +00:00
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("binding to TCP address: %w", err)
|
2022-02-12 16:15:26 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-12 16:33:18 +00:00
|
|
|
errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
|
2022-02-12 13:25:27 +00:00
|
|
|
|
2021-11-13 08:36:45 +00:00
|
|
|
log.Info().
|
2022-02-12 19:30:25 +00:00
|
|
|
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2025-11-23 12:27:51 +05:30
|
|
|
// Only start debug/metrics server if address is configured
|
|
|
|
|
var debugHTTPServer *http.Server
|
2022-02-21 12:50:15 -03:00
|
|
|
|
2025-11-23 12:27:51 +05:30
|
|
|
var debugHTTPListener net.Listener
|
|
|
|
|
|
|
|
|
|
if h.cfg.MetricsAddr != "" {
|
|
|
|
|
debugHTTPListener, err = (&net.ListenConfig{}).Listen(ctx, "tcp", h.cfg.MetricsAddr)
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return fmt.Errorf("binding to TCP address: %w", err)
|
2025-11-23 12:27:51 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debugHTTPServer = h.debugHTTPServer()
|
|
|
|
|
|
|
|
|
|
errorGroup.Go(func() error { return debugHTTPServer.Serve(debugHTTPListener) })
|
|
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
Msgf("listening and serving debug and metrics on: %s", h.cfg.MetricsAddr)
|
|
|
|
|
} else {
|
|
|
|
|
log.Info().Msg("metrics server disabled (metrics_listen_addr is empty)")
|
|
|
|
|
}
|
2022-02-21 12:50:15 -03:00
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
var tailsqlContext context.Context
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
if tailsqlEnabled {
|
2024-02-09 07:27:00 +01:00
|
|
|
if h.cfg.Database.Type != types.DatabaseSqlite {
|
2026-02-06 21:45:32 +01:00
|
|
|
//nolint:gocritic // exitAfterDefer: Fatal exits during initialization before servers start
|
2024-02-09 07:27:00 +01:00
|
|
|
log.Fatal().
|
|
|
|
|
Str("type", h.cfg.Database.Type).
|
|
|
|
|
Msgf("tailsql only support %q", types.DatabaseSqlite)
|
2023-12-20 21:47:48 +01:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
if tailsqlTSKey == "" {
|
2026-02-06 21:45:32 +01:00
|
|
|
//nolint:gocritic // exitAfterDefer: Fatal exits during initialization before servers start
|
2023-12-20 21:47:48 +01:00
|
|
|
log.Fatal().Msg("tailsql requires TS_AUTHKEY to be set")
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
tailsqlContext = context.Background()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
|
|
|
|
go runTailSQLService(ctx, util.TSLogfWrapper(), tailsqlStateDir, h.cfg.Database.Sqlite.Path) //nolint:errcheck
|
2023-12-20 21:47:48 +01:00
|
|
|
}
|
|
|
|
|
|
2022-05-31 10:57:20 +02:00
|
|
|
// Handle common process-killing signals so we can gracefully shut down:
|
|
|
|
|
sigc := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(sigc,
|
|
|
|
|
syscall.SIGHUP,
|
|
|
|
|
syscall.SIGINT,
|
|
|
|
|
syscall.SIGTERM,
|
|
|
|
|
syscall.SIGQUIT,
|
|
|
|
|
syscall.SIGHUP)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-07-11 20:33:24 +02:00
|
|
|
sigFunc := func(c chan os.Signal) {
|
2022-05-31 10:57:20 +02:00
|
|
|
// Wait for a SIGINT or SIGKILL:
|
2022-05-31 13:02:23 +02:00
|
|
|
for {
|
|
|
|
|
sig := <-c
|
|
|
|
|
switch sig {
|
|
|
|
|
case syscall.SIGHUP:
|
|
|
|
|
log.Info().
|
|
|
|
|
Str("signal", sig.String()).
|
2025-07-04 09:30:51 +02:00
|
|
|
Msg("Received SIGHUP, reloading ACL policy")
|
2022-05-31 13:02:23 +02:00
|
|
|
|
2024-12-16 07:48:19 +01:00
|
|
|
if h.cfg.Policy.IsEmpty() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
changes, err := h.state.ReloadPolicy()
|
2024-11-26 15:16:06 +01:00
|
|
|
if err != nil {
|
2025-05-27 16:27:16 +02:00
|
|
|
log.Error().Err(err).Msgf("reloading policy")
|
|
|
|
|
continue
|
2024-11-26 15:16:06 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
h.Change(changes...)
|
2022-06-11 12:54:44 +01:00
|
|
|
|
2022-05-31 13:02:23 +02:00
|
|
|
default:
|
2024-09-09 14:10:22 +02:00
|
|
|
info := func(msg string) { log.Info().Msg(msg) }
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-05-31 13:02:23 +02:00
|
|
|
log.Info().
|
|
|
|
|
Str("signal", sig.String()).
|
|
|
|
|
Msg("Received signal to stop, shutting down gracefully")
|
|
|
|
|
|
2024-12-13 07:52:40 +00:00
|
|
|
scheduleCancel()
|
2024-07-18 10:01:59 +02:00
|
|
|
h.ephemeralGC.Close()
|
2024-05-02 17:57:53 +02:00
|
|
|
|
2022-05-31 13:02:23 +02:00
|
|
|
// Gracefully shut down servers
|
2025-11-23 12:27:51 +05:30
|
|
|
shutdownCtx, cancel := context.WithTimeout(
|
|
|
|
|
context.WithoutCancel(ctx),
|
2023-06-06 10:41:30 +02:00
|
|
|
types.HTTPShutdownTimeout,
|
2022-08-04 10:47:00 +02:00
|
|
|
)
|
2025-11-23 12:27:51 +05:30
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
if debugHTTPServer != nil {
|
|
|
|
|
info("shutting down debug http server")
|
|
|
|
|
|
|
|
|
|
err := debugHTTPServer.Shutdown(shutdownCtx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to shutdown prometheus http")
|
|
|
|
|
}
|
2022-06-17 10:58:22 +02:00
|
|
|
}
|
2025-11-23 12:27:51 +05:30
|
|
|
|
2024-09-09 14:10:22 +02:00
|
|
|
info("shutting down main http server")
|
2025-11-23 12:27:51 +05:30
|
|
|
|
|
|
|
|
err := httpServer.Shutdown(shutdownCtx)
|
|
|
|
|
if err != nil {
|
2024-09-09 14:10:22 +02:00
|
|
|
log.Error().Err(err).Msg("failed to shutdown http")
|
2022-06-17 10:58:22 +02:00
|
|
|
}
|
2024-05-02 13:39:19 +02:00
|
|
|
|
2025-09-10 15:34:16 +02:00
|
|
|
info("closing batcher")
|
|
|
|
|
h.mapBatcher.Close()
|
2024-09-09 14:10:22 +02:00
|
|
|
|
|
|
|
|
info("waiting for netmap stream to close")
|
2025-09-10 15:34:16 +02:00
|
|
|
h.clientStreamsOpen.Wait()
|
2024-09-09 14:10:22 +02:00
|
|
|
|
|
|
|
|
info("shutting down grpc server (socket)")
|
2022-05-31 13:02:23 +02:00
|
|
|
grpcSocket.GracefulStop()
|
|
|
|
|
|
2022-06-30 23:35:22 +02:00
|
|
|
if grpcServer != nil {
|
2024-09-09 14:10:22 +02:00
|
|
|
info("shutting down grpc server (external)")
|
2022-06-30 23:35:22 +02:00
|
|
|
grpcServer.GracefulStop()
|
|
|
|
|
grpcListener.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 21:47:48 +01:00
|
|
|
if tailsqlContext != nil {
|
2024-09-09 14:10:22 +02:00
|
|
|
info("shutting down tailsql")
|
2023-12-20 21:47:48 +01:00
|
|
|
tailsqlContext.Done()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-31 13:02:23 +02:00
|
|
|
// Close network listeners
|
2024-09-09 14:10:22 +02:00
|
|
|
info("closing network listeners")
|
2025-11-23 12:27:51 +05:30
|
|
|
|
|
|
|
|
if debugHTTPListener != nil {
|
|
|
|
|
debugHTTPListener.Close()
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-05-31 13:02:23 +02:00
|
|
|
httpListener.Close()
|
|
|
|
|
grpcGatewayConn.Close()
|
|
|
|
|
|
|
|
|
|
// Stop listening (and unlink the socket if unix type):
|
2024-09-09 14:10:22 +02:00
|
|
|
info("closing socket listener")
|
2022-05-31 13:02:23 +02:00
|
|
|
socketListener.Close()
|
|
|
|
|
|
2025-09-10 15:34:16 +02:00
|
|
|
// Close state connections
|
|
|
|
|
info("closing state and database")
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
err = h.state.Close()
|
2022-06-17 10:58:22 +02:00
|
|
|
if err != nil {
|
2025-09-10 15:34:16 +02:00
|
|
|
log.Error().Err(err).Msg("failed to close state")
|
2022-06-17 10:58:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
Msg("Headscale stopped")
|
|
|
|
|
|
2023-07-07 13:29:53 +02:00
|
|
|
return
|
2022-05-31 13:02:23 +02:00
|
|
|
}
|
2022-05-31 10:57:20 +02:00
|
|
|
}
|
2022-06-30 23:35:22 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-07-11 20:33:24 +02:00
|
|
|
errorGroup.Go(func() error {
|
|
|
|
|
sigFunc(sigc)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
2022-05-31 10:57:20 +02:00
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
return errorGroup.Wait()
|
2021-10-26 20:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
2021-11-14 17:51:34 +01:00
|
|
|
var err error
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2022-06-03 10:14:14 +02:00
|
|
|
if h.cfg.TLS.LetsEncrypt.Hostname != "" {
|
2021-04-23 22:54:15 -04:00
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
2021-11-13 08:36:45 +00:00
|
|
|
log.Warn().
|
|
|
|
|
Msg("Listening with TLS but ServerURL does not start with https://")
|
2021-04-23 22:54:15 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
certManager := autocert.Manager{
|
2021-04-23 22:54:15 -04:00
|
|
|
Prompt: autocert.AcceptTOS,
|
2022-06-03 10:14:14 +02:00
|
|
|
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
|
|
|
|
|
Cache: autocert.DirCache(h.cfg.TLS.LetsEncrypt.CacheDir),
|
2021-10-03 12:26:38 -06:00
|
|
|
Client: &acme.Client{
|
|
|
|
|
DirectoryURL: h.cfg.ACMEURL,
|
2025-12-08 11:39:30 -05:00
|
|
|
HTTPClient: &http.Client{
|
|
|
|
|
Transport: &acmeLogger{
|
|
|
|
|
rt: http.DefaultTransport,
|
|
|
|
|
},
|
|
|
|
|
},
|
2021-10-03 12:26:38 -06:00
|
|
|
},
|
|
|
|
|
Email: h.cfg.ACMEEmail,
|
2021-04-23 22:54:15 -04:00
|
|
|
}
|
2021-10-02 15:29:27 +01:00
|
|
|
|
2022-06-03 10:14:14 +02:00
|
|
|
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
|
2023-06-06 11:12:36 +02:00
|
|
|
case types.TLSALPN01ChallengeType:
|
2021-04-23 22:54:15 -04:00
|
|
|
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
|
|
|
|
// The RFC requires that the validation is done on port 443; in other words, headscale
|
2021-07-24 09:01:20 -04:00
|
|
|
// must be reachable on port 443.
|
2021-11-14 20:32:03 +01:00
|
|
|
return certManager.TLSConfig(), nil
|
2021-11-14 18:44:37 +01:00
|
|
|
|
2023-06-06 11:12:36 +02:00
|
|
|
case types.HTTP01ChallengeType:
|
2021-04-23 22:54:15 -04:00
|
|
|
// Configuration via autocert with HTTP-01. This requires listening on
|
|
|
|
|
// port 80 for the certificate validation in addition to the headscale
|
|
|
|
|
// service, which can be configured to run on any other port.
|
2022-09-04 11:47:05 +02:00
|
|
|
server := &http.Server{
|
|
|
|
|
Addr: h.cfg.TLS.LetsEncrypt.Listen,
|
|
|
|
|
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
|
2024-04-10 15:35:09 +02:00
|
|
|
ReadTimeout: types.HTTPTimeout,
|
2022-09-04 11:47:05 +02:00
|
|
|
}
|
|
|
|
|
|
2021-04-23 22:54:15 -04:00
|
|
|
go func() {
|
2022-09-26 11:33:48 +02:00
|
|
|
err := server.ListenAndServe()
|
2021-08-05 18:11:26 +01:00
|
|
|
log.Fatal().
|
2022-01-25 22:11:15 +00:00
|
|
|
Caller().
|
2022-09-04 11:47:05 +02:00
|
|
|
Err(err).
|
2021-08-05 18:11:26 +01:00
|
|
|
Msg("failed to set up a HTTP server")
|
2021-04-23 22:54:15 -04:00
|
|
|
}()
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
return certManager.TLSConfig(), nil
|
2021-11-14 18:44:37 +01:00
|
|
|
|
|
|
|
|
default:
|
2021-11-15 19:18:14 +00:00
|
|
|
return nil, errUnsupportedLetsEncryptChallengeType
|
2021-04-23 22:54:15 -04:00
|
|
|
}
|
2022-06-03 10:14:14 +02:00
|
|
|
} else if h.cfg.TLS.CertPath == "" {
|
2021-04-23 16:54:35 -04:00
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Warn().Msg("listening without TLS but ServerURL does not start with http://")
|
2021-04-23 16:54:35 -04:00
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-11-14 17:51:34 +01:00
|
|
|
return nil, err
|
2021-04-23 16:54:35 -04:00
|
|
|
} else {
|
|
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Warn().Msg("listening with TLS but ServerURL does not start with https://")
|
2021-04-23 16:54:35 -04:00
|
|
|
}
|
2022-01-29 12:59:31 -05:00
|
|
|
|
2021-11-15 18:31:52 +00:00
|
|
|
tlsConfig := &tls.Config{
|
|
|
|
|
NextProtos: []string{"http/1.1"},
|
|
|
|
|
Certificates: make([]tls.Certificate, 1),
|
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
|
}
|
2022-01-29 12:59:31 -05:00
|
|
|
|
2022-06-03 10:14:14 +02:00
|
|
|
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
|
2021-10-26 20:42:56 +00:00
|
|
|
|
|
|
|
|
return tlsConfig, err
|
2021-04-23 16:54:35 -04:00
|
|
|
}
|
2020-06-21 12:32:08 +02:00
|
|
|
}
|
2021-08-18 23:21:11 +01:00
|
|
|
|
2021-11-28 09:17:18 +00:00
|
|
|
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
|
2024-02-17 13:36:19 +01:00
|
|
|
dir := filepath.Dir(path)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2024-02-17 13:36:19 +01:00
|
|
|
err := util.EnsureDir(dir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("ensuring private key directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-28 09:17:18 +00:00
|
|
|
privateKey, err := os.ReadFile(path)
|
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Info().Str("path", path).Msg("no private key file at path, creating...")
|
2021-11-28 09:17:18 +00:00
|
|
|
|
|
|
|
|
machineKey := key.NewMachine()
|
|
|
|
|
|
|
|
|
|
machineKeyStr, err := machineKey.MarshalText()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
2026-02-05 16:29:54 +00:00
|
|
|
"converting private key to string for saving: %w",
|
2021-11-28 09:17:18 +00:00
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2021-11-28 09:17:18 +00:00
|
|
|
err = os.WriteFile(path, machineKeyStr, privateKeyFileMode)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
2026-02-05 16:29:54 +00:00
|
|
|
"saving private key to disk at path %q: %w",
|
2023-12-20 21:47:48 +01:00
|
|
|
path,
|
2021-11-28 09:17:18 +00:00
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &machineKey, nil
|
|
|
|
|
} else if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return nil, fmt.Errorf("reading private key file: %w", err)
|
2021-11-28 09:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
2022-01-28 17:23:01 +00:00
|
|
|
trimmedPrivateKey := strings.TrimSpace(string(privateKey))
|
2021-11-28 09:17:18 +00:00
|
|
|
|
|
|
|
|
var machineKey key.MachinePrivate
|
2026-02-06 21:45:32 +01:00
|
|
|
if err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil { //nolint:noinlineerr
|
2026-02-05 16:29:54 +00:00
|
|
|
return nil, fmt.Errorf("parsing private key: %w", err)
|
2021-11-28 09:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &machineKey, nil
|
|
|
|
|
}
|
2025-07-28 11:15:53 +02:00
|
|
|
|
|
|
|
|
// Change is used to send changes to nodes.
|
|
|
|
|
// All change should be enqueued here and empty will be automatically
|
|
|
|
|
// ignored.
|
2025-12-15 14:36:21 +00:00
|
|
|
func (h *Headscale) Change(cs ...change.Change) {
|
2025-07-05 23:30:47 +02:00
|
|
|
h.mapBatcher.AddWork(cs...)
|
2025-07-28 11:15:53 +02:00
|
|
|
}
|
2025-12-08 11:39:30 -05:00
|
|
|
|
2026-05-18 18:34:58 +00:00
|
|
|
// HTTPHandler returns an [http.Handler] for the [Headscale] control server.
|
hscontrol: add servertest harness for in-process control plane testing
Add a new hscontrol/servertest package that provides a test harness
for exercising the full Headscale control protocol in-process, using
Tailscale's controlclient.Direct as the client.
The harness consists of:
- TestServer: wraps a Headscale instance with an httptest.Server
- TestClient: wraps controlclient.Direct with NetworkMap tracking
- TestHarness: orchestrates N clients against a single server
- Assertion helpers for mesh completeness, visibility, and consistency
Export minimal accessor methods on Headscale (HTTPHandler, NoisePublicKey,
GetState, SetServerURL, StartBatcher, StartEphemeralGC) so the servertest
package can construct a working server from outside the hscontrol package.
This enables fast, deterministic tests of connection lifecycle, update
propagation, and network weather scenarios without Docker.
2026-03-16 09:16:43 +00:00
|
|
|
// The handler serves the Tailscale control protocol including the /key
|
|
|
|
|
// endpoint and /ts2021 Noise upgrade path.
|
|
|
|
|
func (h *Headscale) HTTPHandler() http.Handler {
|
|
|
|
|
return h.createRouter(grpcRuntime.NewServeMux())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NoisePublicKey returns the server's Noise protocol public key.
|
|
|
|
|
func (h *Headscale) NoisePublicKey() key.MachinePublic {
|
|
|
|
|
return h.noisePrivateKey.Public()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetState returns the server's state manager for programmatic access
|
|
|
|
|
// to users, nodes, policies, and other server state.
|
|
|
|
|
func (h *Headscale) GetState() *state.State {
|
|
|
|
|
return h.state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetServerURLForTest updates the server URL in the configuration.
|
|
|
|
|
// This is needed for test servers where the URL is not known until
|
|
|
|
|
// the HTTP test server starts.
|
|
|
|
|
// It panics when called outside of tests.
|
|
|
|
|
func (h *Headscale) SetServerURLForTest(tb testing.TB, url string) {
|
|
|
|
|
tb.Helper()
|
|
|
|
|
|
|
|
|
|
h.cfg.ServerURL = url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StartBatcherForTest initialises and starts the map response batcher.
|
|
|
|
|
// It registers a cleanup function on tb to stop the batcher.
|
|
|
|
|
// It panics when called outside of tests.
|
|
|
|
|
func (h *Headscale) StartBatcherForTest(tb testing.TB) {
|
|
|
|
|
tb.Helper()
|
|
|
|
|
|
|
|
|
|
h.mapBatcher = mapper.NewBatcherAndMapper(h.cfg, h.state)
|
|
|
|
|
h.mapBatcher.Start()
|
|
|
|
|
tb.Cleanup(func() { h.mapBatcher.Close() })
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 13:41:30 +00:00
|
|
|
// MapBatcher returns the map response batcher (for test use).
|
|
|
|
|
func (h *Headscale) MapBatcher() *mapper.Batcher {
|
|
|
|
|
return h.mapBatcher
|
|
|
|
|
}
|
|
|
|
|
|
hscontrol: add servertest harness for in-process control plane testing
Add a new hscontrol/servertest package that provides a test harness
for exercising the full Headscale control protocol in-process, using
Tailscale's controlclient.Direct as the client.
The harness consists of:
- TestServer: wraps a Headscale instance with an httptest.Server
- TestClient: wraps controlclient.Direct with NetworkMap tracking
- TestHarness: orchestrates N clients against a single server
- Assertion helpers for mesh completeness, visibility, and consistency
Export minimal accessor methods on Headscale (HTTPHandler, NoisePublicKey,
GetState, SetServerURL, StartBatcher, StartEphemeralGC) so the servertest
package can construct a working server from outside the hscontrol package.
This enables fast, deterministic tests of connection lifecycle, update
propagation, and network weather scenarios without Docker.
2026-03-16 09:16:43 +00:00
|
|
|
// StartEphemeralGCForTest starts the ephemeral node garbage collector.
|
|
|
|
|
// It registers a cleanup function on tb to stop the collector.
|
|
|
|
|
// It panics when called outside of tests.
|
|
|
|
|
func (h *Headscale) StartEphemeralGCForTest(tb testing.TB) {
|
|
|
|
|
tb.Helper()
|
|
|
|
|
|
|
|
|
|
go h.ephemeralGC.Start()
|
|
|
|
|
|
|
|
|
|
tb.Cleanup(func() { h.ephemeralGC.Close() })
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 11:39:30 -05:00
|
|
|
// Provide some middleware that can inspect the ACME/autocert https calls
|
|
|
|
|
// and log when things are failing.
|
|
|
|
|
type acmeLogger struct {
|
|
|
|
|
rt http.RoundTripper
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RoundTrip will log when ACME/autocert failures happen either when err != nil OR
|
|
|
|
|
// when http status codes indicate a failure has occurred.
|
|
|
|
|
func (l *acmeLogger) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
|
resp, err := l.rt.RoundTrip(req)
|
|
|
|
|
if err != nil {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Error().Err(err).Str("url", req.URL.String()).Msg("acme request failed")
|
2025-12-08 11:39:30 -05:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Error().Int("status_code", resp.StatusCode).Str("url", req.URL.String()).Bytes("body", body).Msg("acme request returned error")
|
2025-12-08 11:39:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
}
|
2026-02-24 18:47:40 +00:00
|
|
|
|
2026-05-18 18:34:58 +00:00
|
|
|
// [zerologRequestLogger] implements chi's [middleware.LogFormatter]
|
2026-02-24 18:47:40 +00:00
|
|
|
// to route HTTP request logs through zerolog.
|
|
|
|
|
type zerologRequestLogger struct{}
|
|
|
|
|
|
|
|
|
|
func (z *zerologRequestLogger) NewLogEntry(
|
|
|
|
|
r *http.Request,
|
|
|
|
|
) middleware.LogEntry {
|
|
|
|
|
return &zerologLogEntry{
|
|
|
|
|
method: r.Method,
|
|
|
|
|
path: r.URL.Path,
|
|
|
|
|
proto: r.Proto,
|
|
|
|
|
remote: r.RemoteAddr,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type zerologLogEntry struct {
|
|
|
|
|
method string
|
|
|
|
|
path string
|
|
|
|
|
proto string
|
|
|
|
|
remote string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *zerologLogEntry) Write(
|
|
|
|
|
status, bytes int,
|
|
|
|
|
header http.Header,
|
|
|
|
|
elapsed time.Duration,
|
|
|
|
|
extra any,
|
|
|
|
|
) {
|
|
|
|
|
log.Info().
|
|
|
|
|
Str("method", e.method).
|
|
|
|
|
Str("path", e.path).
|
|
|
|
|
Str("proto", e.proto).
|
|
|
|
|
Str("remote", e.remote).
|
|
|
|
|
Int("status", status).
|
|
|
|
|
Int("bytes", bytes).
|
|
|
|
|
Dur("elapsed", elapsed).
|
|
|
|
|
Msg("http request")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *zerologLogEntry) Panic(
|
|
|
|
|
v any,
|
|
|
|
|
stack []byte,
|
|
|
|
|
) {
|
|
|
|
|
log.Error().
|
|
|
|
|
Interface("panic", v).
|
|
|
|
|
Bytes("stack", stack).
|
|
|
|
|
Msg("http handler panic")
|
|
|
|
|
}
|