headscale/app.go

217 lines
6.1 KiB
Go
Raw Normal View History

2020-06-21 10:32:08 +00:00
package headscale
import (
"errors"
2020-06-21 10:32:08 +00:00
"fmt"
"net/http"
2021-02-21 22:54:15 +00:00
"os"
"strings"
"sync"
"time"
2020-06-21 10:32:08 +00:00
2021-08-05 17:11:26 +00:00
"github.com/rs/zerolog/log"
2020-06-21 10:32:08 +00:00
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
2021-07-04 19:40:46 +00:00
"gorm.io/gorm"
"inet.af/netaddr"
2021-02-20 22:57:06 +00:00
"tailscale.com/tailcfg"
2021-06-25 16:57:08 +00:00
"tailscale.com/types/wgkey"
2020-06-21 10:32:08 +00:00
)
2021-02-21 21:14:38 +00:00
// Config contains the initial Headscale configuration
2020-06-21 10:32:08 +00:00
type Config struct {
ServerURL string
Addr string
PrivateKeyPath string
DerpMap *tailcfg.DERPMap
EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix
2020-06-21 10:32:08 +00:00
DBtype string
DBpath string
2020-06-21 10:32:08 +00:00
DBhost string
DBport int
DBname string
DBuser string
DBpass string
TLSLetsEncryptListen string
TLSLetsEncryptHostname string
TLSLetsEncryptCacheDir string
TLSLetsEncryptChallengeType string
TLSCertPath string
TLSKeyPath string
2020-06-21 10:32:08 +00:00
}
2021-02-21 21:14:38 +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
dbType string
dbDebug bool
2021-06-25 16:57:08 +00:00
publicKey *wgkey.Key
privateKey *wgkey.Private
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
clientsPolling sync.Map
2020-06-21 10:32:08 +00:00
}
2021-02-21 21:14:38 +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-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()
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")
}
2020-06-21 10:32:08 +00:00
h := Headscale{
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
2020-06-21 10:32:08 +00:00
return &h, nil
}
// Redirect to our TLS url
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
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout
2021-08-12 19:45:40 +00:00
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
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")
return
}
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")
return
}
for _, m := range *machines {
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-07-04 19:40:46 +00:00
err = h.db.Unscoped().Delete(m).Error
if err != nil {
2021-08-05 19:57:47 +00:00
log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database")
}
}
}
}
}
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades
// This is a way to communitate the CLI with the headscale server
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-02-21 21:14:38 +00:00
// Serve launches a GIN server with the Headscale API
2020-06-21 10:32:08 +00:00
func (h *Headscale) Serve() error {
r := gin.Default()
r.GET("/health", func(c *gin.Context) { c.JSON(200, 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)
var err error
go h.watchForKVUpdates(5000)
2021-08-12 19:45:40 +00:00
go h.expireEphemeralNodes(5000)
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://")
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
}
s := &http.Server{
Addr: h.cfg.Addr,
TLSConfig: m.TLSConfig(),
Handler: r,
}
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
// must be reachable on port 443.
err = s.ListenAndServeTLS("", "")
} 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 15:26:50 +00:00
err = s.ListenAndServeTLS("", "")
} else {
return errors.New("unknown value for TLSLetsEncryptChallengeType")
}
} else if h.cfg.TLSCertPath == "" {
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://")
}
err = r.Run(h.cfg.Addr)
} 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://")
}
err = r.RunTLS(h.cfg.Addr, h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
}
2020-06-21 10:32:08 +00:00
return err
}