2020-06-21 10:32:08 +00:00
|
|
|
package headscale
|
|
|
|
|
|
|
|
import (
|
2021-10-26 20:42:56 +00:00
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
2021-04-24 02:54:15 +00:00
|
|
|
"errors"
|
2020-06-21 10:32:08 +00:00
|
|
|
"fmt"
|
2021-10-26 20:42:56 +00:00
|
|
|
"net"
|
2021-04-24 02:54:15 +00:00
|
|
|
"net/http"
|
2021-10-22 16:55:14 +00:00
|
|
|
"net/url"
|
2021-02-21 22:54:15 +00:00
|
|
|
"os"
|
2021-10-06 22:06:07 +00:00
|
|
|
"sort"
|
2021-04-23 20:54:35 +00:00
|
|
|
"strings"
|
2021-02-23 20:07:52 +00:00
|
|
|
"sync"
|
2021-05-23 00:15:29 +00:00
|
|
|
"time"
|
2020-06-21 10:32:08 +00:00
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2021-10-26 20:42:56 +00:00
|
|
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
|
|
|
apiV1 "github.com/juanfont/headscale/gen/go/v1"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/soheilhy/cmux"
|
2021-10-09 10:22:13 +00:00
|
|
|
ginprometheus "github.com/zsais/go-gin-prometheus"
|
2021-10-03 18:26:38 +00:00
|
|
|
"golang.org/x/crypto/acme"
|
2021-04-24 02:54:15 +00: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-07-04 19:40:46 +00:00
|
|
|
"gorm.io/gorm"
|
2021-08-02 19:06:26 +00:00
|
|
|
"inet.af/netaddr"
|
2021-02-20 22:57:06 +00:00
|
|
|
"tailscale.com/tailcfg"
|
2021-10-02 10:13:05 +00:00
|
|
|
"tailscale.com/types/dnstype"
|
2021-06-25 16:57:08 +00:00
|
|
|
"tailscale.com/types/wgkey"
|
2020-06-21 10:32:08 +00:00
|
|
|
)
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Config contains the initial Headscale configuration.
|
2020-06-21 10:32:08 +00:00
|
|
|
type Config struct {
|
2021-05-23 00:15:29 +00:00
|
|
|
ServerURL string
|
|
|
|
Addr string
|
|
|
|
PrivateKeyPath string
|
|
|
|
EphemeralNodeInactivityTimeout time.Duration
|
2021-08-02 19:06:26 +00:00
|
|
|
IPPrefix netaddr.IPPrefix
|
2021-10-02 09:20:42 +00:00
|
|
|
BaseDomain string
|
2020-06-21 10:32:08 +00:00
|
|
|
|
2021-10-22 16:55:14 +00:00
|
|
|
DERP DERPConfig
|
|
|
|
|
2021-05-15 12:32:26 +00:00
|
|
|
DBtype string
|
|
|
|
DBpath string
|
2020-06-21 10:32:08 +00:00
|
|
|
DBhost string
|
|
|
|
DBport int
|
|
|
|
DBname string
|
|
|
|
DBuser string
|
|
|
|
DBpass string
|
2021-04-23 20:54:35 +00:00
|
|
|
|
2021-07-23 22:12:01 +00:00
|
|
|
TLSLetsEncryptListen string
|
2021-04-24 02:54:15 +00:00
|
|
|
TLSLetsEncryptHostname string
|
|
|
|
TLSLetsEncryptCacheDir string
|
|
|
|
TLSLetsEncryptChallengeType string
|
|
|
|
|
2021-04-23 20:54:35 +00:00
|
|
|
TLSCertPath string
|
|
|
|
TLSKeyPath string
|
2021-08-24 06:09:47 +00:00
|
|
|
|
2021-10-03 18:26:38 +00:00
|
|
|
ACMEURL string
|
|
|
|
ACMEEmail string
|
|
|
|
|
2021-08-24 06:09:47 +00:00
|
|
|
DNSConfig *tailcfg.DNSConfig
|
2020-06-21 10:32:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 16:55:14 +00:00
|
|
|
type DERPConfig struct {
|
|
|
|
URLs []url.URL
|
|
|
|
Paths []string
|
|
|
|
AutoUpdate bool
|
|
|
|
UpdateFrequency time.Duration
|
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Headscale represents the base app of the service.
|
2020-06-21 10:32:08 +00:00
|
|
|
type Headscale struct {
|
|
|
|
cfg Config
|
2021-07-04 19:40:46 +00:00
|
|
|
db *gorm.DB
|
2020-06-21 10:32:08 +00:00
|
|
|
dbString string
|
2021-05-02 18:47:36 +00:00
|
|
|
dbType string
|
|
|
|
dbDebug bool
|
2021-06-25 16:57:08 +00:00
|
|
|
publicKey *wgkey.Key
|
|
|
|
privateKey *wgkey.Private
|
2021-02-23 20:07:52 +00:00
|
|
|
|
2021-10-22 16:55:14 +00:00
|
|
|
DERPMap *tailcfg.DERPMap
|
|
|
|
|
2021-07-03 15:31:32 +00:00
|
|
|
aclPolicy *ACLPolicy
|
2021-07-04 11:24:05 +00:00
|
|
|
aclRules *[]tailcfg.FilterRule
|
2021-07-03 15:31:32 +00:00
|
|
|
|
2021-08-19 17:19:26 +00:00
|
|
|
lastStateChange sync.Map
|
2020-06-21 10:32:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// NewHeadscale returns the Headscale app.
|
2020-06-21 10:32:08 +00:00
|
|
|
func NewHeadscale(cfg Config) (*Headscale, error) {
|
2021-02-21 22:54:15 +00:00
|
|
|
content, err := os.ReadFile(cfg.PrivateKeyPath)
|
2020-06-21 10:32:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-06-25 16:57:08 +00:00
|
|
|
privKey, err := wgkey.ParsePrivate(string(content))
|
2020-06-21 10:32:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pubKey := privKey.Public()
|
2021-05-15 12:32:26 +00:00
|
|
|
|
|
|
|
var dbString string
|
|
|
|
switch cfg.DBtype {
|
|
|
|
case "postgres":
|
|
|
|
dbString = fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable", cfg.DBhost,
|
|
|
|
cfg.DBport, cfg.DBname, cfg.DBuser, cfg.DBpass)
|
|
|
|
case "sqlite3":
|
|
|
|
dbString = cfg.DBpath
|
|
|
|
default:
|
2021-07-11 13:10:37 +00:00
|
|
|
return nil, errors.New("unsupported DB")
|
2021-05-15 12:32:26 +00:00
|
|
|
}
|
|
|
|
|
2020-06-21 10:32:08 +00:00
|
|
|
h := Headscale{
|
2021-05-15 12:32:26 +00:00
|
|
|
cfg: cfg,
|
|
|
|
dbType: cfg.DBtype,
|
|
|
|
dbString: dbString,
|
2020-06-21 10:32:08 +00:00
|
|
|
privateKey: privKey,
|
|
|
|
publicKey: &pubKey,
|
2021-07-04 11:24:05 +00:00
|
|
|
aclRules: &tailcfg.FilterAllowAll, // default allowall
|
2020-06-21 10:32:08 +00:00
|
|
|
}
|
2021-07-04 11:24:05 +00:00
|
|
|
|
2020-06-21 10:32:08 +00:00
|
|
|
err = h.initDB()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-04 19:40:46 +00:00
|
|
|
|
2021-10-02 10:13:05 +00:00
|
|
|
if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS
|
2021-10-09 10:22:13 +00:00
|
|
|
magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain)
|
2021-10-02 10:13:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-10-20 07:35:56 +00:00
|
|
|
// we might have routes already from Split DNS
|
2021-10-22 16:55:14 +00:00
|
|
|
if h.cfg.DNSConfig.Routes == nil {
|
2021-10-19 18:51:43 +00:00
|
|
|
h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
|
|
|
|
}
|
2021-10-10 10:43:41 +00:00
|
|
|
for _, d := range magicDNSDomains {
|
2021-10-02 10:13:05 +00:00
|
|
|
h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-21 10:32:08 +00:00
|
|
|
return &h, nil
|
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Redirect to our TLS url.
|
2021-04-24 02:54:15 +00:00
|
|
|
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
|
|
|
|
target := h.cfg.ServerURL + req.URL.RequestURI()
|
|
|
|
http.Redirect(w, req, target, http.StatusFound)
|
|
|
|
}
|
|
|
|
|
2021-08-12 19:45:40 +00:00
|
|
|
// expireEphemeralNodes deletes ephemeral machine records that have not been
|
2021-10-26 20:42:56 +00:00
|
|
|
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout.
|
2021-08-12 19:45:40 +00:00
|
|
|
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
|
2021-05-23 00:15:29 +00:00
|
|
|
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
|
|
|
|
for range ticker.C {
|
|
|
|
h.expireEphemeralNodesWorker()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Headscale) expireEphemeralNodesWorker() {
|
|
|
|
namespaces, err := h.ListNamespaces()
|
|
|
|
if err != nil {
|
2021-08-05 17:11:26 +00:00
|
|
|
log.Error().Err(err).Msg("Error listing namespaces")
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-05-23 00:15:29 +00:00
|
|
|
return
|
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-05-23 00:15:29 +00:00
|
|
|
for _, ns := range *namespaces {
|
|
|
|
machines, err := h.ListMachinesInNamespace(ns.Name)
|
|
|
|
if err != nil {
|
2021-08-05 19:57:47 +00:00
|
|
|
log.Error().Err(err).Str("namespace", ns.Name).Msg("Error listing machines in namespace")
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-05-23 00:15:29 +00:00
|
|
|
return
|
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-05-23 00:15:29 +00:00
|
|
|
for _, m := range *machines {
|
2021-10-22 16:55:14 +00:00
|
|
|
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral &&
|
|
|
|
time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
|
2021-08-05 19:57:47 +00:00
|
|
|
log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database")
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-07-04 19:40:46 +00:00
|
|
|
err = h.db.Unscoped().Delete(m).Error
|
2021-05-23 00:15:29 +00:00
|
|
|
if err != nil {
|
2021-10-22 16:55:14 +00:00
|
|
|
log.Error().
|
|
|
|
Err(err).
|
|
|
|
Str("machine", m.Name).
|
|
|
|
Msg("🤮 Cannot delete ephemeral machine from the database")
|
2021-05-23 00:15:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
2021-10-05 16:24:46 +00:00
|
|
|
h.setLastStateChangeToNow(ns.Name)
|
2021-05-23 00:15:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-25 15:59:48 +00:00
|
|
|
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades
|
2021-10-26 20:42:56 +00:00
|
|
|
// This is a way to communitate the CLI with the headscale server.
|
2021-07-25 15:59:48 +00:00
|
|
|
func (h *Headscale) watchForKVUpdates(milliSeconds int64) {
|
|
|
|
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
|
|
|
|
for range ticker.C {
|
|
|
|
h.watchForKVUpdatesWorker()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Headscale) watchForKVUpdatesWorker() {
|
|
|
|
h.checkForNamespacesPendingUpdates()
|
|
|
|
// more functions will come here in the future
|
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// Serve launches a GIN server with the Headscale API.
|
2020-06-21 10:32:08 +00:00
|
|
|
func (h *Headscale) Serve() error {
|
2021-10-26 20:42:56 +00:00
|
|
|
var err error
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
l, err := net.Listen("tcp", h.cfg.Addr)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the cmux object that will multiplex 2 protocols on the same port.
|
|
|
|
// The two following listeners will be served on the same port below gracefully.
|
|
|
|
m := cmux.New(l)
|
|
|
|
// Match gRPC requests here
|
|
|
|
grpcListener := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
|
|
|
|
// Otherwise match regular http requests.
|
|
|
|
httpListener := m.Match(cmux.Any())
|
|
|
|
|
|
|
|
// Now create the grpc server with those options.
|
|
|
|
grpcServer := grpc.NewServer()
|
|
|
|
|
|
|
|
// TODO(kradalby): register the new server when we have authentication ready
|
|
|
|
// apiV1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
|
|
|
|
|
|
|
|
grpcGatewayMux := runtime.NewServeMux()
|
|
|
|
|
|
|
|
opts := []grpc.DialOption{grpc.WithInsecure()}
|
|
|
|
|
|
|
|
err = apiV1.RegisterHeadscaleServiceHandlerFromEndpoint(ctx, grpcGatewayMux, h.cfg.Addr, opts)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-21 10:32:08 +00:00
|
|
|
r := gin.Default()
|
2021-10-04 16:28:07 +00:00
|
|
|
|
|
|
|
p := ginprometheus.NewPrometheus("gin")
|
|
|
|
p.Use(r)
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) })
|
2020-06-21 10:32:08 +00:00
|
|
|
r.GET("/key", h.KeyHandler)
|
|
|
|
r.GET("/register", h.RegisterWebAPI)
|
|
|
|
r.POST("/machine/:id/map", h.PollNetMapHandler)
|
|
|
|
r.POST("/machine/:id", h.RegistrationHandler)
|
2021-09-19 16:56:29 +00:00
|
|
|
r.GET("/apple", h.AppleMobileConfig)
|
|
|
|
r.GET("/apple/:platform", h.ApplePlatformConfig)
|
2021-07-25 15:59:48 +00:00
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
r.Any("/api/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
|
|
|
|
r.StaticFile("/swagger/swagger.json", "gen/openapiv2/v1/headscale.swagger.json")
|
|
|
|
|
|
|
|
updateMillisecondsWait := int64(5000)
|
2021-07-25 15:59:48 +00:00
|
|
|
|
2021-10-22 16:55:14 +00:00
|
|
|
// Fetch an initial DERP Map before we start serving
|
|
|
|
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
|
|
|
|
|
|
|
if h.cfg.DERP.AutoUpdate {
|
|
|
|
derpMapCancelChannel := make(chan struct{})
|
|
|
|
defer func() { derpMapCancelChannel <- struct{}{} }()
|
|
|
|
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
|
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
// I HATE THIS
|
|
|
|
go h.watchForKVUpdates(updateMillisecondsWait)
|
|
|
|
go h.expireEphemeralNodes(updateMillisecondsWait)
|
|
|
|
|
|
|
|
httpServer := &http.Server{
|
2021-10-02 14:29:27 +00:00
|
|
|
Addr: h.cfg.Addr,
|
|
|
|
Handler: r,
|
|
|
|
ReadTimeout: 30 * time.Second,
|
|
|
|
// Go does not handle timeouts in HTTP very well, and there is
|
|
|
|
// no good way to handle streaming timeouts, therefore we need to
|
|
|
|
// keep this at unlimited and be careful to clean up connections
|
|
|
|
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming
|
|
|
|
WriteTimeout: 0,
|
2021-08-18 22:21:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-26 20:42:56 +00:00
|
|
|
tlsConfig, err := h.getTLSSettings()
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("Failed to set up TLS configuration")
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if tlsConfig != nil {
|
|
|
|
httpServer.TLSConfig = tlsConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
g := new(errgroup.Group)
|
|
|
|
|
|
|
|
g.Go(func() error { return grpcServer.Serve(grpcListener) })
|
|
|
|
g.Go(func() error { return httpServer.Serve(httpListener) })
|
|
|
|
g.Go(func() error { return m.Serve() })
|
|
|
|
|
|
|
|
log.Info().Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr)
|
|
|
|
|
|
|
|
return g.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
2021-04-24 02:54:15 +00:00
|
|
|
if h.cfg.TLSLetsEncryptHostname != "" {
|
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
2021-08-05 17:11:26 +00:00
|
|
|
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
|
2021-04-24 02:54:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
m := autocert.Manager{
|
|
|
|
Prompt: autocert.AcceptTOS,
|
|
|
|
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
|
|
|
|
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
2021-10-03 18:26:38 +00:00
|
|
|
Client: &acme.Client{
|
|
|
|
DirectoryURL: h.cfg.ACMEURL,
|
|
|
|
},
|
|
|
|
Email: h.cfg.ACMEEmail,
|
2021-04-24 02:54:15 +00:00
|
|
|
}
|
2021-10-02 14:29:27 +00:00
|
|
|
|
2021-04-24 02:54:15 +00:00
|
|
|
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
|
|
|
|
// 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 13:01:20 +00:00
|
|
|
// must be reachable on port 443.
|
2021-10-26 20:42:56 +00:00
|
|
|
return m.TLSConfig(), nil
|
2021-04-24 02:54:15 +00:00
|
|
|
} else if h.cfg.TLSLetsEncryptChallengeType == "HTTP-01" {
|
|
|
|
// 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.
|
|
|
|
go func() {
|
2021-08-05 17:11:26 +00:00
|
|
|
log.Fatal().
|
|
|
|
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
|
|
|
Msg("failed to set up a HTTP server")
|
2021-04-24 02:54:15 +00:00
|
|
|
}()
|
2021-10-26 20:42:56 +00:00
|
|
|
|
|
|
|
return m.TLSConfig(), nil
|
2021-04-24 02:54:15 +00:00
|
|
|
} else {
|
2021-10-26 20:42:56 +00:00
|
|
|
return nil, errors.New("unknown value for TLSLetsEncryptChallengeType")
|
2021-04-24 02:54:15 +00:00
|
|
|
}
|
|
|
|
} else if h.cfg.TLSCertPath == "" {
|
2021-04-23 20:54:35 +00:00
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
|
2021-08-05 17:11:26 +00:00
|
|
|
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
|
2021-04-23 20:54:35 +00:00
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
|
|
|
|
return nil, nil
|
2021-04-23 20:54:35 +00:00
|
|
|
} else {
|
|
|
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
2021-08-05 17:11:26 +00:00
|
|
|
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
|
2021-04-23 20:54:35 +00:00
|
|
|
}
|
2021-10-26 20:42:56 +00:00
|
|
|
var err error
|
|
|
|
tlsConfig := &tls.Config{}
|
|
|
|
tlsConfig.ClientAuth = tls.RequireAnyClientCert
|
|
|
|
tlsConfig.NextProtos = []string{"http/1.1"}
|
|
|
|
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
|
|
|
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
|
|
|
|
|
|
|
|
return tlsConfig, err
|
2021-04-23 20:54:35 +00:00
|
|
|
}
|
2020-06-21 10:32:08 +00:00
|
|
|
}
|
2021-08-18 22:21:11 +00:00
|
|
|
|
2021-08-19 17:19:26 +00:00
|
|
|
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
2021-08-18 22:21:11 +00:00
|
|
|
now := time.Now().UTC()
|
2021-10-04 16:28:07 +00:00
|
|
|
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
|
2021-08-19 17:19:26 +00:00
|
|
|
h.lastStateChange.Store(namespace, now)
|
2021-08-18 22:21:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-06 22:06:07 +00:00
|
|
|
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
|
|
|
times := []time.Time{}
|
|
|
|
|
|
|
|
for _, namespace := range namespaces {
|
|
|
|
if wrapped, ok := h.lastStateChange.Load(namespace); ok {
|
|
|
|
lastChange, _ := wrapped.(time.Time)
|
|
|
|
|
|
|
|
times = append(times, lastChange)
|
|
|
|
}
|
2021-08-19 17:19:26 +00:00
|
|
|
}
|
|
|
|
|
2021-10-06 22:06:07 +00:00
|
|
|
sort.Slice(times, func(i, j int) bool {
|
|
|
|
return times[i].After(times[j])
|
|
|
|
})
|
|
|
|
|
|
|
|
log.Trace().Msgf("Latest times %#v", times)
|
|
|
|
|
|
|
|
if len(times) == 0 {
|
|
|
|
return time.Now().UTC()
|
|
|
|
} else {
|
|
|
|
return times[0]
|
|
|
|
}
|
2021-08-18 22:21:11 +00:00
|
|
|
}
|