From 6e8e2bf5083a268a278af5e331d2f0b3f262d157 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 11:14:38 +0200 Subject: [PATCH 01/63] Generate and read the Noise private key --- app.go | 27 +++++++++++++++++++-------- config-example.yaml | 7 +++++++ config.go | 4 ++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 3e001203..8cfaa615 100644 --- a/app.go +++ b/app.go @@ -72,12 +72,13 @@ const ( // Headscale represents the base app of the service. type Headscale struct { - cfg *Config - db *gorm.DB - dbString string - dbType string - dbDebug bool - privateKey *key.MachinePrivate + cfg *Config + db *gorm.DB + dbString string + dbType string + dbDebug bool + privateKey *key.MachinePrivate + noisePrivateKey *key.MachinePrivate DERPMap *tailcfg.DERPMap DERPServer *DERPServer @@ -120,11 +121,20 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) { } func NewHeadscale(cfg *Config) (*Headscale, error) { - privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) + privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) if err != nil { return nil, fmt.Errorf("failed to read or create private key: %w", err) } + noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to read or create noise private key: %w", err) + } + + if privateKey.Equal(*noisePrivateKey) { + return nil, fmt.Errorf("private key and noise private key are the same") + } + var dbString string switch cfg.DBtype { case Postgres: @@ -151,7 +161,8 @@ func NewHeadscale(cfg *Config) (*Headscale, error) { cfg: cfg, dbType: cfg.DBtype, dbString: dbString, - privateKey: privKey, + privateKey: privateKey, + noisePrivateKey: noisePrivateKey, aclRules: tailcfg.FilterAllowAll, // default allowall registrationCache: registrationCache, pollNetMapStreamWG: sync.WaitGroup{}, diff --git a/config-example.yaml b/config-example.yaml index d3d155e2..e090d910 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -41,6 +41,13 @@ grpc_allow_insecure: false # autogenerated if it's missing private_key_path: /var/lib/headscale/private.key +# The Noise private key is used to encrypt the +# traffic between headscale and Tailscale clients when +# using the new Noise-based TS2021 protocol. +# The noise private key file which will be +# autogenerated if it's missing +noise_private_key_path: /var/lib/headscale/noise_private.key + # List of IP prefixes to allocate tailaddresses from. # Each prefix consists of either an IPv4 or IPv6 address, # and the associated prefix length, delimited by a slash. diff --git a/config.go b/config.go index 69358401..a792bab9 100644 --- a/config.go +++ b/config.go @@ -34,6 +34,7 @@ type Config struct { NodeUpdateCheckInterval time.Duration IPPrefixes []netaddr.IPPrefix PrivateKeyPath string + NoisePrivateKeyPath string BaseDomain string LogLevel zerolog.Level DisableUpdateCheck bool @@ -487,6 +488,9 @@ func GetHeadscaleConfig() (*Config, error) { PrivateKeyPath: AbsolutePathFromConfigPath( viper.GetString("private_key_path"), ), + NoisePrivateKeyPath: AbsolutePathFromConfigPath( + viper.GetString("noise_private_key_path"), + ), BaseDomain: baseDomain, DERP: derpConfig, From 3e8f0e99849538568f329303f032b3655f4add99 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 11:24:05 +0200 Subject: [PATCH 02/63] Added support for Noise clients in /key handler --- api.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/api.go b/api.go index 561545b8..52149d58 100644 --- a/api.go +++ b/api.go @@ -9,6 +9,7 @@ import ( "html/template" "io" "net/http" + "strconv" "strings" "time" @@ -30,6 +31,12 @@ const ( ErrRegisterMethodCLIDoesNotSupportExpire = Error( "machines registered with CLI does not support expire", ) + + // The CapabilityVersion is used by Tailscale clients to indicate + // their codebase version. Tailscale clients can communicate over TS2021 + // from CapabilityVersion 28. + // See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go + NoiseCapabilityVersion = 28 ) func (h *Headscale) HealthHandler( @@ -76,6 +83,45 @@ func (h *Headscale) KeyHandler( writer http.ResponseWriter, req *http.Request, ) { + // New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion + clientCapabilityStr := req.URL.Query().Get("v") + if clientCapabilityStr != "" { + clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr) + if err != nil { + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + writer.WriteHeader(http.StatusBadRequest) + _, err := writer.Write([]byte("Wrong params")) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + + if clientCapabilityVersion >= NoiseCapabilityVersion { + // Tailscale has a different key for the TS2021 protocol + resp := tailcfg.OverTLSPublicKeyResponse{ + LegacyPublicKey: h.privateKey.Public(), + PublicKey: h.noisePrivateKey.Public(), + } + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err = json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + } + + // Old clients don't send a 'v' parameter, so we send the legacy public key writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public()))) From 014e7abc6855973c7caaeac27aad72eab9b3b88a Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 14:46:23 +0200 Subject: [PATCH 03/63] Make private key errors constants --- app.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 8cfaa615..861b9558 100644 --- a/app.go +++ b/app.go @@ -51,6 +51,10 @@ const ( errUnsupportedLetsEncryptChallengeType = Error( "unknown value for Lets Encrypt challenge type", ) + + ErrFailedPrivateKey = Error("failed to read or create private key") + ErrFailedNoisePrivateKey = Error("failed to read or create Noise protocol private key") + ErrSamePrivateKeys = Error("private key and noise private key are the same") ) const ( @@ -123,16 +127,16 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) { func NewHeadscale(cfg *Config) (*Headscale, error) { privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) if err != nil { - return nil, fmt.Errorf("failed to read or create private key: %w", err) + return nil, ErrFailedPrivateKey } noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath) if err != nil { - return nil, fmt.Errorf("failed to read or create noise private key: %w", err) + return nil, ErrFailedNoisePrivateKey } if privateKey.Equal(*noisePrivateKey) { - return nil, fmt.Errorf("private key and noise private key are the same") + return nil, ErrSamePrivateKeys } var dbString string From b261d19cfeadba59254ad7ca2caaea03708e9879 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 20:52:11 +0200 Subject: [PATCH 04/63] Added Noise upgrade handler and Noise mux --- noise.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 noise.go diff --git a/noise.go b/noise.go new file mode 100644 index 00000000..bd7d4bbd --- /dev/null +++ b/noise.go @@ -0,0 +1,134 @@ +package headscale + +import ( + "encoding/base64" + "net/http" + + "github.com/rs/zerolog/log" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "tailscale.com/control/controlbase" + "tailscale.com/net/netutil" +) + +const ( + errWrongConnectionUpgrade = Error("wrong connection upgrade") + errCannotHijack = Error("cannot hijack connection") + errNoiseHandshakeFailed = Error("noise handshake failed") +) + +const ( + // ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade. + ts2021UpgradePath = "/ts2021" + + // upgradeHeader is the value of the Upgrade HTTP header used to + // indicate the Tailscale control protocol. + upgradeHeaderValue = "tailscale-control-protocol" + + // handshakeHeaderName is the HTTP request header that can + // optionally contain base64-encoded initial handshake + // payload, to save an RTT. + handshakeHeaderName = "X-Tailscale-Handshake" +) + +// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn +// in order to use the Noise-based TS2021 protocol. Listens in /ts2021. +func (h *Headscale) NoiseUpgradeHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr) + + // Under normal circumpstances, we should be able to use the controlhttp.AcceptHTTP() + // function to do this - kindly left there by the Tailscale authors for us to use. + // (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go) + // + // When we used to use Gin, we had troubles here as Gin seems to do some + // fun stuff, and not flusing the writer properly. + // So have getNoiseConnection() that is essentially an AcceptHTTP, but in our side. + noiseConn, err := h.getNoiseConnection(writer, req) + if err != nil { + log.Error().Err(err).Msg("noise upgrade failed") + http.Error(writer, err.Error(), http.StatusInternalServerError) + + return + } + + server := http.Server{} + server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{}) + server.Serve(netutil.NewOneConnListener(noiseConn, nil)) +} + +// getNoiseConnection is basically AcceptHTTP from tailscale +// TODO(juan): Figure out why we need to do this at all. +func (h *Headscale) getNoiseConnection( + writer http.ResponseWriter, + req *http.Request, +) (*controlbase.Conn, error) { + next := req.Header.Get("Upgrade") + if next == "" { + http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest) + + return nil, errWrongConnectionUpgrade + } + if next != upgradeHeaderValue { + http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest) + + return nil, errWrongConnectionUpgrade + } + + initB64 := req.Header.Get(handshakeHeaderName) + if initB64 == "" { + log.Warn(). + Caller(). + Msg("no handshake header") + http.Error(writer, "missing Tailscale handshake header", http.StatusBadRequest) + + return nil, errWrongConnectionUpgrade + } + + init, err := base64.StdEncoding.DecodeString(initB64) + if err != nil { + log.Warn().Err(err).Msg("invalid handshake header") + http.Error(writer, "invalid tailscale handshake header", http.StatusBadRequest) + + return nil, errWrongConnectionUpgrade + } + + hijacker, ok := writer.(http.Hijacker) + if !ok { + log.Error().Caller().Err(err).Msgf("Hijack failed") + http.Error(writer, errCannotHijack.Error(), http.StatusInternalServerError) + + return nil, errCannotHijack + } + + // This is what changes from the original AcceptHTTP() function. + writer.Header().Set("Upgrade", upgradeHeaderValue) + writer.Header().Set("Connection", "upgrade") + writer.WriteHeader(http.StatusSwitchingProtocols) + // end + + netConn, conn, err := hijacker.Hijack() + if err != nil { + log.Error().Caller().Err(err).Msgf("Hijack failed") + http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError) + return nil, errCannotHijack + } + if err := conn.Flush(); err != nil { + netConn.Close() + + return nil, errCannotHijack + } + + netConn = netutil.NewDrainBufConn(netConn, conn.Reader) + + nc, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init) + if err != nil { + netConn.Close() + + return nil, errNoiseHandshakeFailed + } + + return nc, nil +} From be24bacb797315e5ca28392960c2b895e4228a69 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 20:55:37 +0200 Subject: [PATCH 05/63] Add noise mux and Noise path to base router --- app.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 861b9558..1a7ba720 100644 --- a/app.go +++ b/app.go @@ -84,6 +84,8 @@ type Headscale struct { privateKey *key.MachinePrivate noisePrivateKey *key.MachinePrivate + noiseMux *mux.Router + DERPMap *tailcfg.DERPMap DERPServer *DERPServer @@ -430,6 +432,8 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error { func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router { router := mux.NewRouter() + router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost) + router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet) router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet) router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet) @@ -459,6 +463,15 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router { return router } +func (h *Headscale) createNoiseMux() *mux.Router { + router := mux.NewRouter() + + //router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost) + //router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost) + + return router +} + // Serve launches a GIN server with the Headscale API. func (h *Headscale) Serve() error { var err error @@ -612,9 +625,16 @@ func (h *Headscale) Serve() error { // // HTTP setup // - + // This is the regular router that we expose + // over our main Addr. It also serves the legacy Tailcale API router := h.createRouter(grpcGatewayMux) + // This router is served only over the Noise connection, and exposes only the new API. + // + // The HTTP2 server that exposes this router is created for + // a single hijacked connection from /ts2021, using netutil.NewOneConnListener + h.noiseMux = h.createNoiseMux() + httpServer := &http.Server{ Addr: h.cfg.Addr, Handler: router, From fdd0c5040261d2d7aa1bf4f292aa01411fed2619 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 21:03:02 +0200 Subject: [PATCH 06/63] Added helper method to fetch machines by any nodekey + tests --- machine.go | 13 +++++++++++ machine_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/machine.go b/machine.go index aebfbcef..b436fd87 100644 --- a/machine.go +++ b/machine.go @@ -375,6 +375,19 @@ func (h *Headscale) GetMachineByNodeKey( return &machine, nil } +// GetMachineByAnyNodeKey finds a Machine by its current NodeKey or the old one, and returns the Machine struct. +func (h *Headscale) GetMachineByAnyNodeKey( + nodeKey key.NodePublic, oldNodeKey key.NodePublic, +) (*Machine, error) { + machine := Machine{} + if result := h.db.Preload("Namespace").First(&machine, "node_key = ? OR node_key = ?", + NodePublicKeyStripPrefix(nodeKey), NodePublicKeyStripPrefix(oldNodeKey)); result.Error != nil { + return nil, result.Error + } + + return &machine, nil +} + // UpdateMachineFromDatabase takes a Machine struct pointer (typically already loaded from database // and updates it with the latest data from the database. func (h *Headscale) UpdateMachineFromDatabase(machine *Machine) error { diff --git a/machine_test.go b/machine_test.go index 53d065ff..5da0906f 100644 --- a/machine_test.go +++ b/machine_test.go @@ -11,6 +11,7 @@ import ( "gopkg.in/check.v1" "inet.af/netaddr" "tailscale.com/tailcfg" + "tailscale.com/types/key" ) func (s *Suite) TestGetMachine(c *check.C) { @@ -65,6 +66,63 @@ func (s *Suite) TestGetMachineByID(c *check.C) { c.Assert(err, check.IsNil) } +func (s *Suite) TestGetMachineByNodeKey(c *check.C) { + namespace, err := app.CreateNamespace("test") + c.Assert(err, check.IsNil) + + pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = app.GetMachineByID(0) + c.Assert(err, check.NotNil) + + nodeKey := key.NewNode() + + machine := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()), + DiscoKey: "faa", + Hostname: "testmachine", + NamespaceID: namespace.ID, + RegisterMethod: RegisterMethodAuthKey, + AuthKeyID: uint(pak.ID), + } + app.db.Save(&machine) + + _, err = app.GetMachineByNodeKey(nodeKey.Public()) + c.Assert(err, check.IsNil) +} + +func (s *Suite) TestGetMachineByAnyNodeKey(c *check.C) { + namespace, err := app.CreateNamespace("test") + c.Assert(err, check.IsNil) + + pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = app.GetMachineByID(0) + c.Assert(err, check.NotNil) + + nodeKey := key.NewNode() + oldNodeKey := key.NewNode() + + machine := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()), + DiscoKey: "faa", + Hostname: "testmachine", + NamespaceID: namespace.ID, + RegisterMethod: RegisterMethodAuthKey, + AuthKeyID: uint(pak.ID), + } + app.db.Save(&machine) + + _, err = app.GetMachineByAnyNodeKey(nodeKey.Public(), oldNodeKey.Public()) + c.Assert(err, check.IsNil) +} + func (s *Suite) TestDeleteMachine(c *check.C) { namespace, err := app.CreateNamespace("test") c.Assert(err, check.IsNil) From 1880035f6fe3e2e65f01607526f433f75c9313e9 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 13 Aug 2022 21:12:19 +0200 Subject: [PATCH 07/63] Add registration handler over Noise protocol --- app.go | 2 +- noise_api.go | 427 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 noise_api.go diff --git a/app.go b/app.go index 1a7ba720..9c0f11d4 100644 --- a/app.go +++ b/app.go @@ -466,7 +466,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router { func (h *Headscale) createNoiseMux() *mux.Router { router := mux.NewRouter() - //router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost) + router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost) //router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost) return router diff --git a/noise_api.go b/noise_api.go new file mode 100644 index 00000000..475e6022 --- /dev/null +++ b/noise_api.go @@ -0,0 +1,427 @@ +package headscale + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" +) + +// // NoiseRegistrationHandler handles the actual registration process of a machine +func (h *Headscale) NoiseRegistrationHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr) + if req.Method != http.MethodPost { + http.Error(writer, "Wrong method", http.StatusMethodNotAllowed) + return + } + body, _ := io.ReadAll(req.Body) + registerRequest := tailcfg.RegisterRequest{} + if err := json.Unmarshal(body, ®isterRequest); err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse RegisterRequest") + machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + log.Trace().Caller(). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("old_node_key", registerRequest.OldNodeKey.ShortString()). + Msg("New node is registering") + + now := time.Now().UTC() + machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) + if errors.Is(err, gorm.ErrRecordNotFound) { + // If the machine has AuthKey set, handle registration via PreAuthKeys + if registerRequest.Auth.AuthKey != "" { + h.handleNoiseAuthKey(writer, req, registerRequest) + + return + } + + // Check if the node is waiting for interactive login. + // + // TODO(juan): We could use this field to improve our protocol implementation, + // and hold the request until the client closes it, or the interactive + // login is completed (i.e., the user registers the machine). + // This is not implemented yet, as it is no strictly required. The only side-effect + // is that the client will hammer headscale with requests until it gets a + // successful RegisterResponse. + if registerRequest.Followup != "" { + if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("Machine is waiting for interactive login") + + ticker := time.NewTicker(registrationHoldoff) + select { + case <-req.Context().Done(): + return + case <-ticker.C: + h.handleNoiseMachineRegistrationNew(writer, req, registerRequest) + + return + } + } + } + + log.Info(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("New Noise machine not yet in the database") + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.Hostinfo.Hostname). + Err(err) + + return + } + + // The machine did not have a key to authenticate, which means + // that we rely on a method that calls back some how (OpenID or CLI) + // We create the machine and then keep it around until a callback + // happens + newMachine := Machine{ + MachineKey: "", + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), + LastSeen: &now, + Expiry: &time.Time{}, + } + + if !registerRequest.Expiry.IsZero() { + log.Trace(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Time("expiry", registerRequest.Expiry). + Msg("Non-zero expiry time requested") + newMachine.Expiry = ®isterRequest.Expiry + } + + h.registrationCache.Set( + NodePublicKeyStripPrefix(registerRequest.NodeKey), + newMachine, + registerCacheExpiration, + ) + + h.handleNoiseMachineRegistrationNew(writer, req, registerRequest) + + return + } + + // The machine is already registered, so we need to pass through reauth or key update. + if machine != nil { + // If the NodeKey stored in headscale is the same as the key presented in a registration + // request, then we have a node that is either: + // - Trying to log out (sending a expiry in the past) + // - A valid, registered machine, looking for the node map + // - Expired machine wanting to reauthenticate + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { + // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) + // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 + if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) { + h.handleNoiseNodeLogOut(writer, req, *machine) + + return + } + + // If machine is not expired, and is register, we have a already accepted this machine, + // let it proceed with a valid registration + if !machine.isExpired() { + h.handleNoiseNodeValidRegistration(writer, req, *machine) + + return + } + } + + // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && + !machine.isExpired() { + h.handleNoiseNodeRefreshKey(writer, req, registerRequest, *machine) + + return + } + + // The node has expired + h.handleNoiseNodeExpired(writer, req, registerRequest, *machine) + + return + } +} + +func (h *Headscale) handleNoiseAuthKey( + w http.ResponseWriter, + r *http.Request, + registerRequest tailcfg.RegisterRequest, +) { + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname) + resp := tailcfg.RegisterResponse{} + + pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey) + if err != nil { + log.Error(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Err(err). + Msg("Failed authentication via AuthKey") + resp.MachineAuthorized = false + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(resp) + + log.Error(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("Failed authentication via AuthKey over Noise") + + if pak != nil { + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + } else { + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc() + } + + return + } + + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("Authentication key was valid, proceeding to acquire IP addresses") + + nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey) + + // retrieve machine information if it exist + // The error is not important, because if it does not + // exist, then this is a new machine and we will move + // on to registration. + machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) + if machine != nil { + log.Trace(). + Caller(). + Str("machine", machine.Hostname). + Msg("machine already registered, refreshing with new auth key") + + machine.NodeKey = nodeKey + machine.AuthKeyID = uint(pak.ID) + h.RefreshMachine(machine, registerRequest.Expiry) + } else { + now := time.Now().UTC() + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.Hostinfo.Hostname). + Err(err) + + return + } + + machineToRegister := Machine{ + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NamespaceID: pak.Namespace.ID, + MachineKey: "", + RegisterMethod: RegisterMethodAuthKey, + Expiry: ®isterRequest.Expiry, + NodeKey: nodeKey, + LastSeen: &now, + AuthKeyID: uint(pak.ID), + } + + machine, err = h.RegisterMachine( + machineToRegister, + ) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("could not register machine") + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + http.Error(w, "Internal error", http.StatusInternalServerError) + + return + } + } + + h.UsePreAuthKey(pak) + + resp.MachineAuthorized = true + resp.User = *pak.Namespace.toUser() + + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). + Inc() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + + log.Info(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). + Msg("Successfully authenticated via AuthKey on Noise") +} + +func (h *Headscale) handleNoiseNodeValidRegistration( + w http.ResponseWriter, + r *http.Request, + machine Machine, +) { + resp := tailcfg.RegisterResponse{} + + // The machine registration is valid, respond with redirect to /map + log.Debug(). + Str("machine", machine.Hostname). + Msg("Client is registered and we have the current NodeKey. All clear to /map") + + resp.AuthURL = "" + resp.MachineAuthorized = true + resp.User = *machine.Namespace.toUser() + resp.Login = *machine.Namespace.toLogin() + + machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name). + Inc() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (h *Headscale) handleNoiseMachineRegistrationNew( + w http.ResponseWriter, + r *http.Request, + registerRequest tailcfg.RegisterRequest, +) { + resp := tailcfg.RegisterResponse{} + + // The machine registration is new, redirect the client to the registration URL + log.Debug(). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("The node is sending us a new NodeKey, sending auth url") + if h.cfg.OIDC.Issuer != "" { + resp.AuthURL = fmt.Sprintf( + "%s/oidc/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), + NodePublicKeyStripPrefix(registerRequest.NodeKey), + ) + } else { + resp.AuthURL = fmt.Sprintf("%s/register?key=%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (h *Headscale) handleNoiseNodeLogOut( + w http.ResponseWriter, + r *http.Request, + machine Machine, +) { + resp := tailcfg.RegisterResponse{} + + log.Info(). + Str("machine", machine.Hostname). + Msg("Client requested logout") + + h.ExpireMachine(&machine) + + resp.AuthURL = "" + resp.MachineAuthorized = false + resp.User = *machine.Namespace.toUser() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (h *Headscale) handleNoiseNodeRefreshKey( + w http.ResponseWriter, + r *http.Request, + registerRequest tailcfg.RegisterRequest, + machine Machine, +) { + resp := tailcfg.RegisterResponse{} + + log.Debug(). + Str("machine", machine.Hostname). + Msg("We have the OldNodeKey in the database. This is a key refresh") + machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) + h.db.Save(&machine) + + resp.AuthURL = "" + resp.User = *machine.Namespace.toUser() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (h *Headscale) handleNoiseNodeExpired( + w http.ResponseWriter, + r *http.Request, + registerRequest tailcfg.RegisterRequest, + machine Machine, +) { + resp := tailcfg.RegisterResponse{} + + // The client has registered before, but has expired + log.Debug(). + Caller(). + Str("machine", machine.Hostname). + Msg("Machine registration has expired. Sending a authurl to register") + + if registerRequest.Auth.AuthKey != "" { + h.handleNoiseAuthKey(w, r, registerRequest) + + return + } + + if h.cfg.OIDC.Issuer != "" { + resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey) + } else { + resp.AuthURL = fmt.Sprintf("%s/register?key=%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey) + } + + machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). + Inc() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} From c7cea9ef16faa67959a7500be2e31e6c3e838a5c Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 03:07:28 +0200 Subject: [PATCH 08/63] updated paths --- noise_api.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noise_api.go b/noise_api.go index 475e6022..4bc28fc4 100644 --- a/noise_api.go +++ b/noise_api.go @@ -412,10 +412,10 @@ func (h *Headscale) handleNoiseNodeExpired( if h.cfg.OIDC.Issuer != "" { resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey) + strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) } else { - resp.AuthURL = fmt.Sprintf("%s/register?key=%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey) + resp.AuthURL = fmt.Sprintf("%s/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) } machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). From 39b85b02bbef9f0ecd74112524e883d3b3dcbd59 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 03:20:53 +0200 Subject: [PATCH 09/63] Move getMapResponse into reusable function by TS2019 and TS2021 --- api.go | 140 ++++++++++++++++++++++++++++++-------------------------- poll.go | 4 +- 2 files changed, 78 insertions(+), 66 deletions(-) diff --git a/api.go b/api.go index 82e13c77..86d77b6a 100644 --- a/api.go +++ b/api.go @@ -396,78 +396,16 @@ func (h *Headscale) RegistrationHandler( } } -func (h *Headscale) getMapResponse( +func (h *Headscale) getLegacyMapResponseData( machineKey key.MachinePublic, mapRequest tailcfg.MapRequest, machine *Machine, ) ([]byte, error) { - log.Trace(). - Str("func", "getMapResponse"). - Str("machine", mapRequest.Hostinfo.Hostname). - Msg("Creating Map response") - node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) + resp, err := h.generateMapResponse(mapRequest, machine) if err != nil { - log.Error(). - Caller(). - Str("func", "getMapResponse"). - Err(err). - Msg("Cannot convert to node") - return nil, err } - peers, err := h.getValidPeers(machine) - if err != nil { - log.Error(). - Caller(). - Str("func", "getMapResponse"). - Err(err). - Msg("Cannot fetch peers") - - return nil, err - } - - profiles := getMapResponseUserProfiles(*machine, peers) - - nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) - if err != nil { - log.Error(). - Caller(). - Str("func", "getMapResponse"). - Err(err). - Msg("Failed to convert peers to Tailscale nodes") - - return nil, err - } - - dnsConfig := getMapResponseDNSConfig( - h.cfg.DNSConfig, - h.cfg.BaseDomain, - *machine, - peers, - ) - - resp := tailcfg.MapResponse{ - KeepAlive: false, - Node: node, - Peers: nodePeers, - DNSConfig: dnsConfig, - Domain: h.cfg.BaseDomain, - PacketFilter: h.aclRules, - DERPMap: h.DERPMap, - UserProfiles: profiles, - Debug: &tailcfg.Debug{ - DisableLogTail: !h.cfg.LogTail.Enabled, - RandomizeClientPort: h.cfg.RandomizeClientPort, - }, - } - - log.Trace(). - Str("func", "getMapResponse"). - Str("machine", mapRequest.Hostinfo.Hostname). - // Interface("payload", resp). - Msgf("Generated map response: %s", tailMapResponseToString(resp)) - var respBody []byte if mapRequest.Compress == "zstd" { src, err := json.Marshal(resp) @@ -498,6 +436,80 @@ func (h *Headscale) getMapResponse( return data, nil } +func (h *Headscale) generateMapResponse( + mapRequest tailcfg.MapRequest, + machine *Machine, +) (*tailcfg.MapResponse, error) { + log.Trace(). + Str("func", "generateMapResponse"). + Str("machine", mapRequest.Hostinfo.Hostname). + Msg("Creating Map response") + node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Cannot convert to node") + + return nil, err + } + + peers, err := h.getValidPeers(machine) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Cannot fetch peers") + + return nil, err + } + + profiles := getMapResponseUserProfiles(*machine, peers) + + nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Failed to convert peers to Tailscale nodes") + + return nil, err + } + + dnsConfig := getMapResponseDNSConfig( + h.cfg.DNSConfig, + h.cfg.BaseDomain, + *machine, + peers, + ) + + resp := tailcfg.MapResponse{ + KeepAlive: false, + Node: node, + Peers: nodePeers, + DNSConfig: dnsConfig, + Domain: h.cfg.BaseDomain, + PacketFilter: h.aclRules, + DERPMap: h.DERPMap, + UserProfiles: profiles, + Debug: &tailcfg.Debug{ + DisableLogTail: !h.cfg.LogTail.Enabled, + RandomizeClientPort: h.cfg.RandomizeClientPort, + }, + } + + log.Trace(). + Str("func", "generateMapResponse"). + Str("machine", mapRequest.Hostinfo.Hostname). + // Interface("payload", resp). + Msgf("Generated map response: %s", tailMapResponseToString(resp)) + + return &resp, nil +} + func (h *Headscale) getMapKeepAliveResponse( machineKey key.MachinePublic, mapRequest tailcfg.MapRequest, diff --git a/poll.go b/poll.go index 9c17b5cb..a51c9362 100644 --- a/poll.go +++ b/poll.go @@ -143,7 +143,7 @@ func (h *Headscale) PollNetMapHandler( } } - data, err := h.getMapResponse(machineKey, mapRequest, machine) + data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine) if err != nil { log.Error(). Str("handler", "PollNetMap"). @@ -491,7 +491,7 @@ func (h *Headscale) PollNetMapStream( Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("There has been updates since the last successful update to %s", machine.Hostname) - data, err := h.getMapResponse(machineKey, mapRequest, machine) + data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). From 9994fce9d5781da62ba783c616e398ec39c56b44 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:00:43 +0200 Subject: [PATCH 10/63] Fixed some linting errors --- noise_api.go | 73 +++++++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/noise_api.go b/noise_api.go index 4bc28fc4..fc0f0911 100644 --- a/noise_api.go +++ b/noise_api.go @@ -14,7 +14,7 @@ import ( "tailscale.com/tailcfg" ) -// // NoiseRegistrationHandler handles the actual registration process of a machine +// // NoiseRegistrationHandler handles the actual registration process of a machine. func (h *Headscale) NoiseRegistrationHandler( writer http.ResponseWriter, req *http.Request, @@ -175,8 +175,8 @@ func (h *Headscale) NoiseRegistrationHandler( } func (h *Headscale) handleNoiseAuthKey( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, registerRequest tailcfg.RegisterRequest, ) { log.Debug(). @@ -194,9 +194,9 @@ func (h *Headscale) handleNoiseAuthKey( Msg("Failed authentication via AuthKey") resp.MachineAuthorized = false - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(writer).Encode(resp) log.Error(). Caller(). @@ -270,7 +270,7 @@ func (h *Headscale) handleNoiseAuthKey( Msg("could not register machine") machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). Inc() - http.Error(w, "Internal error", http.StatusInternalServerError) + http.Error(writer, "Internal error", http.StatusInternalServerError) return } @@ -284,9 +284,9 @@ func (h *Headscale) handleNoiseAuthKey( machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). Inc() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(resp) log.Info(). Caller(). @@ -320,8 +320,8 @@ func (h *Headscale) handleNoiseNodeValidRegistration( } func (h *Headscale) handleNoiseMachineRegistrationNew( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, registerRequest tailcfg.RegisterRequest, ) { resp := tailcfg.RegisterResponse{} @@ -337,18 +337,18 @@ func (h *Headscale) handleNoiseMachineRegistrationNew( NodePublicKeyStripPrefix(registerRequest.NodeKey), ) } else { - resp.AuthURL = fmt.Sprintf("%s/register?key=%s", + resp.AuthURL = fmt.Sprintf("%s/register/%s", strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(resp) } func (h *Headscale) handleNoiseNodeLogOut( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, machine Machine, ) { resp := tailcfg.RegisterResponse{} @@ -363,14 +363,20 @@ func (h *Headscale) handleNoiseNodeLogOut( resp.MachineAuthorized = false resp.User = *machine.Namespace.toUser() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err := json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("could not encode response") + } } func (h *Headscale) handleNoiseNodeRefreshKey( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, registerRequest tailcfg.RegisterRequest, machine Machine, ) { @@ -385,14 +391,14 @@ func (h *Headscale) handleNoiseNodeRefreshKey( resp.AuthURL = "" resp.User = *machine.Namespace.toUser() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(resp) } func (h *Headscale) handleNoiseNodeExpired( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, registerRequest tailcfg.RegisterRequest, machine Machine, ) { @@ -405,7 +411,7 @@ func (h *Headscale) handleNoiseNodeExpired( Msg("Machine registration has expired. Sending a authurl to register") if registerRequest.Auth.AuthKey != "" { - h.handleNoiseAuthKey(w, r, registerRequest) + h.handleNoiseAuthKey(writer, req, registerRequest) return } @@ -421,7 +427,10 @@ func (h *Headscale) handleNoiseNodeExpired( machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). Inc() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err := json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error().Caller().Err(err).Msg("Failed to encode response") + } } From 281ae59b5aa266d0110fa49159e370e884f912f2 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:18:33 +0200 Subject: [PATCH 11/63] Update integration tests to work with Noise protocol --- integration_test/etc/alt-config.dump.gold.yaml | 1 + integration_test/etc/alt-config.yaml | 1 + integration_test/etc/config.dump.gold.yaml | 1 + integration_test/etc_embedded_derp/config.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml index e8934230..e02d7067 100644 --- a/integration_test/etc/alt-config.dump.gold.yaml +++ b/integration_test/etc/alt-config.dump.gold.yaml @@ -38,6 +38,7 @@ oidc: - email strip_email_domain: true private_key_path: private.key +noise_private_key_path: noise_private.key server_url: http://headscale:18080 tls_client_auth_mode: relaxed tls_letsencrypt_cache_dir: /var/www/.cache diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml index fa1bfcb3..8a6d7396 100644 --- a/integration_test/etc/alt-config.yaml +++ b/integration_test/etc/alt-config.yaml @@ -14,6 +14,7 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key +noise_private_key_path: noise_private.key listen_addr: 0.0.0.0:18080 metrics_listen_addr: 127.0.0.1:19090 server_url: http://headscale:18080 diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml index 17bb0ca0..f474e890 100644 --- a/integration_test/etc/config.dump.gold.yaml +++ b/integration_test/etc/config.dump.gold.yaml @@ -38,6 +38,7 @@ oidc: - email strip_email_domain: true private_key_path: private.key +noise_private_key_path: noise_private.key server_url: http://headscale:8080 tls_client_auth_mode: relaxed tls_letsencrypt_cache_dir: /var/www/.cache diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml index e6ad3b00..86946116 100644 --- a/integration_test/etc_embedded_derp/config.yaml +++ b/integration_test/etc_embedded_derp/config.yaml @@ -14,6 +14,7 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key +noise_private_key_path: noise_private.key listen_addr: 0.0.0.0:8443 server_url: https://headscale:8443 tls_cert_path: "/etc/headscale/tls/server.crt" From ade1b7377972830030e4e3fe84996191e3a7faae Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:35:14 +0200 Subject: [PATCH 12/63] Output an error when a user runs headscale without noise_private_key_path defined --- cmd/headscale/cli/root.go | 4 ++-- config.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 270ca555..2c28c584 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -28,12 +28,12 @@ func initConfig() { if cfgFile != "" { err := headscale.LoadConfig(cfgFile, true) if err != nil { - log.Fatal().Caller().Err(err) + log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile) } } else { err := headscale.LoadConfig("", false) if err != nil { - log.Fatal().Caller().Err(err) + log.Fatal().Caller().Err(err).Msgf("Error loading config") } } diff --git a/config.go b/config.go index a792bab9..99e7a4cf 100644 --- a/config.go +++ b/config.go @@ -184,6 +184,10 @@ func LoadConfig(path string, isFile bool) error { errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n" } + if !viper.IsSet("noise_private_key_path") { + errorText += "Fatal config error: headscale now requires a new `noise_private_key_path` field in the config file for the Tailscale v2 protocol\n" + } + if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { From 3bea20850a7042fd7a8b209f509cef78854fc3f0 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:40:22 +0200 Subject: [PATCH 13/63] Some linting fixes --- noise.go | 5 +++-- noise_api.go | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/noise.go b/noise.go index bd7d4bbd..53deae33 100644 --- a/noise.go +++ b/noise.go @@ -113,6 +113,7 @@ func (h *Headscale) getNoiseConnection( if err != nil { log.Error().Caller().Err(err).Msgf("Hijack failed") http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError) + return nil, errCannotHijack } if err := conn.Flush(); err != nil { @@ -123,12 +124,12 @@ func (h *Headscale) getNoiseConnection( netConn = netutil.NewDrainBufConn(netConn, conn.Reader) - nc, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init) + noiseConn, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init) if err != nil { netConn.Close() return nil, errNoiseHandshakeFailed } - return nc, nil + return noiseConn, nil } diff --git a/noise_api.go b/noise_api.go index fc0f0911..f20305ca 100644 --- a/noise_api.go +++ b/noise_api.go @@ -22,6 +22,7 @@ func (h *Headscale) NoiseRegistrationHandler( log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr) if req.Method != http.MethodPost { http.Error(writer, "Wrong method", http.StatusMethodNotAllowed) + return } body, _ := io.ReadAll(req.Body) @@ -296,8 +297,8 @@ func (h *Headscale) handleNoiseAuthKey( } func (h *Headscale) handleNoiseNodeValidRegistration( - w http.ResponseWriter, - r *http.Request, + writer http.ResponseWriter, + req *http.Request, machine Machine, ) { resp := tailcfg.RegisterResponse{} @@ -314,9 +315,9 @@ func (h *Headscale) handleNoiseNodeValidRegistration( machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name). Inc() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(resp) } func (h *Headscale) handleNoiseMachineRegistrationNew( From eb8d8f142c88dbe71e990c6a7bbaba031f4217c2 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:44:07 +0200 Subject: [PATCH 14/63] And more linting stuff --- noise.go | 5 ++++- noise_api.go | 46 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/noise.go b/noise.go index 53deae33..16754c69 100644 --- a/noise.go +++ b/noise.go @@ -56,7 +56,10 @@ func (h *Headscale) NoiseUpgradeHandler( server := http.Server{} server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{}) - server.Serve(netutil.NewOneConnListener(noiseConn, nil)) + err = server.Serve(netutil.NewOneConnListener(noiseConn, nil)) + if err != nil { + log.Error().Err(err).Msg("noise server launch failed") + } } // getNoiseConnection is basically AcceptHTTP from tailscale diff --git a/noise_api.go b/noise_api.go index f20305ca..29ab98df 100644 --- a/noise_api.go +++ b/noise_api.go @@ -197,7 +197,13 @@ func (h *Headscale) handleNoiseAuthKey( writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(writer).Encode(resp) + err = json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to encode response") + } log.Error(). Caller(). @@ -234,7 +240,16 @@ func (h *Headscale) handleNoiseAuthKey( machine.NodeKey = nodeKey machine.AuthKeyID = uint(pak.ID) - h.RefreshMachine(machine, registerRequest.Expiry) + err = h.RefreshMachine(machine, registerRequest.Expiry) + if err != nil { + log.Error(). + Caller(). + Str("machine", machine.Hostname). + Err(err). + Msg("Failed to refresh machine") + + return + } } else { now := time.Now().UTC() @@ -277,7 +292,18 @@ func (h *Headscale) handleNoiseAuthKey( } } - h.UsePreAuthKey(pak) + err = h.UsePreAuthKey(pak) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to use pre-auth key") + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } resp.MachineAuthorized = true resp.User = *pak.Namespace.toUser() @@ -358,7 +384,17 @@ func (h *Headscale) handleNoiseNodeLogOut( Str("machine", machine.Hostname). Msg("Client requested logout") - h.ExpireMachine(&machine) + err := h.ExpireMachine(&machine) + if err != nil { + log.Error(). + Caller(). + Str("func", "handleMachineLogOut"). + Err(err). + Msg("Failed to expire machine") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } resp.AuthURL = "" resp.MachineAuthorized = false @@ -366,7 +402,7 @@ func (h *Headscale) handleNoiseNodeLogOut( writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - err := json.NewEncoder(writer).Encode(resp) + err = json.NewEncoder(writer).Encode(resp) if err != nil { log.Error(). Caller(). From 20d2615081e79a40c711476a4464c78dddc59b51 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 12:47:04 +0200 Subject: [PATCH 15/63] Check json encoder errors --- noise_api.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/noise_api.go b/noise_api.go index 29ab98df..6bf555e6 100644 --- a/noise_api.go +++ b/noise_api.go @@ -313,7 +313,15 @@ func (h *Headscale) handleNoiseAuthKey( writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - json.NewEncoder(writer).Encode(resp) + err = json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to encode response") + + return + } log.Info(). Caller(). @@ -343,7 +351,13 @@ func (h *Headscale) handleNoiseNodeValidRegistration( Inc() writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - json.NewEncoder(writer).Encode(resp) + err := json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to encode response") + } } func (h *Headscale) handleNoiseMachineRegistrationNew( @@ -370,7 +384,13 @@ func (h *Headscale) handleNoiseMachineRegistrationNew( writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - json.NewEncoder(writer).Encode(resp) + err := json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to encode response") + } } func (h *Headscale) handleNoiseNodeLogOut( @@ -430,7 +450,13 @@ func (h *Headscale) handleNoiseNodeRefreshKey( writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) - json.NewEncoder(writer).Encode(resp) + err := json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to encode response") + } } func (h *Headscale) handleNoiseNodeExpired( From ff46f3ff494aff9d13c367a735db537e7a073697 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 16:13:17 +0200 Subject: [PATCH 16/63] Move reusable method to common api file --- api.go | 74 ----------------------------------------------- api_common.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 74 deletions(-) create mode 100644 api_common.go diff --git a/api.go b/api.go index 86d77b6a..9006e759 100644 --- a/api.go +++ b/api.go @@ -436,80 +436,6 @@ func (h *Headscale) getLegacyMapResponseData( return data, nil } -func (h *Headscale) generateMapResponse( - mapRequest tailcfg.MapRequest, - machine *Machine, -) (*tailcfg.MapResponse, error) { - log.Trace(). - Str("func", "generateMapResponse"). - Str("machine", mapRequest.Hostinfo.Hostname). - Msg("Creating Map response") - node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) - if err != nil { - log.Error(). - Caller(). - Str("func", "generateMapResponse"). - Err(err). - Msg("Cannot convert to node") - - return nil, err - } - - peers, err := h.getValidPeers(machine) - if err != nil { - log.Error(). - Caller(). - Str("func", "generateMapResponse"). - Err(err). - Msg("Cannot fetch peers") - - return nil, err - } - - profiles := getMapResponseUserProfiles(*machine, peers) - - nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) - if err != nil { - log.Error(). - Caller(). - Str("func", "generateMapResponse"). - Err(err). - Msg("Failed to convert peers to Tailscale nodes") - - return nil, err - } - - dnsConfig := getMapResponseDNSConfig( - h.cfg.DNSConfig, - h.cfg.BaseDomain, - *machine, - peers, - ) - - resp := tailcfg.MapResponse{ - KeepAlive: false, - Node: node, - Peers: nodePeers, - DNSConfig: dnsConfig, - Domain: h.cfg.BaseDomain, - PacketFilter: h.aclRules, - DERPMap: h.DERPMap, - UserProfiles: profiles, - Debug: &tailcfg.Debug{ - DisableLogTail: !h.cfg.LogTail.Enabled, - RandomizeClientPort: h.cfg.RandomizeClientPort, - }, - } - - log.Trace(). - Str("func", "generateMapResponse"). - Str("machine", mapRequest.Hostinfo.Hostname). - // Interface("payload", resp). - Msgf("Generated map response: %s", tailMapResponseToString(resp)) - - return &resp, nil -} - func (h *Headscale) getMapKeepAliveResponse( machineKey key.MachinePublic, mapRequest tailcfg.MapRequest, diff --git a/api_common.go b/api_common.go new file mode 100644 index 00000000..5ffbed02 --- /dev/null +++ b/api_common.go @@ -0,0 +1,80 @@ +package headscale + +import ( + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" +) + +func (h *Headscale) generateMapResponse( + mapRequest tailcfg.MapRequest, + machine *Machine, +) (*tailcfg.MapResponse, error) { + log.Trace(). + Str("func", "generateMapResponse"). + Str("machine", mapRequest.Hostinfo.Hostname). + Msg("Creating Map response") + node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Cannot convert to node") + + return nil, err + } + + peers, err := h.getValidPeers(machine) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Cannot fetch peers") + + return nil, err + } + + profiles := getMapResponseUserProfiles(*machine, peers) + + nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) + if err != nil { + log.Error(). + Caller(). + Str("func", "generateMapResponse"). + Err(err). + Msg("Failed to convert peers to Tailscale nodes") + + return nil, err + } + + dnsConfig := getMapResponseDNSConfig( + h.cfg.DNSConfig, + h.cfg.BaseDomain, + *machine, + peers, + ) + + resp := tailcfg.MapResponse{ + KeepAlive: false, + Node: node, + Peers: nodePeers, + DNSConfig: dnsConfig, + Domain: h.cfg.BaseDomain, + PacketFilter: h.aclRules, + DERPMap: h.DERPMap, + UserProfiles: profiles, + Debug: &tailcfg.Debug{ + DisableLogTail: !h.cfg.LogTail.Enabled, + RandomizeClientPort: h.cfg.RandomizeClientPort, + }, + } + + log.Trace(). + Str("func", "generateMapResponse"). + Str("machine", mapRequest.Hostinfo.Hostname). + // Interface("payload", resp). + Msgf("Generated map response: %s", tailMapResponseToString(resp)) + + return &resp, nil +} From cab828c9d49dfa2fdf8c94e9156719befd2b58f6 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 16:52:57 +0200 Subject: [PATCH 17/63] Fixed unit tests to load config --- cmd/headscale/headscale_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 917ae432..b0667f78 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -165,7 +165,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { // defer os.RemoveAll(tmpDir) configYaml := []byte( - "---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"", + "---\nnoise_private_key_path: \"noise_private.key\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"\n", ) writeConfig(c, tmpDir, configYaml) @@ -192,7 +192,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { // Check configuration validation errors (2) configYaml = []byte( - "---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", + "---\nnoise_private_key_path: \"noise_private.key\"\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", ) writeConfig(c, tmpDir, configYaml) err = headscale.LoadConfig(tmpDir, false) From 78a179c9719dc5d2c2e6de0ed7812d8d2163fefc Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 16:53:54 +0200 Subject: [PATCH 18/63] Minor update in docs --- docs/running-headscale-container.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index b36f3bbf..4a9f1518 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -53,6 +53,8 @@ server_url: http://your-host-name:8080 # Change to your hostname or host IP metrics_listen_addr: 0.0.0.0:9090 # The default /var/lib/headscale path is not writable in the container private_key_path: /etc/headscale/private.key +# The default /var/lib/headscale path is not writable in the container +noise_private_key_path: /var/lib/headscale/noise_private.key # The default /var/lib/headscale path is not writable in the container db_path: /etc/headscale/db.sqlite ``` From 0d0042b7e6a61d0cf6564351c68ec76e1910c01b Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 17:04:07 +0200 Subject: [PATCH 19/63] Added zstd constant for linting --- api.go | 4 ++-- utils.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index 9006e759..6324cca1 100644 --- a/api.go +++ b/api.go @@ -407,7 +407,7 @@ func (h *Headscale) getLegacyMapResponseData( } var respBody []byte - if mapRequest.Compress == "zstd" { + if mapRequest.Compress == ZstdCompression { src, err := json.Marshal(resp) if err != nil { log.Error(). @@ -445,7 +445,7 @@ func (h *Headscale) getMapKeepAliveResponse( } var respBody []byte var err error - if mapRequest.Compress == "zstd" { + if mapRequest.Compress == ZstdCompression { src, err := json.Marshal(mapResponse) if err != nil { log.Error(). diff --git a/utils.go b/utils.go index b4362535..089e867b 100644 --- a/utils.go +++ b/utils.go @@ -59,6 +59,8 @@ const ( privateHexPrefix = "privkey:" PermissionFallback = 0o700 + + ZstdCompression = "zstd" ) func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string { From c10142f7670655cd70b7f42c1503d18b64ec5ac3 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 17:05:04 +0200 Subject: [PATCH 20/63] Added noise poll handler --- app.go | 2 +- noise_poll.go | 698 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 noise_poll.go diff --git a/app.go b/app.go index 9c0f11d4..f632264c 100644 --- a/app.go +++ b/app.go @@ -467,7 +467,7 @@ func (h *Headscale) createNoiseMux() *mux.Router { router := mux.NewRouter() router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost) - //router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost) + router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost) return router } diff --git a/noise_poll.go b/noise_poll.go new file mode 100644 index 00000000..1489fff0 --- /dev/null +++ b/noise_poll.go @@ -0,0 +1,698 @@ +package headscale + +import ( + "context" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/klauspost/compress/zstd" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol +// +// This is the busiest endpoint, as it keeps the HTTP long poll that updates +// the clients when something in the network changes. +// +// The clients POST stuff like HostInfo and their Endpoints here, but +// only after their first request (marked with the ReadOnly field). +// +// At this moment the updates are sent in a quite horrendous way, but they kinda work. +func (h *Headscale) NoisePollNetMapHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace(). + Str("handler", "NoisePollNetMap"). + Msg("PollNetMapHandler called") + body, _ := io.ReadAll(req.Body) + + mapRequest := tailcfg.MapRequest{} + if err := json.Unmarshal(body, &mapRequest); err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse MapRequest") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{}) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "NoisePollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusNotFound) + + return + } + log.Error(). + Str("handler", "NoisePollNetMap"). + Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + log.Trace(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Found machine in database") + + machine.Hostname = mapRequest.Hostinfo.Hostname + machine.HostInfo = HostInfo(*mapRequest.Hostinfo) + machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey) + now := time.Now().UTC() + + // update ACLRules with peer informations (to update server tags if necessary) + if h.aclPolicy != nil { + err = h.UpdateACLRules() + if err != nil { + log.Error(). + Caller(). + Str("func", "handleAuthKey"). + Str("machine", machine.Hostname). + Err(err) + } + } + + // From Tailscale client: + // + // ReadOnly is whether the client just wants to fetch the MapResponse, + // without updating their Endpoints. The Endpoints field will be ignored and + // LastSeen will not be updated and peers will not be notified of changes. + // + // The intended use is for clients to discover the DERP map at start-up + // before their first real endpoint update. + if !mapRequest.ReadOnly { + machine.Endpoints = mapRequest.Endpoints + machine.LastSeen = &now + } + + if err := h.db.Updates(machine).Error; err != nil { + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Err(err). + Msg("Failed to persist/update machine in the database") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + } + + resp, err := h.getNoiseMapResponse(mapRequest, machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Err(err). + Msg("Failed to get Map response") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + // We update our peers if the client is not sending ReadOnly in the MapRequest + // so we don't distribute its initial request (it comes with + // empty endpoints to peers) + + // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 + log.Debug(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Bool("readOnly", mapRequest.ReadOnly). + Bool("omitPeers", mapRequest.OmitPeers). + Bool("stream", mapRequest.Stream). + Msg("Noise client map request processed") + + if mapRequest.ReadOnly { + log.Info(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Client is starting up. Probably interested in a DERP map") + // w.Header().Set("Content-Type", "application/json") + // w.WriteHeader(http.StatusOK) + _, err = writer.Write(resp) + if err != nil { + log.Warn().Msgf("Could not send JSON response: %s", err) + } + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + log.Info().Msgf("Noise client map response sent for %s (len %d)", machine.Hostname, len(resp)) + + return + } + + // There has been an update to _any_ of the nodes that the other nodes would + // need to know about + h.setLastStateChangeToNow(machine.Namespace.Name) + + // The request is not ReadOnly, so we need to set up channels for updating + // peers via longpoll + + // Only create update channel if it has not been created + log.Trace(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Loading or creating update channel") + + const chanSize = 8 + updateChan := make(chan struct{}, chanSize) + + pollDataChan := make(chan []byte, chanSize) + defer closeChanWithLog(pollDataChan, machine.Hostname, "pollDataChan") + + keepAliveChan := make(chan []byte) + + if mapRequest.OmitPeers && !mapRequest.Stream { + log.Info(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Client sent endpoint update and is ok with a response without peer list") + + _, err := writer.Write(resp) + if err != nil { + log.Warn().Msgf("Could not send response: %s", err) + + return + } + + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + // It sounds like we should update the nodes when we have received a endpoint update + // even tho the comments in the tailscale code dont explicitly say so. + updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "endpoint-update"). + Inc() + updateChan <- struct{}{} + + return + } else if mapRequest.OmitPeers && mapRequest.Stream { + log.Warn(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Ignoring request, don't know how to handle it") + http.Error(writer, "Internal error", http.StatusBadRequest) + + return + } + + log.Info(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Client is ready to access the tailnet") + log.Info(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Sending initial map") + pollDataChan <- resp + + log.Info(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Notifying peers") + updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update"). + Inc() + updateChan <- struct{}{} + + h.NoisePollNetMapStream( + writer, + req, + machine, + mapRequest, + pollDataChan, + keepAliveChan, + updateChan, + ) + + log.Trace(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Finished stream, closing PollNetMap session") +} + +// PollNetMapStream takes care of /machine/:id/map +// stream logic, ensuring we communicate updates and data +// to the connected clients. +func (h *Headscale) NoisePollNetMapStream( + writer http.ResponseWriter, + req *http.Request, + machine *Machine, + mapRequest tailcfg.MapRequest, + pollDataChan chan []byte, + keepAliveChan chan []byte, + updateChan chan struct{}, +) { + ctx := context.WithValue(context.Background(), machineNameContextKey, machine.Hostname) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go h.noiseScheduledPollWorker( + ctx, + updateChan, + keepAliveChan, + mapRequest, + machine, + ) + + for { + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Msg("Waiting for data to stream...") + + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan) + + select { + case data := <-pollDataChan: + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Int("bytes", len(data)). + Msg("Sending data received via pollData channel") + _, err := writer.Write(data) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Err(err). + Msg("Cannot write data") + + break + } + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Int("bytes", len(data)). + Msg("Data from pollData channel written successfully") + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachineFromDatabase(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Err(err). + Msg("Cannot update machine from database") + + // client has been removed from database + // since the stream opened, terminate connection. + break + } + now := time.Now().UTC() + machine.LastSeen = &now + + lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname). + Set(float64(now.Unix())) + machine.LastSuccessfulUpdate = &now + + err = h.TouchMachine(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Err(err). + Msg("Cannot update machine LastSuccessfulUpdate") + } else { + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Int("bytes", len(data)). + Msg("Machine entry in database updated successfully after sending pollData") + } + + break + + case data := <-keepAliveChan: + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Int("bytes", len(data)). + Msg("Sending keep alive message") + + _, err := writer.Write(data) + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Err(err). + Msg("Cannot write keep alive message") + + break + } + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Int("bytes", len(data)). + Msg("Keep alive sent successfully") + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachineFromDatabase(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Err(err). + Msg("Cannot update machine from database") + + // client has been removed from database + // since the stream opened, terminate connection. + break + } + now := time.Now().UTC() + machine.LastSeen = &now + err = h.TouchMachine(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Err(err). + Msg("Cannot update machine LastSeen") + } else { + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Int("bytes", len(data)). + Msg("Machine updated successfully after sending keep alive") + } + + break + + case <-updateChan: + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Msg("Received a request for update") + updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname). + Inc() + if h.isOutdated(machine) { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } + log.Debug(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Time("last_successful_update", lastUpdate). + Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). + Msgf("There has been updates since the last successful update to %s", machine.Hostname) + data, err := h.getNoiseMapResponse(mapRequest, machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Err(err). + Msg("Could not get the map update") + } + _, err = writer.Write(data) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Err(err). + Msg("Could not write the map response") + updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed"). + Inc() + + break + } + + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Msg("Updated Map has been sent") + updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "success"). + Inc() + + // Keep track of the last successful update, + // we sometimes end in a state were the update + // is not picked up by a client and we use this + // to determine if we should "force" an update. + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachineFromDatabase(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Err(err). + Msg("Cannot update machine from database") + + // client has been removed from database + // since the stream opened, terminate connection. + break + } + now := time.Now().UTC() + + lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname). + Set(float64(now.Unix())) + machine.LastSuccessfulUpdate = &now + + err = h.TouchMachine(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "update"). + Err(err). + Msg("Cannot update machine LastSuccessfulUpdate") + } + } else { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } + log.Trace(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Time("last_successful_update", lastUpdate). + Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). + Msgf("%s is up to date", machine.Hostname) + } + + break + + case <-ctx.Done(): + log.Info(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Msg("The client has closed the connection") + // TODO: Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err := h.UpdateMachineFromDatabase(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "Done"). + Err(err). + Msg("Cannot update machine from database") + + // client has been removed from database + // since the stream opened, terminate connection. + break + } + now := time.Now().UTC() + machine.LastSeen = &now + err = h.TouchMachine(machine) + if err != nil { + log.Error(). + Str("handler", "NoisePollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "Done"). + Err(err). + Msg("Cannot update machine LastSeen") + } + + break + } + } +} + +func (h *Headscale) noiseScheduledPollWorker( + ctx context.Context, + updateChan chan struct{}, + keepAliveChan chan []byte, + mapRequest tailcfg.MapRequest, + machine *Machine, +) { + keepAliveTicker := time.NewTicker(keepAliveInterval) + updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval) + + defer closeChanWithLog( + updateChan, + fmt.Sprint(ctx.Value(machineNameContextKey)), + "updateChan", + ) + defer closeChanWithLog( + keepAliveChan, + fmt.Sprint(ctx.Value(machineNameContextKey)), + "updateChan", + ) + + for { + select { + case <-ctx.Done(): + return + + case <-keepAliveTicker.C: + data, err := h.getNoiseMapKeepAliveResponse(mapRequest) + if err != nil { + log.Error(). + Str("func", "keepAlive"). + Err(err). + Msg("Error generating the keep alive msg") + + return + } + + log.Debug(). + Str("func", "keepAlive"). + Str("machine", machine.Hostname). + Msg("Sending keepalive") + keepAliveChan <- data + + case <-updateCheckerTicker.C: + log.Debug(). + Str("func", "scheduledPollWorker"). + Str("machine", machine.Hostname). + Msg("Sending update request") + updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update"). + Inc() + updateChan <- struct{}{} + } + } +} + +func (h *Headscale) getNoiseMapKeepAliveResponse(req tailcfg.MapRequest) ([]byte, error) { + resp := tailcfg.MapResponse{ + KeepAlive: true, + } + + // The TS2021 protocol does not rely anymore on the machine key to + // encrypt in a NaCl box the map response. We just send it back + // unencrypted via the encrypted Noise channel. + // declare the incoming size on the first 4 bytes + respBody, err := json.Marshal(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot marshal map response") + } + + var srcCompressed []byte + if req.Compress == ZstdCompression { + encoder, _ := zstd.NewWriter(nil) + srcCompressed = encoder.EncodeAll(respBody, nil) + } else { + srcCompressed = respBody + } + + data := make([]byte, reservedResponseHeaderSize) + binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed))) + data = append(data, srcCompressed...) + + return data, nil +} + +func (h *Headscale) getNoiseMapResponse( + req tailcfg.MapRequest, + machine *Machine, +) ([]byte, error) { + log.Trace(). + Str("func", "getNoiseMapResponse"). + Str("machine", req.Hostinfo.Hostname). + Msg("Creating Map response") + + resp, err := h.generateMapResponse(req, machine) + if err != nil { + log.Error(). + Str("func", "getNoiseMapResponse"). + Err(err). + Msg("Error generating the map response") + + return nil, err + } + + log.Trace(). + Str("func", "getNoiseMapResponse"). + Str("machine", req.Hostinfo.Hostname). + Msgf("Generated map response: %s", tailMapResponseToString(*resp)) + + // The TS2021 protocol does not rely anymore on the machine key to + // encrypt in a NaCl box the map response. We just send it back + // unencrypted via the encrypted Noise channel. + // declare the incoming size on the first 4 bytes + respBody, err := json.Marshal(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot marshal map response") + } + + var srcCompressed []byte + if req.Compress == ZstdCompression { + encoder, _ := zstd.NewWriter(nil) + srcCompressed = encoder.EncodeAll(respBody, nil) + } else { + srcCompressed = respBody + } + + data := make([]byte, reservedResponseHeaderSize) + binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed))) + data = append(data, srcCompressed...) + + return data, nil +} From 0f09e19e38fe2fa4c6156a9c8a8450817b9c5c2f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 17:09:14 +0200 Subject: [PATCH 21/63] Updated go.mod checksum --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0e8bdc94..0c257ff5 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorSha256 = "sha256-RzmnAh81BN4tbzAGzJbb6CMuws8kuPJDw7aPkRRnSS8="; + vendorSha256 = "sha256-1VYegqEearzbqEX8ZLbsvHrRKbM/HIm/XIqQjMbvxkA="; ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ]; }; From aaa33cf09345bdfba43e797cc8fd6efc66dbd411 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:07:05 +0200 Subject: [PATCH 22/63] Minor change in router --- api.go | 1 + app.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api.go b/api.go index 27cbede4..5ac0d3d9 100644 --- a/api.go +++ b/api.go @@ -679,6 +679,7 @@ func (h *Headscale) handleMachineRegistrationNew( // The machine registration is new, redirect the client to the registration URL log.Debug(). + Caller(). Str("machine", registerRequest.Hostinfo.Hostname). Msg("The node seems to be new, sending auth url") if h.cfg.OIDC.Issuer != "" { diff --git a/app.go b/app.go index f632264c..a5015c98 100644 --- a/app.go +++ b/app.go @@ -467,7 +467,7 @@ func (h *Headscale) createNoiseMux() *mux.Router { router := mux.NewRouter() router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost) - router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost) + router.HandleFunc("/machine/map", h.NoisePollNetMapHandler) return router } From ab18c721bb45143e93aa04f3d42fe5e9abe6dd42 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:07:29 +0200 Subject: [PATCH 23/63] Support for Noise machines in getPeers --- machine.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/machine.go b/machine.go index b436fd87..d9fd789d 100644 --- a/machine.go +++ b/machine.go @@ -600,11 +600,14 @@ func (machine Machine) toNode( } var machineKey key.MachinePublic - err = machineKey.UnmarshalText( - []byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)), - ) - if err != nil { - return nil, fmt.Errorf("failed to parse machine public key: %w", err) + if machine.MachineKey != "" { + // MachineKey is only used in the legacy protocol + err = machineKey.UnmarshalText( + []byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)), + ) + if err != nil { + return nil, fmt.Errorf("failed to parse machine public key: %w", err) + } } var discoKey key.DiscoPublic From e640c6df05d6cde2d484314a89359f33f2124c48 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:10:08 +0200 Subject: [PATCH 24/63] Fixes in Noise poll (clients should work now) --- noise_api.go | 3 ++- noise_poll.go | 56 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/noise_api.go b/noise_api.go index 6bf555e6..98a66d22 100644 --- a/noise_api.go +++ b/noise_api.go @@ -369,8 +369,9 @@ func (h *Headscale) handleNoiseMachineRegistrationNew( // The machine registration is new, redirect the client to the registration URL log.Debug(). + Caller(). Str("machine", registerRequest.Hostinfo.Hostname). - Msg("The node is sending us a new NodeKey, sending auth url") + Msg("The node seems to be new, sending auth url") if h.cfg.OIDC.Issuer != "" { resp.AuthURL = fmt.Sprintf( "%s/oidc/register/%s", diff --git a/noise_poll.go b/noise_poll.go index 1489fff0..1d9dfe37 100644 --- a/noise_poll.go +++ b/noise_poll.go @@ -298,10 +298,19 @@ func (h *Headscale) NoisePollNetMapStream( Err(err). Msg("Cannot write data") - break + return } - if f, ok := writer.(http.Flusher); ok { - f.Flush() + + flusher, ok := writer.(http.Flusher) + if !ok { + log.Error(). + Caller(). + Str("handler", "PollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "pollData"). + Msg("Cannot cast writer to http.Flusher") + } else { + flusher.Flush() } log.Trace(). Str("handler", "NoisePollNetMapStream"). @@ -323,7 +332,7 @@ func (h *Headscale) NoisePollNetMapStream( // client has been removed from database // since the stream opened, terminate connection. - break + return } now := time.Now().UTC() machine.LastSeen = &now @@ -353,27 +362,34 @@ func (h *Headscale) NoisePollNetMapStream( case data := <-keepAliveChan: log.Trace(). - Str("handler", "NoisePollNetMapStream"). + Str("handler", "PollNetMapStream"). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Int("bytes", len(data)). Msg("Sending keep alive message") - _, err := writer.Write(data) - if f, ok := writer.(http.Flusher); ok { - f.Flush() - } - if err != nil { log.Error(). - Str("handler", "NoisePollNetMapStream"). + Str("handler", "PollNetMapStream"). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Err(err). Msg("Cannot write keep alive message") - break + return } + flusher, ok := writer.(http.Flusher) + if !ok { + log.Error(). + Caller(). + Str("handler", "PollNetMapStream"). + Str("machine", machine.Hostname). + Str("channel", "keepAlive"). + Msg("Cannot cast writer to http.Flusher") + } else { + flusher.Flush() + } + log.Trace(). Str("handler", "NoisePollNetMapStream"). Str("machine", machine.Hostname). @@ -394,7 +410,7 @@ func (h *Headscale) NoisePollNetMapStream( // client has been removed from database // since the stream opened, terminate connection. - break + return } now := time.Now().UTC() machine.LastSeen = &now @@ -415,7 +431,7 @@ func (h *Headscale) NoisePollNetMapStream( Msg("Machine updated successfully after sending keep alive") } - break + return case <-updateChan: log.Trace(). @@ -456,7 +472,7 @@ func (h *Headscale) NoisePollNetMapStream( updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed"). Inc() - break + return } if f, ok := writer.(http.Flusher); ok { @@ -489,7 +505,7 @@ func (h *Headscale) NoisePollNetMapStream( // client has been removed from database // since the stream opened, terminate connection. - break + return } now := time.Now().UTC() @@ -519,7 +535,7 @@ func (h *Headscale) NoisePollNetMapStream( Msgf("%s is up to date", machine.Hostname) } - break + return case <-ctx.Done(): log.Info(). @@ -540,7 +556,7 @@ func (h *Headscale) NoisePollNetMapStream( // client has been removed from database // since the stream opened, terminate connection. - break + return } now := time.Now().UTC() machine.LastSeen = &now @@ -554,7 +570,7 @@ func (h *Headscale) NoisePollNetMapStream( Msg("Cannot update machine LastSeen") } - break + return } } } @@ -606,7 +622,7 @@ func (h *Headscale) noiseScheduledPollWorker( log.Debug(). Str("func", "scheduledPollWorker"). Str("machine", machine.Hostname). - Msg("Sending update request") + Msg("Sending noise update request") updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update"). Inc() updateChan <- struct{}{} From d0898ecabce313c09647dd1594fa2be53a546e1b Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:15:58 +0200 Subject: [PATCH 25/63] Move common parts of the protocol to dedicated file --- api.go | 281 --------------------------------------- protocol_common.go | 66 +++++++++ protocol_common_utils.go | 47 +++++++ 3 files changed, 113 insertions(+), 281 deletions(-) create mode 100644 protocol_common.go create mode 100644 protocol_common_utils.go diff --git a/api.go b/api.go index 5ac0d3d9..38e8e64b 100644 --- a/api.go +++ b/api.go @@ -4,19 +4,15 @@ import ( "bytes" "encoding/binary" "encoding/json" - "errors" "fmt" "html/template" - "io" "net/http" - "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/klauspost/compress/zstd" "github.com/rs/zerolog/log" - "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" ) @@ -77,62 +73,6 @@ func (h *Headscale) HealthHandler( respond(nil) } -// KeyHandler provides the Headscale pub key -// Listens in /key. -func (h *Headscale) KeyHandler( - writer http.ResponseWriter, - req *http.Request, -) { - // New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion - clientCapabilityStr := req.URL.Query().Get("v") - if clientCapabilityStr != "" { - clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr) - if err != nil { - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - writer.WriteHeader(http.StatusBadRequest) - _, err := writer.Write([]byte("Wrong params")) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - return - } - - if clientCapabilityVersion >= NoiseCapabilityVersion { - // Tailscale has a different key for the TS2021 protocol - resp := tailcfg.OverTLSPublicKeyResponse{ - LegacyPublicKey: h.privateKey.Public(), - PublicKey: h.noisePrivateKey.Public(), - } - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err = json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - return - } - } - - // Old clients don't send a 'v' parameter, so we send the legacy public key - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public()))) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - type registerWebAPITemplateConfig struct { Key string } @@ -211,191 +151,6 @@ func (h *Headscale) RegisterWebAPI( } } -// RegistrationHandler handles the actual registration process of a machine -// Endpoint /machine/:mkey. -func (h *Headscale) RegistrationHandler( - writer http.ResponseWriter, - req *http.Request, -) { - vars := mux.Vars(req) - machineKeyStr, ok := vars["mkey"] - if !ok || machineKeyStr == "" { - log.Error(). - Str("handler", "RegistrationHandler"). - Msg("No machine ID in request") - http.Error(writer, "No machine ID in request", http.StatusBadRequest) - - return - } - - body, _ := io.ReadAll(req.Body) - - var machineKey key.MachinePublic - err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot parse machine key") - machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() - http.Error(writer, "Cannot parse machine key", http.StatusBadRequest) - - return - } - registerRequest := tailcfg.RegisterRequest{} - err = decode(body, ®isterRequest, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot decode message") - machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() - http.Error(writer, "Cannot decode message", http.StatusBadRequest) - - return - } - - now := time.Now().UTC() - machine, err := h.GetMachineByMachineKey(machineKey) - if errors.Is(err, gorm.ErrRecordNotFound) { - machineKeyStr := MachinePublicKeyStripPrefix(machineKey) - - // If the machine has AuthKey set, handle registration via PreAuthKeys - if registerRequest.Auth.AuthKey != "" { - h.handleAuthKey(writer, req, machineKey, registerRequest) - - return - } - - // Check if the node is waiting for interactive login. - // - // TODO(juan): We could use this field to improve our protocol implementation, - // and hold the request until the client closes it, or the interactive - // login is completed (i.e., the user registers the machine). - // This is not implemented yet, as it is no strictly required. The only side-effect - // is that the client will hammer headscale with requests until it gets a - // successful RegisterResponse. - if registerRequest.Followup != "" { - if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("Machine is waiting for interactive login") - - ticker := time.NewTicker(registrationHoldoff) - select { - case <-req.Context().Done(): - return - case <-ticker.C: - h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) - - return - } - } - } - - log.Info(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("New machine not yet in the database") - - givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err) - - return - } - - // The machine did not have a key to authenticate, which means - // that we rely on a method that calls back some how (OpenID or CLI) - // We create the machine and then keep it around until a callback - // happens - newMachine := Machine{ - MachineKey: machineKeyStr, - Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, - NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), - LastSeen: &now, - Expiry: &time.Time{}, - } - - if !registerRequest.Expiry.IsZero() { - log.Trace(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Time("expiry", registerRequest.Expiry). - Msg("Non-zero expiry time requested") - newMachine.Expiry = ®isterRequest.Expiry - } - - h.registrationCache.Set( - newMachine.NodeKey, - newMachine, - registerCacheExpiration, - ) - - h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) - - return - } - - // The machine is already registered, so we need to pass through reauth or key update. - if machine != nil { - // If the NodeKey stored in headscale is the same as the key presented in a registration - // request, then we have a node that is either: - // - Trying to log out (sending a expiry in the past) - // - A valid, registered machine, looking for the node map - // - Expired machine wanting to reauthenticate - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { - // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) - // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 - if !registerRequest.Expiry.IsZero() && - registerRequest.Expiry.UTC().Before(now) { - h.handleMachineLogOut(writer, req, machineKey, *machine) - - return - } - - // If machine is not expired, and is register, we have a already accepted this machine, - // let it proceed with a valid registration - if !machine.isExpired() { - h.handleMachineValidRegistration(writer, req, machineKey, *machine) - - return - } - } - - // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && - !machine.isExpired() { - h.handleMachineRefreshKey( - writer, - req, - machineKey, - registerRequest, - *machine, - ) - - return - } - - // The machine has expired - h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine) - - return - } -} - func (h *Headscale) getLegacyMapResponseData( machineKey key.MachinePublic, mapRequest tailcfg.MapRequest, @@ -436,42 +191,6 @@ func (h *Headscale) getLegacyMapResponseData( return data, nil } -func (h *Headscale) getMapKeepAliveResponse( - machineKey key.MachinePublic, - mapRequest tailcfg.MapRequest, -) ([]byte, error) { - mapResponse := tailcfg.MapResponse{ - KeepAlive: true, - } - var respBody []byte - var err error - if mapRequest.Compress == ZstdCompression { - src, err := json.Marshal(mapResponse) - if err != nil { - log.Error(). - Caller(). - Str("func", "getMapKeepAliveResponse"). - Err(err). - Msg("Failed to marshal keepalive response for the client") - - return nil, err - } - encoder, _ := zstd.NewWriter(nil) - srcCompressed := encoder.EncodeAll(src, nil) - respBody = h.privateKey.SealTo(machineKey, srcCompressed) - } else { - respBody, err = encode(mapResponse, &machineKey, h.privateKey) - if err != nil { - return nil, err - } - } - data := make([]byte, reservedResponseHeaderSize) - binary.LittleEndian.PutUint32(data, uint32(len(respBody))) - data = append(data, respBody...) - - return data, nil -} - func (h *Headscale) handleMachineLogOut( writer http.ResponseWriter, req *http.Request, diff --git a/protocol_common.go b/protocol_common.go new file mode 100644 index 00000000..c8eab80e --- /dev/null +++ b/protocol_common.go @@ -0,0 +1,66 @@ +package headscale + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" +) + +// KeyHandler provides the Headscale pub key +// Listens in /key. +func (h *Headscale) KeyHandler( + writer http.ResponseWriter, + req *http.Request, +) { + // New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion + clientCapabilityStr := req.URL.Query().Get("v") + if clientCapabilityStr != "" { + clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr) + if err != nil { + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + writer.WriteHeader(http.StatusBadRequest) + _, err := writer.Write([]byte("Wrong params")) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + + if clientCapabilityVersion >= NoiseCapabilityVersion { + // Tailscale has a different key for the TS2021 protocol + resp := tailcfg.OverTLSPublicKeyResponse{ + LegacyPublicKey: h.privateKey.Public(), + PublicKey: h.noisePrivateKey.Public(), + } + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + err = json.NewEncoder(writer).Encode(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + } + + // Old clients don't send a 'v' parameter, so we send the legacy public key + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public()))) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } +} diff --git a/protocol_common_utils.go b/protocol_common_utils.go new file mode 100644 index 00000000..5939b6a4 --- /dev/null +++ b/protocol_common_utils.go @@ -0,0 +1,47 @@ +package headscale + +import ( + "encoding/binary" + "encoding/json" + + "github.com/klauspost/compress/zstd" + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +func (h *Headscale) getMapKeepAliveResponse( + machineKey key.MachinePublic, + mapRequest tailcfg.MapRequest, +) ([]byte, error) { + mapResponse := tailcfg.MapResponse{ + KeepAlive: true, + } + var respBody []byte + var err error + if mapRequest.Compress == ZstdCompression { + src, err := json.Marshal(mapResponse) + if err != nil { + log.Error(). + Caller(). + Str("func", "getMapKeepAliveResponse"). + Err(err). + Msg("Failed to marshal keepalive response for the client") + + return nil, err + } + encoder, _ := zstd.NewWriter(nil) + srcCompressed := encoder.EncodeAll(src, nil) + respBody = h.privateKey.SealTo(machineKey, srcCompressed) + } else { + respBody, err = encode(mapResponse, &machineKey, h.privateKey) + if err != nil { + return nil, err + } + } + data := make([]byte, reservedResponseHeaderSize) + binary.LittleEndian.PutUint32(data, uint32(len(respBody))) + data = append(data, respBody...) + + return data, nil +} From db89fdea23b5e82a43d0f0d38d0de47e75f35543 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:16:29 +0200 Subject: [PATCH 26/63] Added file for legacy protocol --- protocol_legacy.go | 199 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 protocol_legacy.go diff --git a/protocol_legacy.go b/protocol_legacy.go new file mode 100644 index 00000000..5c23b172 --- /dev/null +++ b/protocol_legacy.go @@ -0,0 +1,199 @@ +package headscale + +import ( + "errors" + "io" + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// RegistrationHandler handles the actual registration process of a machine +// Endpoint /machine/:mkey. +func (h *Headscale) RegistrationHandler( + writer http.ResponseWriter, + req *http.Request, +) { + vars := mux.Vars(req) + machineKeyStr, ok := vars["mkey"] + if !ok || machineKeyStr == "" { + log.Error(). + Str("handler", "RegistrationHandler"). + Msg("No machine ID in request") + http.Error(writer, "No machine ID in request", http.StatusBadRequest) + + return + } + + body, _ := io.ReadAll(req.Body) + + var machineKey key.MachinePublic + err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse machine key") + machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() + http.Error(writer, "Cannot parse machine key", http.StatusBadRequest) + + return + } + registerRequest := tailcfg.RegisterRequest{} + err = decode(body, ®isterRequest, &machineKey, h.privateKey) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot decode message") + machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() + http.Error(writer, "Cannot decode message", http.StatusBadRequest) + + return + } + + now := time.Now().UTC() + machine, err := h.GetMachineByMachineKey(machineKey) + if errors.Is(err, gorm.ErrRecordNotFound) { + machineKeyStr := MachinePublicKeyStripPrefix(machineKey) + + // If the machine has AuthKey set, handle registration via PreAuthKeys + if registerRequest.Auth.AuthKey != "" { + h.handleAuthKey(writer, req, machineKey, registerRequest) + + return + } + + // Check if the node is waiting for interactive login. + // + // TODO(juan): We could use this field to improve our protocol implementation, + // and hold the request until the client closes it, or the interactive + // login is completed (i.e., the user registers the machine). + // This is not implemented yet, as it is no strictly required. The only side-effect + // is that the client will hammer headscale with requests until it gets a + // successful RegisterResponse. + if registerRequest.Followup != "" { + if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("Machine is waiting for interactive login") + + ticker := time.NewTicker(registrationHoldoff) + select { + case <-req.Context().Done(): + return + case <-ticker.C: + h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) + + return + } + } + } + + log.Info(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("New machine not yet in the database") + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.Hostinfo.Hostname). + Err(err) + + return + } + + // The machine did not have a key to authenticate, which means + // that we rely on a method that calls back some how (OpenID or CLI) + // We create the machine and then keep it around until a callback + // happens + newMachine := Machine{ + MachineKey: machineKeyStr, + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), + LastSeen: &now, + Expiry: &time.Time{}, + } + + if !registerRequest.Expiry.IsZero() { + log.Trace(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Time("expiry", registerRequest.Expiry). + Msg("Non-zero expiry time requested") + newMachine.Expiry = ®isterRequest.Expiry + } + + h.registrationCache.Set( + newMachine.NodeKey, + newMachine, + registerCacheExpiration, + ) + + h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) + + return + } + + // The machine is already registered, so we need to pass through reauth or key update. + if machine != nil { + // If the NodeKey stored in headscale is the same as the key presented in a registration + // request, then we have a node that is either: + // - Trying to log out (sending a expiry in the past) + // - A valid, registered machine, looking for the node map + // - Expired machine wanting to reauthenticate + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { + // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) + // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 + if !registerRequest.Expiry.IsZero() && + registerRequest.Expiry.UTC().Before(now) { + h.handleMachineLogOut(writer, req, machineKey, *machine) + + return + } + + // If machine is not expired, and is register, we have a already accepted this machine, + // let it proceed with a valid registration + if !machine.isExpired() { + h.handleMachineValidRegistration(writer, req, machineKey, *machine) + + return + } + } + + // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && + !machine.isExpired() { + h.handleMachineRefreshKey( + writer, + req, + machineKey, + registerRequest, + *machine, + ) + + return + } + + // The machine has expired + h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine) + + return + } +} From 35f3dee1d0015b9280e689f269144510ec925b69 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 21:19:52 +0200 Subject: [PATCH 27/63] Move Noise API to new file --- noise_api.go => protocol_noise.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename noise_api.go => protocol_noise.go (100%) diff --git a/noise_api.go b/protocol_noise.go similarity index 100% rename from noise_api.go rename to protocol_noise.go From f4bab6b2901b7486813b8c59e8217dbc90cf8002 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 22:50:39 +0200 Subject: [PATCH 28/63] Created common methods for keep and map poll responses --- protocol_common_utils.go | 92 +++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/protocol_common_utils.go b/protocol_common_utils.go index 5939b6a4..3eb54715 100644 --- a/protocol_common_utils.go +++ b/protocol_common_utils.go @@ -10,35 +10,87 @@ import ( "tailscale.com/types/key" ) -func (h *Headscale) getMapKeepAliveResponse( - machineKey key.MachinePublic, +func (h *Headscale) getMapResponseData( mapRequest tailcfg.MapRequest, + machine *Machine, + isNoise bool, ) ([]byte, error) { - mapResponse := tailcfg.MapResponse{ + mapResponse, err := h.generateMapResponse(mapRequest, machine) + if err != nil { + return nil, err + } + + if isNoise { + return h.marshalResponse(mapResponse, mapRequest.Compress, key.MachinePublic{}) + } + + var machineKey key.MachinePublic + err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey))) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse client key") + + return nil, err + } + + return h.marshalResponse(mapResponse, mapRequest.Compress, machineKey) +} + +func (h *Headscale) getMapKeepAliveResponseData( + mapRequest tailcfg.MapRequest, + machine *Machine, + isNoise bool, +) ([]byte, error) { + keepAliveResponse := tailcfg.MapResponse{ KeepAlive: true, } - var respBody []byte - var err error - if mapRequest.Compress == ZstdCompression { - src, err := json.Marshal(mapResponse) - if err != nil { - log.Error(). - Caller(). - Str("func", "getMapKeepAliveResponse"). - Err(err). - Msg("Failed to marshal keepalive response for the client") - return nil, err - } + if isNoise { + return h.marshalResponse(keepAliveResponse, mapRequest.Compress, key.MachinePublic{}) + } + + var machineKey key.MachinePublic + err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey))) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse client key") + + return nil, err + } + + return h.marshalResponse(keepAliveResponse, mapRequest.Compress, machineKey) +} + +func (h *Headscale) marshalResponse( + resp interface{}, + compression string, + machineKey key.MachinePublic, +) ([]byte, error) { + jsonBody, err := json.Marshal(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot marshal map response") + } + + var respBody []byte + if compression == ZstdCompression { encoder, _ := zstd.NewWriter(nil) - srcCompressed := encoder.EncodeAll(src, nil) - respBody = h.privateKey.SealTo(machineKey, srcCompressed) + respBody = encoder.EncodeAll(jsonBody, nil) + if !machineKey.IsZero() { // if legacy protocol + respBody = h.privateKey.SealTo(machineKey, respBody) + } } else { - respBody, err = encode(mapResponse, &machineKey, h.privateKey) - if err != nil { - return nil, err + if !machineKey.IsZero() { // if legacy protocol + respBody = h.privateKey.SealTo(machineKey, jsonBody) } } + data := make([]byte, reservedResponseHeaderSize) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) data = append(data, respBody...) From df8ecdb6038ace48708d9030cea8f5da2aba41fa Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 22:57:03 +0200 Subject: [PATCH 29/63] Working on common codebase for poll, starting with legacy --- poll.go => protocol_common_poll.go | 128 ++++++----------------------- protocol_legacy_poll.go | 94 +++++++++++++++++++++ 2 files changed, 121 insertions(+), 101 deletions(-) rename poll.go => protocol_common_poll.go (84%) create mode 100644 protocol_legacy_poll.go diff --git a/poll.go b/protocol_common_poll.go similarity index 84% rename from poll.go rename to protocol_common_poll.go index a51c9362..988d225a 100644 --- a/poll.go +++ b/protocol_common_poll.go @@ -2,17 +2,12 @@ package headscale import ( "context" - "errors" "fmt" - "io" "net/http" "time" - "github.com/gorilla/mux" "github.com/rs/zerolog/log" - "gorm.io/gorm" "tailscale.com/tailcfg" - "tailscale.com/types/key" ) const ( @@ -23,83 +18,13 @@ type contextKey string const machineNameContextKey = contextKey("machineName") -// PollNetMapHandler takes care of /machine/:id/map -// -// This is the busiest endpoint, as it keeps the HTTP long poll that updates -// the clients when something in the network changes. -// -// The clients POST stuff like HostInfo and their Endpoints here, but -// only after their first request (marked with the ReadOnly field). -// -// At this moment the updates are sent in a quite horrendous way, but they kinda work. -func (h *Headscale) PollNetMapHandler( +func (h *Headscale) handlePollCommon( writer http.ResponseWriter, req *http.Request, + machine *Machine, + mapRequest tailcfg.MapRequest, + isNoise bool, ) { - vars := mux.Vars(req) - machineKeyStr, ok := vars["mkey"] - if !ok || machineKeyStr == "" { - log.Error(). - Str("handler", "PollNetMap"). - Msg("No machine key in request") - http.Error(writer, "No machine key in request", http.StatusBadRequest) - - return - } - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", machineKeyStr). - Msg("PollNetMapHandler called") - body, _ := io.ReadAll(req.Body) - - var machineKey key.MachinePublic - err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Err(err). - Msg("Cannot parse client key") - - http.Error(writer, "Cannot parse client key", http.StatusBadRequest) - - return - } - mapRequest := tailcfg.MapRequest{} - err = decode(body, &mapRequest, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Err(err). - Msg("Cannot decode message") - http.Error(writer, "Cannot decode message", http.StatusBadRequest) - - return - } - - machine, err := h.GetMachineByMachineKey(machineKey) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn(). - Str("handler", "PollNetMap"). - Msgf("Ignoring request, cannot find machine with key %s", machineKey.String()) - - http.Error(writer, "", http.StatusUnauthorized) - - return - } - log.Error(). - Str("handler", "PollNetMap"). - Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String()) - http.Error(writer, "", http.StatusInternalServerError) - - return - } - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", machineKeyStr). - Str("machine", machine.Hostname). - Msg("Found machine in database") - machine.Hostname = mapRequest.Hostinfo.Hostname machine.HostInfo = HostInfo(*mapRequest.Hostinfo) machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey) @@ -107,7 +32,7 @@ func (h *Headscale) PollNetMapHandler( // update ACLRules with peer informations (to update server tags if necessary) if h.aclPolicy != nil { - err = h.UpdateACLRules() + err := h.UpdateACLRules() if err != nil { log.Error(). Caller(). @@ -133,7 +58,7 @@ func (h *Headscale) PollNetMapHandler( if err != nil { log.Error(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). Msg("Failed to persist/update machine in the database") @@ -143,11 +68,11 @@ func (h *Headscale) PollNetMapHandler( } } - data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine) + mapResp, err := h.getMapResponseData(mapRequest, machine, isNoise) if err != nil { log.Error(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). Msg("Failed to get Map response") @@ -163,7 +88,6 @@ func (h *Headscale) PollNetMapHandler( // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 log.Debug(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). Str("machine", machine.Hostname). Bool("readOnly", mapRequest.ReadOnly). Bool("omitPeers", mapRequest.OmitPeers). @@ -178,7 +102,7 @@ func (h *Headscale) PollNetMapHandler( writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) - _, err := writer.Write(data) + _, err := writer.Write(mapResp) if err != nil { log.Error(). Caller(). @@ -186,6 +110,10 @@ func (h *Headscale) PollNetMapHandler( Msg("Failed to write response") } + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + return } @@ -198,8 +126,7 @@ func (h *Headscale) PollNetMapHandler( // Only create update channel if it has not been created log.Trace(). - Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Caller(). Str("machine", machine.Hostname). Msg("Loading or creating update channel") @@ -218,7 +145,7 @@ func (h *Headscale) PollNetMapHandler( Msg("Client sent endpoint update and is ok with a response without peer list") writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) - _, err := writer.Write(data) + _, err := writer.Write(mapResp) if err != nil { log.Error(). Caller(). @@ -250,7 +177,7 @@ func (h *Headscale) PollNetMapHandler( Str("handler", "PollNetMap"). Str("machine", machine.Hostname). Msg("Sending initial map") - pollDataChan <- data + pollDataChan <- mapResp log.Info(). Str("handler", "PollNetMap"). @@ -260,35 +187,34 @@ func (h *Headscale) PollNetMapHandler( Inc() updateChan <- struct{}{} - h.PollNetMapStream( + h.pollNetMapStream( writer, req, machine, mapRequest, - machineKey, pollDataChan, keepAliveChan, updateChan, + isNoise, ) + log.Trace(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). Str("machine", machine.Hostname). Msg("Finished stream, closing PollNetMap session") } -// PollNetMapStream takes care of /machine/:id/map -// stream logic, ensuring we communicate updates and data -// to the connected clients. -func (h *Headscale) PollNetMapStream( +// pollNetMapStream stream logic for /machine/map, +// ensuring we communicate updates and data to the connected clients. +func (h *Headscale) pollNetMapStream( writer http.ResponseWriter, req *http.Request, machine *Machine, mapRequest tailcfg.MapRequest, - machineKey key.MachinePublic, pollDataChan chan []byte, keepAliveChan chan []byte, updateChan chan struct{}, + isNoise bool, ) { h.pollNetMapStreamWG.Add(1) defer h.pollNetMapStreamWG.Done() @@ -302,9 +228,9 @@ func (h *Headscale) PollNetMapStream( ctx, updateChan, keepAliveChan, - machineKey, mapRequest, machine, + isNoise, ) log.Trace(). @@ -491,7 +417,7 @@ func (h *Headscale) PollNetMapStream( Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("There has been updates since the last successful update to %s", machine.Hostname) - data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine) + data, err := h.getMapResponseData(mapRequest, machine, false) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -637,9 +563,9 @@ func (h *Headscale) scheduledPollWorker( ctx context.Context, updateChan chan struct{}, keepAliveChan chan []byte, - machineKey key.MachinePublic, mapRequest tailcfg.MapRequest, machine *Machine, + isNoise bool, ) { keepAliveTicker := time.NewTicker(keepAliveInterval) updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval) @@ -661,7 +587,7 @@ func (h *Headscale) scheduledPollWorker( return case <-keepAliveTicker.C: - data, err := h.getMapKeepAliveResponse(machineKey, mapRequest) + data, err := h.getMapKeepAliveResponseData(mapRequest, machine, isNoise) if err != nil { log.Error(). Str("func", "keepAlive"). diff --git a/protocol_legacy_poll.go b/protocol_legacy_poll.go new file mode 100644 index 00000000..a42f399c --- /dev/null +++ b/protocol_legacy_poll.go @@ -0,0 +1,94 @@ +package headscale + +import ( + "errors" + "io" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// PollNetMapHandler takes care of /machine/:id/map +// +// This is the busiest endpoint, as it keeps the HTTP long poll that updates +// the clients when something in the network changes. +// +// The clients POST stuff like HostInfo and their Endpoints here, but +// only after their first request (marked with the ReadOnly field). +// +// At this moment the updates are sent in a quite horrendous way, but they kinda work. +func (h *Headscale) PollNetMapHandler( + writer http.ResponseWriter, + req *http.Request, +) { + vars := mux.Vars(req) + machineKeyStr, ok := vars["mkey"] + if !ok || machineKeyStr == "" { + log.Error(). + Str("handler", "PollNetMap"). + Msg("No machine key in request") + http.Error(writer, "No machine key in request", http.StatusBadRequest) + + return + } + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", machineKeyStr). + Msg("PollNetMapHandler called") + body, _ := io.ReadAll(req.Body) + + var machineKey key.MachinePublic + err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) + if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Err(err). + Msg("Cannot parse client key") + + http.Error(writer, "Cannot parse client key", http.StatusBadRequest) + + return + } + mapRequest := tailcfg.MapRequest{} + err = decode(body, &mapRequest, &machineKey, h.privateKey) + if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Err(err). + Msg("Cannot decode message") + http.Error(writer, "Cannot decode message", http.StatusBadRequest) + + return + } + + machine, err := h.GetMachineByMachineKey(machineKey) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "PollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", machineKey.String()) + + http.Error(writer, "", http.StatusUnauthorized) + + return + } + log.Error(). + Str("handler", "PollNetMap"). + Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String()) + http.Error(writer, "", http.StatusInternalServerError) + + return + } + + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", machineKeyStr). + Str("machine", machine.Hostname). + Msg("Found machine in database") + + h.handlePollCommon(writer, req, machine, mapRequest, false) +} From 7cc227d01e15e24283ac56a555992752b49e7a16 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 23:11:33 +0200 Subject: [PATCH 30/63] Added Noise field to logging --- protocol_common_poll.go | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/protocol_common_poll.go b/protocol_common_poll.go index 988d225a..11178cd3 100644 --- a/protocol_common_poll.go +++ b/protocol_common_poll.go @@ -36,6 +36,7 @@ func (h *Headscale) handlePollCommon( if err != nil { log.Error(). Caller(). + Bool("noise", isNoise). Str("func", "handleAuthKey"). Str("machine", machine.Hostname). Err(err) @@ -58,6 +59,7 @@ func (h *Headscale) handlePollCommon( if err != nil { log.Error(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). @@ -72,6 +74,7 @@ func (h *Headscale) handlePollCommon( if err != nil { log.Error(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). @@ -88,6 +91,7 @@ func (h *Headscale) handlePollCommon( // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 log.Debug(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Bool("readOnly", mapRequest.ReadOnly). Bool("omitPeers", mapRequest.OmitPeers). @@ -97,6 +101,7 @@ func (h *Headscale) handlePollCommon( if mapRequest.ReadOnly { log.Info(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Client is starting up. Probably interested in a DERP map") @@ -127,6 +132,7 @@ func (h *Headscale) handlePollCommon( // Only create update channel if it has not been created log.Trace(). Caller(). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Loading or creating update channel") @@ -141,6 +147,7 @@ func (h *Headscale) handlePollCommon( if mapRequest.OmitPeers && !mapRequest.Stream { log.Info(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Client sent endpoint update and is ok with a response without peer list") writer.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -162,6 +169,7 @@ func (h *Headscale) handlePollCommon( } else if mapRequest.OmitPeers && mapRequest.Stream { log.Warn(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Ignoring request, don't know how to handle it") http.Error(writer, "", http.StatusBadRequest) @@ -171,16 +179,19 @@ func (h *Headscale) handlePollCommon( log.Info(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Client is ready to access the tailnet") log.Info(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Sending initial map") pollDataChan <- mapResp log.Info(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Notifying peers") updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update"). @@ -200,6 +211,7 @@ func (h *Headscale) handlePollCommon( log.Trace(). Str("handler", "PollNetMap"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Finished stream, closing PollNetMap session") } @@ -235,11 +247,13 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Waiting for data to stream...") log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan) @@ -248,6 +262,7 @@ func (h *Headscale) pollNetMapStream( case data := <-pollDataChan: log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Int("bytes", len(data)). @@ -256,6 +271,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Err(err). @@ -269,6 +285,7 @@ func (h *Headscale) pollNetMapStream( log.Error(). Caller(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Msg("Cannot cast writer to http.Flusher") @@ -278,6 +295,7 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Int("bytes", len(data)). @@ -289,6 +307,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Err(err). @@ -309,6 +328,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Err(err). @@ -319,6 +339,7 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Int("bytes", len(data)). @@ -335,6 +356,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Err(err). @@ -347,6 +369,7 @@ func (h *Headscale) pollNetMapStream( log.Error(). Caller(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Msg("Cannot cast writer to http.Flusher") @@ -356,6 +379,7 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Int("bytes", len(data)). @@ -367,6 +391,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Err(err). @@ -382,6 +407,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Err(err). @@ -392,6 +418,7 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Int("bytes", len(data)). @@ -400,6 +427,7 @@ func (h *Headscale) pollNetMapStream( case <-updateChan: log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Msg("Received a request for update") @@ -413,6 +441,7 @@ func (h *Headscale) pollNetMapStream( } log.Debug(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). @@ -421,6 +450,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Err(err). @@ -432,6 +462,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Err(err). @@ -447,6 +478,7 @@ func (h *Headscale) pollNetMapStream( log.Error(). Caller(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Msg("Cannot cast writer to http.Flusher") @@ -456,6 +488,7 @@ func (h *Headscale) pollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Msg("Updated Map has been sent") @@ -473,6 +506,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Err(err). @@ -492,6 +526,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Err(err). @@ -506,6 +541,7 @@ func (h *Headscale) pollNetMapStream( } log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). @@ -524,6 +560,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "Done"). Err(err). @@ -539,6 +576,7 @@ func (h *Headscale) pollNetMapStream( if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "Done"). Err(err). @@ -551,6 +589,7 @@ func (h *Headscale) pollNetMapStream( case <-h.shutdownChan: log.Info(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("The long-poll handler is shutting down") @@ -591,6 +630,7 @@ func (h *Headscale) scheduledPollWorker( if err != nil { log.Error(). Str("func", "keepAlive"). + Bool("noise", isNoise). Err(err). Msg("Error generating the keep alive msg") @@ -600,6 +640,7 @@ func (h *Headscale) scheduledPollWorker( log.Debug(). Str("func", "keepAlive"). Str("machine", machine.Hostname). + Bool("noise", isNoise). Msg("Sending keepalive") keepAliveChan <- data @@ -607,6 +648,7 @@ func (h *Headscale) scheduledPollWorker( log.Debug(). Str("func", "scheduledPollWorker"). Str("machine", machine.Hostname). + Bool("noise", isNoise). Msg("Sending update request") updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update"). Inc() From e29b344e0f85cc64d6b5e3fdfc1639fefa92fa98 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 23:12:18 +0200 Subject: [PATCH 31/63] Move Noise poll to new file, and use common poll --- noise_poll.go | 714 ----------------------------------------- protocol_noise_poll.go | 67 ++++ 2 files changed, 67 insertions(+), 714 deletions(-) delete mode 100644 noise_poll.go create mode 100644 protocol_noise_poll.go diff --git a/noise_poll.go b/noise_poll.go deleted file mode 100644 index 1d9dfe37..00000000 --- a/noise_poll.go +++ /dev/null @@ -1,714 +0,0 @@ -package headscale - -import ( - "context" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "time" - - "github.com/klauspost/compress/zstd" - "github.com/rs/zerolog/log" - "gorm.io/gorm" - "tailscale.com/tailcfg" - "tailscale.com/types/key" -) - -// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol -// -// This is the busiest endpoint, as it keeps the HTTP long poll that updates -// the clients when something in the network changes. -// -// The clients POST stuff like HostInfo and their Endpoints here, but -// only after their first request (marked with the ReadOnly field). -// -// At this moment the updates are sent in a quite horrendous way, but they kinda work. -func (h *Headscale) NoisePollNetMapHandler( - writer http.ResponseWriter, - req *http.Request, -) { - log.Trace(). - Str("handler", "NoisePollNetMap"). - Msg("PollNetMapHandler called") - body, _ := io.ReadAll(req.Body) - - mapRequest := tailcfg.MapRequest{} - if err := json.Unmarshal(body, &mapRequest); err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot parse MapRequest") - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - - machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{}) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn(). - Str("handler", "NoisePollNetMap"). - Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String()) - http.Error(writer, "Internal error", http.StatusNotFound) - - return - } - log.Error(). - Str("handler", "NoisePollNetMap"). - Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String()) - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - log.Trace(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Found machine in database") - - machine.Hostname = mapRequest.Hostinfo.Hostname - machine.HostInfo = HostInfo(*mapRequest.Hostinfo) - machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey) - now := time.Now().UTC() - - // update ACLRules with peer informations (to update server tags if necessary) - if h.aclPolicy != nil { - err = h.UpdateACLRules() - if err != nil { - log.Error(). - Caller(). - Str("func", "handleAuthKey"). - Str("machine", machine.Hostname). - Err(err) - } - } - - // From Tailscale client: - // - // ReadOnly is whether the client just wants to fetch the MapResponse, - // without updating their Endpoints. The Endpoints field will be ignored and - // LastSeen will not be updated and peers will not be notified of changes. - // - // The intended use is for clients to discover the DERP map at start-up - // before their first real endpoint update. - if !mapRequest.ReadOnly { - machine.Endpoints = mapRequest.Endpoints - machine.LastSeen = &now - } - - if err := h.db.Updates(machine).Error; err != nil { - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Err(err). - Msg("Failed to persist/update machine in the database") - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - } - - resp, err := h.getNoiseMapResponse(mapRequest, machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Err(err). - Msg("Failed to get Map response") - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - - // We update our peers if the client is not sending ReadOnly in the MapRequest - // so we don't distribute its initial request (it comes with - // empty endpoints to peers) - - // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 - log.Debug(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Bool("readOnly", mapRequest.ReadOnly). - Bool("omitPeers", mapRequest.OmitPeers). - Bool("stream", mapRequest.Stream). - Msg("Noise client map request processed") - - if mapRequest.ReadOnly { - log.Info(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Client is starting up. Probably interested in a DERP map") - // w.Header().Set("Content-Type", "application/json") - // w.WriteHeader(http.StatusOK) - _, err = writer.Write(resp) - if err != nil { - log.Warn().Msgf("Could not send JSON response: %s", err) - } - if f, ok := writer.(http.Flusher); ok { - f.Flush() - } - - log.Info().Msgf("Noise client map response sent for %s (len %d)", machine.Hostname, len(resp)) - - return - } - - // There has been an update to _any_ of the nodes that the other nodes would - // need to know about - h.setLastStateChangeToNow(machine.Namespace.Name) - - // The request is not ReadOnly, so we need to set up channels for updating - // peers via longpoll - - // Only create update channel if it has not been created - log.Trace(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Loading or creating update channel") - - const chanSize = 8 - updateChan := make(chan struct{}, chanSize) - - pollDataChan := make(chan []byte, chanSize) - defer closeChanWithLog(pollDataChan, machine.Hostname, "pollDataChan") - - keepAliveChan := make(chan []byte) - - if mapRequest.OmitPeers && !mapRequest.Stream { - log.Info(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Client sent endpoint update and is ok with a response without peer list") - - _, err := writer.Write(resp) - if err != nil { - log.Warn().Msgf("Could not send response: %s", err) - - return - } - - if f, ok := writer.(http.Flusher); ok { - f.Flush() - } - - // It sounds like we should update the nodes when we have received a endpoint update - // even tho the comments in the tailscale code dont explicitly say so. - updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "endpoint-update"). - Inc() - updateChan <- struct{}{} - - return - } else if mapRequest.OmitPeers && mapRequest.Stream { - log.Warn(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Ignoring request, don't know how to handle it") - http.Error(writer, "Internal error", http.StatusBadRequest) - - return - } - - log.Info(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Client is ready to access the tailnet") - log.Info(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Sending initial map") - pollDataChan <- resp - - log.Info(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Notifying peers") - updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update"). - Inc() - updateChan <- struct{}{} - - h.NoisePollNetMapStream( - writer, - req, - machine, - mapRequest, - pollDataChan, - keepAliveChan, - updateChan, - ) - - log.Trace(). - Str("handler", "NoisePollNetMap"). - Str("machine", machine.Hostname). - Msg("Finished stream, closing PollNetMap session") -} - -// PollNetMapStream takes care of /machine/:id/map -// stream logic, ensuring we communicate updates and data -// to the connected clients. -func (h *Headscale) NoisePollNetMapStream( - writer http.ResponseWriter, - req *http.Request, - machine *Machine, - mapRequest tailcfg.MapRequest, - pollDataChan chan []byte, - keepAliveChan chan []byte, - updateChan chan struct{}, -) { - ctx := context.WithValue(context.Background(), machineNameContextKey, machine.Hostname) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - go h.noiseScheduledPollWorker( - ctx, - updateChan, - keepAliveChan, - mapRequest, - machine, - ) - - for { - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Msg("Waiting for data to stream...") - - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan) - - select { - case data := <-pollDataChan: - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Int("bytes", len(data)). - Msg("Sending data received via pollData channel") - _, err := writer.Write(data) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Err(err). - Msg("Cannot write data") - - return - } - - flusher, ok := writer.(http.Flusher) - if !ok { - log.Error(). - Caller(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Msg("Cannot cast writer to http.Flusher") - } else { - flusher.Flush() - } - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Int("bytes", len(data)). - Msg("Data from pollData channel written successfully") - // TODO(kradalby): Abstract away all the database calls, this can cause race conditions - // when an outdated machine object is kept alive, e.g. db is update from - // command line, but then overwritten. - err = h.UpdateMachineFromDatabase(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Err(err). - Msg("Cannot update machine from database") - - // client has been removed from database - // since the stream opened, terminate connection. - return - } - now := time.Now().UTC() - machine.LastSeen = &now - - lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname). - Set(float64(now.Unix())) - machine.LastSuccessfulUpdate = &now - - err = h.TouchMachine(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Err(err). - Msg("Cannot update machine LastSuccessfulUpdate") - } else { - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "pollData"). - Int("bytes", len(data)). - Msg("Machine entry in database updated successfully after sending pollData") - } - - break - - case data := <-keepAliveChan: - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Int("bytes", len(data)). - Msg("Sending keep alive message") - _, err := writer.Write(data) - if err != nil { - log.Error(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Err(err). - Msg("Cannot write keep alive message") - - return - } - flusher, ok := writer.(http.Flusher) - if !ok { - log.Error(). - Caller(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Msg("Cannot cast writer to http.Flusher") - } else { - flusher.Flush() - } - - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Int("bytes", len(data)). - Msg("Keep alive sent successfully") - // TODO(kradalby): Abstract away all the database calls, this can cause race conditions - // when an outdated machine object is kept alive, e.g. db is update from - // command line, but then overwritten. - err = h.UpdateMachineFromDatabase(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Err(err). - Msg("Cannot update machine from database") - - // client has been removed from database - // since the stream opened, terminate connection. - return - } - now := time.Now().UTC() - machine.LastSeen = &now - err = h.TouchMachine(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Err(err). - Msg("Cannot update machine LastSeen") - } else { - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "keepAlive"). - Int("bytes", len(data)). - Msg("Machine updated successfully after sending keep alive") - } - - return - - case <-updateChan: - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Msg("Received a request for update") - updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname). - Inc() - if h.isOutdated(machine) { - var lastUpdate time.Time - if machine.LastSuccessfulUpdate != nil { - lastUpdate = *machine.LastSuccessfulUpdate - } - log.Debug(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Time("last_successful_update", lastUpdate). - Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). - Msgf("There has been updates since the last successful update to %s", machine.Hostname) - data, err := h.getNoiseMapResponse(mapRequest, machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Err(err). - Msg("Could not get the map update") - } - _, err = writer.Write(data) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Err(err). - Msg("Could not write the map response") - updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed"). - Inc() - - return - } - - if f, ok := writer.(http.Flusher); ok { - f.Flush() - } - - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Msg("Updated Map has been sent") - updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "success"). - Inc() - - // Keep track of the last successful update, - // we sometimes end in a state were the update - // is not picked up by a client and we use this - // to determine if we should "force" an update. - // TODO(kradalby): Abstract away all the database calls, this can cause race conditions - // when an outdated machine object is kept alive, e.g. db is update from - // command line, but then overwritten. - err = h.UpdateMachineFromDatabase(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Err(err). - Msg("Cannot update machine from database") - - // client has been removed from database - // since the stream opened, terminate connection. - return - } - now := time.Now().UTC() - - lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname). - Set(float64(now.Unix())) - machine.LastSuccessfulUpdate = &now - - err = h.TouchMachine(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "update"). - Err(err). - Msg("Cannot update machine LastSuccessfulUpdate") - } - } else { - var lastUpdate time.Time - if machine.LastSuccessfulUpdate != nil { - lastUpdate = *machine.LastSuccessfulUpdate - } - log.Trace(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Time("last_successful_update", lastUpdate). - Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). - Msgf("%s is up to date", machine.Hostname) - } - - return - - case <-ctx.Done(): - log.Info(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Msg("The client has closed the connection") - // TODO: Abstract away all the database calls, this can cause race conditions - // when an outdated machine object is kept alive, e.g. db is update from - // command line, but then overwritten. - err := h.UpdateMachineFromDatabase(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "Done"). - Err(err). - Msg("Cannot update machine from database") - - // client has been removed from database - // since the stream opened, terminate connection. - return - } - now := time.Now().UTC() - machine.LastSeen = &now - err = h.TouchMachine(machine) - if err != nil { - log.Error(). - Str("handler", "NoisePollNetMapStream"). - Str("machine", machine.Hostname). - Str("channel", "Done"). - Err(err). - Msg("Cannot update machine LastSeen") - } - - return - } - } -} - -func (h *Headscale) noiseScheduledPollWorker( - ctx context.Context, - updateChan chan struct{}, - keepAliveChan chan []byte, - mapRequest tailcfg.MapRequest, - machine *Machine, -) { - keepAliveTicker := time.NewTicker(keepAliveInterval) - updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval) - - defer closeChanWithLog( - updateChan, - fmt.Sprint(ctx.Value(machineNameContextKey)), - "updateChan", - ) - defer closeChanWithLog( - keepAliveChan, - fmt.Sprint(ctx.Value(machineNameContextKey)), - "updateChan", - ) - - for { - select { - case <-ctx.Done(): - return - - case <-keepAliveTicker.C: - data, err := h.getNoiseMapKeepAliveResponse(mapRequest) - if err != nil { - log.Error(). - Str("func", "keepAlive"). - Err(err). - Msg("Error generating the keep alive msg") - - return - } - - log.Debug(). - Str("func", "keepAlive"). - Str("machine", machine.Hostname). - Msg("Sending keepalive") - keepAliveChan <- data - - case <-updateCheckerTicker.C: - log.Debug(). - Str("func", "scheduledPollWorker"). - Str("machine", machine.Hostname). - Msg("Sending noise update request") - updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update"). - Inc() - updateChan <- struct{}{} - } - } -} - -func (h *Headscale) getNoiseMapKeepAliveResponse(req tailcfg.MapRequest) ([]byte, error) { - resp := tailcfg.MapResponse{ - KeepAlive: true, - } - - // The TS2021 protocol does not rely anymore on the machine key to - // encrypt in a NaCl box the map response. We just send it back - // unencrypted via the encrypted Noise channel. - // declare the incoming size on the first 4 bytes - respBody, err := json.Marshal(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot marshal map response") - } - - var srcCompressed []byte - if req.Compress == ZstdCompression { - encoder, _ := zstd.NewWriter(nil) - srcCompressed = encoder.EncodeAll(respBody, nil) - } else { - srcCompressed = respBody - } - - data := make([]byte, reservedResponseHeaderSize) - binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed))) - data = append(data, srcCompressed...) - - return data, nil -} - -func (h *Headscale) getNoiseMapResponse( - req tailcfg.MapRequest, - machine *Machine, -) ([]byte, error) { - log.Trace(). - Str("func", "getNoiseMapResponse"). - Str("machine", req.Hostinfo.Hostname). - Msg("Creating Map response") - - resp, err := h.generateMapResponse(req, machine) - if err != nil { - log.Error(). - Str("func", "getNoiseMapResponse"). - Err(err). - Msg("Error generating the map response") - - return nil, err - } - - log.Trace(). - Str("func", "getNoiseMapResponse"). - Str("machine", req.Hostinfo.Hostname). - Msgf("Generated map response: %s", tailMapResponseToString(*resp)) - - // The TS2021 protocol does not rely anymore on the machine key to - // encrypt in a NaCl box the map response. We just send it back - // unencrypted via the encrypted Noise channel. - // declare the incoming size on the first 4 bytes - respBody, err := json.Marshal(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot marshal map response") - } - - var srcCompressed []byte - if req.Compress == ZstdCompression { - encoder, _ := zstd.NewWriter(nil) - srcCompressed = encoder.EncodeAll(respBody, nil) - } else { - srcCompressed = respBody - } - - data := make([]byte, reservedResponseHeaderSize) - binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed))) - data = append(data, srcCompressed...) - - return data, nil -} diff --git a/protocol_noise_poll.go b/protocol_noise_poll.go new file mode 100644 index 00000000..7556ae30 --- /dev/null +++ b/protocol_noise_poll.go @@ -0,0 +1,67 @@ +package headscale + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol +// +// This is the busiest endpoint, as it keeps the HTTP long poll that updates +// the clients when something in the network changes. +// +// The clients POST stuff like HostInfo and their Endpoints here, but +// only after their first request (marked with the ReadOnly field). +// +// At this moment the updates are sent in a quite horrendous way, but they kinda work. +func (h *Headscale) NoisePollNetMapHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace(). + Str("handler", "NoisePollNetMap"). + Msg("PollNetMapHandler called") + body, _ := io.ReadAll(req.Body) + + mapRequest := tailcfg.MapRequest{} + if err := json.Unmarshal(body, &mapRequest); err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse MapRequest") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{}) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "NoisePollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusNotFound) + + return + } + log.Error(). + Str("handler", "NoisePollNetMap"). + Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String()) + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + log.Debug(). + Str("handler", "NoisePollNetMap"). + Str("machine", machine.Hostname). + Msg("Found Noise machine in database") + + h.handlePollCommon(writer, req, machine, mapRequest, true) +} From 704a19b0a58117a392f0a172a422ef7640ee2303 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 23:13:07 +0200 Subject: [PATCH 32/63] Removed legacy method to generate MapResponse --- api.go | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/api.go b/api.go index 38e8e64b..ab8c2a87 100644 --- a/api.go +++ b/api.go @@ -2,7 +2,6 @@ package headscale import ( "bytes" - "encoding/binary" "encoding/json" "fmt" "html/template" @@ -11,7 +10,6 @@ import ( "time" "github.com/gorilla/mux" - "github.com/klauspost/compress/zstd" "github.com/rs/zerolog/log" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -151,46 +149,6 @@ func (h *Headscale) RegisterWebAPI( } } -func (h *Headscale) getLegacyMapResponseData( - machineKey key.MachinePublic, - mapRequest tailcfg.MapRequest, - machine *Machine, -) ([]byte, error) { - resp, err := h.generateMapResponse(mapRequest, machine) - if err != nil { - return nil, err - } - - var respBody []byte - if mapRequest.Compress == ZstdCompression { - src, err := json.Marshal(resp) - if err != nil { - log.Error(). - Caller(). - Str("func", "getMapResponse"). - Err(err). - Msg("Failed to marshal response for the client") - - return nil, err - } - - encoder, _ := zstd.NewWriter(nil) - srcCompressed := encoder.EncodeAll(src, nil) - respBody = h.privateKey.SealTo(machineKey, srcCompressed) - } else { - respBody, err = encode(resp, &machineKey, h.privateKey) - if err != nil { - return nil, err - } - } - // declare the incoming size on the first 4 bytes - data := make([]byte, reservedResponseHeaderSize) - binary.LittleEndian.PutUint32(data, uint32(len(respBody))) - data = append(data, respBody...) - - return data, nil -} - func (h *Headscale) handleMachineLogOut( writer http.ResponseWriter, req *http.Request, From f599bea216a4541d7f488e4f7f7e555123c48580 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 14 Aug 2022 23:15:41 +0200 Subject: [PATCH 33/63] Fixed issue when not using compression --- protocol_common_utils.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol_common_utils.go b/protocol_common_utils.go index 3eb54715..516b616c 100644 --- a/protocol_common_utils.go +++ b/protocol_common_utils.go @@ -88,6 +88,8 @@ func (h *Headscale) marshalResponse( } else { if !machineKey.IsZero() { // if legacy protocol respBody = h.privateKey.SealTo(machineKey, jsonBody) + } else { + respBody = jsonBody } } From 5cf9eedf42e883437eba2d793700747e00ff3674 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 15 Aug 2022 10:43:39 +0200 Subject: [PATCH 34/63] Minor logging corrections --- noise.go | 2 +- protocol_common_poll.go | 7 ++++--- utils.go | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/noise.go b/noise.go index 16754c69..c3800576 100644 --- a/noise.go +++ b/noise.go @@ -58,7 +58,7 @@ func (h *Headscale) NoiseUpgradeHandler( server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{}) err = server.Serve(netutil.NewOneConnListener(noiseConn, nil)) if err != nil { - log.Error().Err(err).Msg("noise server launch failed") + log.Info().Err(err).Msg("The HTTP2 server was closed") } } diff --git a/protocol_common_poll.go b/protocol_common_poll.go index 11178cd3..f8265d38 100644 --- a/protocol_common_poll.go +++ b/protocol_common_poll.go @@ -18,6 +18,8 @@ type contextKey string const machineNameContextKey = contextKey("machineName") +// handlePollCommon is the common code for the legacy and Noise protocols to +// managed the poll loop. func (h *Headscale) handlePollCommon( writer http.ResponseWriter, req *http.Request, @@ -37,7 +39,6 @@ func (h *Headscale) handlePollCommon( log.Error(). Caller(). Bool("noise", isNoise). - Str("func", "handleAuthKey"). Str("machine", machine.Hostname). Err(err) } @@ -246,13 +247,13 @@ func (h *Headscale) pollNetMapStream( ) log.Trace(). - Str("handler", "PollNetMapStream"). + Str("handler", "pollNetMapStream"). Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Waiting for data to stream...") log.Trace(). - Str("handler", "PollNetMapStream"). + Str("handler", "pollNetMapStream"). Bool("noise", isNoise). Str("machine", machine.Hostname). Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan) diff --git a/utils.go b/utils.go index 089e867b..7727a245 100644 --- a/utils.go +++ b/utils.go @@ -118,7 +118,10 @@ func decode( pubKey *key.MachinePublic, privKey *key.MachinePrivate, ) error { - log.Trace().Int("length", len(msg)).Msg("Trying to decrypt") + log.Trace(). + Str("pubkey", pubKey.ShortString()). + Int("length", len(msg)). + Msg("Trying to decrypt") decrypted, ok := privKey.OpenFrom(*pubKey, msg) if !ok { From b8980b9ed36e69ada83aa0d3f027b96344e970ef Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 15 Aug 2022 10:44:22 +0200 Subject: [PATCH 35/63] More minor logging stuff --- protocol_legacy_poll.go | 2 +- protocol_noise_poll.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol_legacy_poll.go b/protocol_legacy_poll.go index a42f399c..f7ef6548 100644 --- a/protocol_legacy_poll.go +++ b/protocol_legacy_poll.go @@ -88,7 +88,7 @@ func (h *Headscale) PollNetMapHandler( Str("handler", "PollNetMap"). Str("id", machineKeyStr). Str("machine", machine.Hostname). - Msg("Found machine in database") + Msg("A machine is entering polling via the legacy protocol") h.handlePollCommon(writer, req, machine, mapRequest, false) } diff --git a/protocol_noise_poll.go b/protocol_noise_poll.go index 7556ae30..8498dcfb 100644 --- a/protocol_noise_poll.go +++ b/protocol_noise_poll.go @@ -61,7 +61,7 @@ func (h *Headscale) NoisePollNetMapHandler( log.Debug(). Str("handler", "NoisePollNetMap"). Str("machine", machine.Hostname). - Msg("Found Noise machine in database") + Msg("A machine is entering polling via the Noise protocol") h.handlePollCommon(writer, req, machine, mapRequest, true) } From 8db7629edf64071d7dce682b0df333c785003b4e Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 15 Aug 2022 10:53:06 +0200 Subject: [PATCH 36/63] Fix config file in integration tests for Noise --- integration_test/etc/config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml index e6b34afa..c6eede6b 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -14,6 +14,7 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key +noise_private_key_path: noise_private.key listen_addr: 0.0.0.0:8080 metrics_listen_addr: 127.0.0.1:9090 server_url: http://headscale:8080 From 865f1ffb3c355ff8edda3aecea0aa6aa09d7a397 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 15 Aug 2022 11:25:47 +0200 Subject: [PATCH 37/63] Fix issues with DERP integration tests due to tailscale/tailscale#4323 --- integration_test/etc_embedded_derp/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml index 86946116..86a59848 100644 --- a/integration_test/etc_embedded_derp/config.yaml +++ b/integration_test/etc_embedded_derp/config.yaml @@ -15,8 +15,8 @@ dns_config: db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key noise_private_key_path: noise_private.key -listen_addr: 0.0.0.0:8443 -server_url: https://headscale:8443 +listen_addr: 0.0.0.0:443 +server_url: https://headscale:443 tls_cert_path: "/etc/headscale/tls/server.crt" tls_key_path: "/etc/headscale/tls/server.key" tls_client_auth_mode: disabled From b3cf5289f894009c2a040d31f28da144729fca4e Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 15 Aug 2022 23:35:06 +0200 Subject: [PATCH 38/63] Use CapVer to offer Noise only to supported clients --- api.go | 6 ------ protocol_common.go | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index ab8c2a87..56ca3db6 100644 --- a/api.go +++ b/api.go @@ -25,12 +25,6 @@ const ( ErrRegisterMethodCLIDoesNotSupportExpire = Error( "machines registered with CLI does not support expire", ) - - // The CapabilityVersion is used by Tailscale clients to indicate - // their codebase version. Tailscale clients can communicate over TS2021 - // from CapabilityVersion 28. - // See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go - NoiseCapabilityVersion = 28 ) func (h *Headscale) HealthHandler( diff --git a/protocol_common.go b/protocol_common.go index c8eab80e..e196c3ee 100644 --- a/protocol_common.go +++ b/protocol_common.go @@ -9,6 +9,19 @@ import ( "tailscale.com/tailcfg" ) +const ( + // The CapabilityVersion is used by Tailscale clients to indicate + // their codebase version. Tailscale clients can communicate over TS2021 + // from CapabilityVersion 28, but we only have good support for it + // since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port). + // + // Related to this change, there is https://github.com/tailscale/tailscale/pull/5379, + // where CapabilityVersion 39 is introduced to indicate #4323 was merged. + // + // See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go + NoiseCapabilityVersion = 39 +) + // KeyHandler provides the Headscale pub key // Listens in /key. func (h *Headscale) KeyHandler( @@ -18,6 +31,10 @@ func (h *Headscale) KeyHandler( // New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion clientCapabilityStr := req.URL.Query().Get("v") if clientCapabilityStr != "" { + log.Debug(). + Str("handler", "/key"). + Str("v", clientCapabilityStr). + Msg("New noise client") clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr) if err != nil { writer.Header().Set("Content-Type", "text/plain; charset=utf-8") @@ -52,6 +69,9 @@ func (h *Headscale) KeyHandler( return } } + log.Debug(). + Str("handler", "/key"). + Msg("New legacy client") // Old clients don't send a 'v' parameter, so we send the legacy public key writer.Header().Set("Content-Type", "text/plain; charset=utf-8") From eb461d0713dbf9a3972eea14ac6c5f840d8e6ee1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 00:18:02 +0200 Subject: [PATCH 39/63] Enable HEAD and unstable in integration tests --- integration_common_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_common_test.go b/integration_common_test.go index 0235f25b..2b824366 100644 --- a/integration_common_test.go +++ b/integration_common_test.go @@ -30,8 +30,8 @@ var ( IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48") tailscaleVersions = []string{ - // "head", - // "unstable", + "head", + "unstable", "1.28.0", "1.26.2", "1.24.2", From 0db7fc5ab726937e7ab860fd917a70f84dae5927 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 13:39:15 +0200 Subject: [PATCH 40/63] Mark all namespaces to lastChange now --- app.go | 2 +- machine.go | 8 ++++---- protocol_common_poll.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index 7722bf79..966fb3a9 100644 --- a/app.go +++ b/app.go @@ -270,7 +270,7 @@ func (h *Headscale) expireEphemeralNodesWorker() { } if expiredFound { - h.setLastStateChangeToNow(namespace.Name) + h.setLastStateChangeToNow() } } } diff --git a/machine.go b/machine.go index d9fd789d..1773e8ff 100644 --- a/machine.go +++ b/machine.go @@ -410,7 +410,7 @@ func (h *Headscale) SetTags(machine *Machine, tags []string) error { if err := h.UpdateACLRules(); err != nil && !errors.Is(err, errEmptyPolicy) { return err } - h.setLastStateChangeToNow(machine.Namespace.Name) + h.setLastStateChangeToNow() if err := h.db.Save(machine).Error; err != nil { return fmt.Errorf("failed to update tags for machine in the database: %w", err) @@ -424,7 +424,7 @@ func (h *Headscale) ExpireMachine(machine *Machine) error { now := time.Now() machine.Expiry = &now - h.setLastStateChangeToNow(machine.Namespace.Name) + h.setLastStateChangeToNow() if err := h.db.Save(machine).Error; err != nil { return fmt.Errorf("failed to expire machine in the database: %w", err) @@ -451,7 +451,7 @@ func (h *Headscale) RenameMachine(machine *Machine, newName string) error { } machine.GivenName = newName - h.setLastStateChangeToNow(machine.Namespace.Name) + h.setLastStateChangeToNow() if err := h.db.Save(machine).Error; err != nil { return fmt.Errorf("failed to rename machine in the database: %w", err) @@ -467,7 +467,7 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) error { machine.LastSuccessfulUpdate = &now machine.Expiry = &expiry - h.setLastStateChangeToNow(machine.Namespace.Name) + h.setLastStateChangeToNow() if err := h.db.Save(machine).Error; err != nil { return fmt.Errorf( diff --git a/protocol_common_poll.go b/protocol_common_poll.go index f8265d38..65dcb556 100644 --- a/protocol_common_poll.go +++ b/protocol_common_poll.go @@ -125,7 +125,7 @@ func (h *Headscale) handlePollCommon( // There has been an update to _any_ of the nodes that the other nodes would // need to know about - h.setLastStateChangeToNow(machine.Namespace.Name) + h.setLastStateChangeToNow() // The request is not ReadOnly, so we need to set up channels for updating // peers via longpoll From c0fe1abf4ddbd5b7ccda5dc42d02cb0bcf0f6a26 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 17:51:43 +0200 Subject: [PATCH 41/63] Use node_key to find peers --- machine.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/machine.go b/machine.go index 1773e8ff..89f0c7e7 100644 --- a/machine.go +++ b/machine.go @@ -244,8 +244,8 @@ func (h *Headscale) ListPeers(machine *Machine) (Machines, error) { Msg("Finding direct peers") machines := Machines{} - if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?", - machine.MachineKey).Find(&machines).Error; err != nil { + if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("node_key <> ?", + machine.NodeKey).Find(&machines).Error; err != nil { log.Error().Err(err).Msg("Error accessing db") return Machines{}, err From ce53bb0eee3b8cd16a6dca8b52492faf87cf0839 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 17:52:59 +0200 Subject: [PATCH 42/63] Minor changes to HEAD Dockerfile --- Dockerfile.tailscale-HEAD | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index b62f7e2b..c6e894da 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -7,7 +7,9 @@ RUN apt-get update \ RUN git clone https://github.com/tailscale/tailscale.git -WORKDIR tailscale +WORKDIR /go/tailscale + +RUN git checkout main RUN sh build_dist.sh tailscale.com/cmd/tailscale RUN sh build_dist.sh tailscale.com/cmd/tailscaled From b71a881d0e455b1543c101fd0547263ef1bde2e1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 18:19:04 +0200 Subject: [PATCH 43/63] Retry magicdns tests --- integration_general_test.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/integration_general_test.go b/integration_general_test.go index d4c64c38..3296762a 100644 --- a/integration_general_test.go +++ b/integration_general_test.go @@ -683,6 +683,18 @@ func (s *IntegrationTestSuite) TestMagicDNS() { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) + retry := func(times int, sleepInverval time.Duration, doWork func() (string, error)) (result string, err error) { + for attempts := 0; attempts < times; attempts++ { + result, err = doWork() + if err == nil { + return + } + time.Sleep(sleepInverval) + } + + return + } + for hostname, tailscale := range scales.tailscales { for _, peername := range hostnames { if strings.Contains(peername, hostname) { @@ -693,17 +705,20 @@ func (s *IntegrationTestSuite) TestMagicDNS() { command := []string{ "tailscale", "ip", peername, } + result, err := retry(10, 1*time.Second, func() (string, error) { + log.Printf( + "Resolving name %s from %s\n", + peername, + hostname, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + return result, err + }) - log.Printf( - "Resolving name %s from %s\n", - peername, - hostname, - ) - result, err := ExecuteCommand( - &tailscale, - command, - []string{}, - ) assert.Nil(t, err) log.Printf("Result for %s: %s\n", hostname, result) From ba07bac46a0c116987305f8161de151daf1d9626 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 16 Aug 2022 18:42:22 +0200 Subject: [PATCH 44/63] Use IPv4 in the tests --- integration_general_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_general_test.go b/integration_general_test.go index 3296762a..840b8165 100644 --- a/integration_general_test.go +++ b/integration_general_test.go @@ -566,7 +566,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { command := []string{ "tailscale", "file", "cp", fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf("%s:", ips[peername][1]), + fmt.Sprintf("%s:", ips[peername][0]), } retry(10, 1*time.Second, func() error { log.Printf( From 8a707de5f171957c60dd1317c5f18b44bd704e7c Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 18 Aug 2022 17:53:04 +0200 Subject: [PATCH 45/63] Add local Docker DNS server (makes resolving http://headscale more reliable) --- integration_test/etc/config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml index c6eede6b..9866d554 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -11,6 +11,7 @@ dns_config: magic_dns: true domains: [] nameservers: + - 127.0.0.11 - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key From 7185f8dfeae70541df7bf5cd561fe84c93a0c5df Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 18 Aug 2022 17:53:25 +0200 Subject: [PATCH 46/63] Only use released versions in public integration tests --- integration_common_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_common_test.go b/integration_common_test.go index 2b824366..0235f25b 100644 --- a/integration_common_test.go +++ b/integration_common_test.go @@ -30,8 +30,8 @@ var ( IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48") tailscaleVersions = []string{ - "head", - "unstable", + // "head", + // "unstable", "1.28.0", "1.26.2", "1.24.2", From f43a83aad7e0a1a4a3b2559c663a064a5fa2d828 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 18 Aug 2022 17:53:36 +0200 Subject: [PATCH 47/63] Find out IPv4 for taildrop --- integration_general_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/integration_general_test.go b/integration_general_test.go index 840b8165..d363ee26 100644 --- a/integration_general_test.go +++ b/integration_general_test.go @@ -562,11 +562,23 @@ func (s *IntegrationTestSuite) TestTailDrop() { if peername == hostname { continue } + + var ip4 netaddr.IP + for _, ip := range ips[peername] { + if ip.Is4() { + ip4 = ip + break + } + } + if ip4.IsZero() { + panic("no ipv4 address found") + } + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { command := []string{ "tailscale", "file", "cp", fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf("%s:", ips[peername][0]), + fmt.Sprintf("%s:", ip4), } retry(10, 1*time.Second, func() error { log.Printf( From cf731fafab53a81fa17d72ba828a3a2f306ac92a Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 18 Aug 2022 17:56:01 +0200 Subject: [PATCH 48/63] Catch retry error in taildrop send --- integration_general_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration_general_test.go b/integration_general_test.go index d363ee26..9f2ca3ea 100644 --- a/integration_general_test.go +++ b/integration_general_test.go @@ -580,7 +580,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { fmt.Sprintf("/tmp/file_from_%s", hostname), fmt.Sprintf("%s:", ip4), } - retry(10, 1*time.Second, func() error { + err := retry(10, 1*time.Second, func() error { log.Printf( "Sending file from %s to %s\n", hostname, @@ -594,6 +594,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { ) return err }) + assert.Nil(t, err) }) } From f9a2a2b57a062d5ef9801a18c77469b89bb4a1d3 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 18 Aug 2022 18:07:15 +0200 Subject: [PATCH 49/63] Add docker DNS IP to the remaining files --- integration_test/etc/alt-config.dump.gold.yaml | 1 + integration_test/etc/alt-config.yaml | 1 + integration_test/etc/config.dump.gold.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml index e02d7067..e71c9576 100644 --- a/integration_test/etc/alt-config.dump.gold.yaml +++ b/integration_test/etc/alt-config.dump.gold.yaml @@ -18,6 +18,7 @@ dns_config: domains: [] magic_dns: true nameservers: + - 127.0.0.11 - 1.1.1.1 ephemeral_node_inactivity_timeout: 30m node_update_check_interval: 10s diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml index 8a6d7396..35dd9e48 100644 --- a/integration_test/etc/alt-config.yaml +++ b/integration_test/etc/alt-config.yaml @@ -11,6 +11,7 @@ dns_config: magic_dns: true domains: [] nameservers: + - 127.0.0.11 - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml index f474e890..71539656 100644 --- a/integration_test/etc/config.dump.gold.yaml +++ b/integration_test/etc/config.dump.gold.yaml @@ -18,6 +18,7 @@ dns_config: domains: [] magic_dns: true nameservers: + - 127.0.0.11 - 1.1.1.1 ephemeral_node_inactivity_timeout: 30m node_update_check_interval: 10s From 9d430d3c727fad9b6adc6efb28eab8718562f524 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 18 Aug 2022 21:33:56 +0200 Subject: [PATCH 50/63] Update noise.go Co-authored-by: Kristoffer Dalby --- noise.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noise.go b/noise.go index c3800576..cf18fbfb 100644 --- a/noise.go +++ b/noise.go @@ -39,7 +39,7 @@ func (h *Headscale) NoiseUpgradeHandler( ) { log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr) - // Under normal circumpstances, we should be able to use the controlhttp.AcceptHTTP() + // Under normal circumstances, we should be able to use the controlhttp.AcceptHTTP() // function to do this - kindly left there by the Tailscale authors for us to use. // (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go) // From a87a9636e3f1432b3f944fc7dd81a7adba198d75 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 19 Aug 2022 14:19:29 +0200 Subject: [PATCH 51/63] Expanded response marshal methods to support legacy and Noise --- protocol_common_utils.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/protocol_common_utils.go b/protocol_common_utils.go index 516b616c..3dc435f3 100644 --- a/protocol_common_utils.go +++ b/protocol_common_utils.go @@ -21,7 +21,7 @@ func (h *Headscale) getMapResponseData( } if isNoise { - return h.marshalResponse(mapResponse, mapRequest.Compress, key.MachinePublic{}) + return h.marshalMapResponse(mapResponse, key.MachinePublic{}, mapRequest.Compress) } var machineKey key.MachinePublic @@ -35,7 +35,7 @@ func (h *Headscale) getMapResponseData( return nil, err } - return h.marshalResponse(mapResponse, mapRequest.Compress, machineKey) + return h.marshalMapResponse(mapResponse, machineKey, mapRequest.Compress) } func (h *Headscale) getMapKeepAliveResponseData( @@ -48,7 +48,7 @@ func (h *Headscale) getMapKeepAliveResponseData( } if isNoise { - return h.marshalResponse(keepAliveResponse, mapRequest.Compress, key.MachinePublic{}) + return h.marshalMapResponse(keepAliveResponse, key.MachinePublic{}, mapRequest.Compress) } var machineKey key.MachinePublic @@ -62,13 +62,32 @@ func (h *Headscale) getMapKeepAliveResponseData( return nil, err } - return h.marshalResponse(keepAliveResponse, mapRequest.Compress, machineKey) + return h.marshalMapResponse(keepAliveResponse, machineKey, mapRequest.Compress) } func (h *Headscale) marshalResponse( resp interface{}, - compression string, machineKey key.MachinePublic, +) ([]byte, error) { + jsonBody, err := json.Marshal(resp) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot marshal response") + } + + if machineKey.IsZero() { // if Noise + return jsonBody, nil + } + + return h.privateKey.SealTo(machineKey, jsonBody), nil +} + +func (h *Headscale) marshalMapResponse( + resp interface{}, + machineKey key.MachinePublic, + compression string, ) ([]byte, error) { jsonBody, err := json.Marshal(resp) if err != nil { From e2bffd4f5a63cb91a184941196e93e989201b4ce Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 19 Aug 2022 14:20:24 +0200 Subject: [PATCH 52/63] Make legacy protocol use common methods for client registration --- api.go | 433 ----------------------------- protocol_common.go | 660 +++++++++++++++++++++++++++++++++++++++++++++ protocol_legacy.go | 142 +--------- 3 files changed, 661 insertions(+), 574 deletions(-) diff --git a/api.go b/api.go index 56ca3db6..18ac72fe 100644 --- a/api.go +++ b/api.go @@ -3,16 +3,12 @@ package headscale import ( "bytes" "encoding/json" - "fmt" "html/template" "net/http" - "strings" "time" "github.com/gorilla/mux" "github.com/rs/zerolog/log" - "tailscale.com/tailcfg" - "tailscale.com/types/key" ) const ( @@ -142,432 +138,3 @@ func (h *Headscale) RegisterWebAPI( Msg("Failed to write response") } } - -func (h *Headscale) handleMachineLogOut( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - log.Info(). - Str("machine", machine.Hostname). - Msg("Client requested logout") - - err := h.ExpireMachine(&machine) - if err != nil { - log.Error(). - Caller(). - Str("func", "handleMachineLogOut"). - Err(err). - Msg("Failed to expire machine") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - resp.AuthURL = "" - resp.MachineAuthorized = false - resp.User = *machine.Namespace.toUser() - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot encode message") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - -func (h *Headscale) handleMachineValidRegistration( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - // The machine registration is valid, respond with redirect to /map - log.Debug(). - Str("machine", machine.Hostname). - Msg("Client is registered and we have the current NodeKey. All clear to /map") - - resp.AuthURL = "" - resp.MachineAuthorized = true - resp.User = *machine.Namespace.toUser() - resp.Login = *machine.Namespace.toLogin() - - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot encode message") - machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name). - Inc() - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - -func (h *Headscale) handleMachineExpired( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - registerRequest tailcfg.RegisterRequest, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - // The client has registered before, but has expired - log.Debug(). - Str("machine", machine.Hostname). - Msg("Machine registration has expired. Sending a authurl to register") - - if registerRequest.Auth.AuthKey != "" { - h.handleAuthKey(writer, req, machineKey, registerRequest) - - return - } - - if h.cfg.OIDC.Issuer != "" { - resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), - NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } else { - resp.AuthURL = fmt.Sprintf("%s/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), - NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } - - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot encode message") - machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). - Inc() - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - -func (h *Headscale) handleMachineRefreshKey( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - registerRequest tailcfg.RegisterRequest, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - log.Debug(). - Str("machine", machine.Hostname). - Msg("We have the OldNodeKey in the database. This is a key refresh") - machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) - - if err := h.db.Save(&machine).Error; err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to update machine key in the database") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - resp.AuthURL = "" - resp.User = *machine.Namespace.toUser() - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot encode message") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - -func (h *Headscale) handleMachineRegistrationNew( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - registerRequest tailcfg.RegisterRequest, -) { - resp := tailcfg.RegisterResponse{} - - // The machine registration is new, redirect the client to the registration URL - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("The node seems to be new, sending auth url") - if h.cfg.OIDC.Issuer != "" { - resp.AuthURL = fmt.Sprintf( - "%s/oidc/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), - NodePublicKeyStripPrefix(registerRequest.NodeKey), - ) - } else { - resp.AuthURL = fmt.Sprintf("%s/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), - NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } - - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Cannot encode message") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - -// TODO: check if any locks are needed around IP allocation. -func (h *Headscale) handleAuthKey( - writer http.ResponseWriter, - req *http.Request, - machineKey key.MachinePublic, - registerRequest tailcfg.RegisterRequest, -) { - machineKeyStr := MachinePublicKeyStripPrefix(machineKey) - - log.Debug(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname) - resp := tailcfg.RegisterResponse{} - - pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey) - if err != nil { - log.Error(). - Caller(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Err(err). - Msg("Failed authentication via AuthKey") - resp.MachineAuthorized = false - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Err(err). - Msg("Cannot encode message") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - - return - } - - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusUnauthorized) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - log.Error(). - Caller(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("Failed authentication via AuthKey") - - if pak != nil { - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - } else { - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc() - } - - return - } - - log.Debug(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("Authentication key was valid, proceeding to acquire IP addresses") - - nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey) - - // retrieve machine information if it exist - // The error is not important, because if it does not - // exist, then this is a new machine and we will move - // on to registration. - machine, _ := h.GetMachineByMachineKey(machineKey) - if machine != nil { - log.Trace(). - Caller(). - Str("machine", machine.Hostname). - Msg("machine already registered, refreshing with new auth key") - - machine.NodeKey = nodeKey - machine.AuthKeyID = uint(pak.ID) - err := h.RefreshMachine(machine, registerRequest.Expiry) - if err != nil { - log.Error(). - Caller(). - Str("machine", machine.Hostname). - Err(err). - Msg("Failed to refresh machine") - - return - } - } else { - now := time.Now().UTC() - - givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err) - - return - } - - machineToRegister := Machine{ - Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, - NamespaceID: pak.Namespace.ID, - MachineKey: machineKeyStr, - RegisterMethod: RegisterMethodAuthKey, - Expiry: ®isterRequest.Expiry, - NodeKey: nodeKey, - LastSeen: &now, - AuthKeyID: uint(pak.ID), - } - - machine, err = h.RegisterMachine( - machineToRegister, - ) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("could not register machine") - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - } - - err = h.UsePreAuthKey(pak) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to use pre-auth key") - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - resp.MachineAuthorized = true - resp.User = *pak.Namespace.toUser() - respBody, err := encode(resp, &machineKey, h.privateKey) - if err != nil { - log.Error(). - Caller(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Err(err). - Msg("Cannot encode message") - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). - Inc() - writer.Header().Set("Content-Type", "application/json; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err = writer.Write(respBody) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - log.Info(). - Str("func", "handleAuthKey"). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). - Msg("Successfully authenticated via AuthKey") -} diff --git a/protocol_common.go b/protocol_common.go index e196c3ee..998b2026 100644 --- a/protocol_common.go +++ b/protocol_common.go @@ -2,11 +2,17 @@ package headscale import ( "encoding/json" + "errors" + "fmt" "net/http" "strconv" + "strings" + "time" "github.com/rs/zerolog/log" + "gorm.io/gorm" "tailscale.com/tailcfg" + "tailscale.com/types/key" ) const ( @@ -84,3 +90,657 @@ func (h *Headscale) KeyHandler( Msg("Failed to write response") } } + +// handleRegisterCommon is the common logic for registering a client in the legacy and Noise protocols +// +// When using Noise, the machineKey is Zero. +func (h *Headscale) handleRegisterCommon( + writer http.ResponseWriter, + req *http.Request, + registerRequest tailcfg.RegisterRequest, + machineKey key.MachinePublic, +) { + + now := time.Now().UTC() + machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) + if errors.Is(err, gorm.ErrRecordNotFound) { + + // If the machine has AuthKey set, handle registration via PreAuthKeys + if registerRequest.Auth.AuthKey != "" { + h.handleAuthKeyCommon(writer, req, registerRequest, machineKey) + + return + } + + // Check if the node is waiting for interactive login. + // + // TODO(juan): We could use this field to improve our protocol implementation, + // and hold the request until the client closes it, or the interactive + // login is completed (i.e., the user registers the machine). + // This is not implemented yet, as it is no strictly required. The only side-effect + // is that the client will hammer headscale with requests until it gets a + // successful RegisterResponse. + if registerRequest.Followup != "" { + if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Bool("noise", machineKey.IsZero()). + Msg("Machine is waiting for interactive login") + + ticker := time.NewTicker(registrationHoldoff) + select { + case <-req.Context().Done(): + return + case <-ticker.C: + h.handleNewMachineCommon(writer, req, registerRequest, machineKey) + + return + } + } + } + + log.Info(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Bool("noise", machineKey.IsZero()). + Msg("New machine not yet in the database") + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.Hostinfo.Hostname). + Err(err) + + return + } + + // The machine did not have a key to authenticate, which means + // that we rely on a method that calls back some how (OpenID or CLI) + // We create the machine and then keep it around until a callback + // happens + newMachine := Machine{ + MachineKey: MachinePublicKeyStripPrefix(machineKey), + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), + LastSeen: &now, + Expiry: &time.Time{}, + } + + if !registerRequest.Expiry.IsZero() { + log.Trace(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Time("expiry", registerRequest.Expiry). + Msg("Non-zero expiry time requested") + newMachine.Expiry = ®isterRequest.Expiry + } + + h.registrationCache.Set( + newMachine.NodeKey, + newMachine, + registerCacheExpiration, + ) + + h.handleNewMachineCommon(writer, req, registerRequest, machineKey) + + return + } + + // The machine is already registered, so we need to pass through reauth or key update. + if machine != nil { + // If the NodeKey stored in headscale is the same as the key presented in a registration + // request, then we have a node that is either: + // - Trying to log out (sending a expiry in the past) + // - A valid, registered machine, looking for the node map + // - Expired machine wanting to reauthenticate + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { + // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) + // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 + if !registerRequest.Expiry.IsZero() && + registerRequest.Expiry.UTC().Before(now) { + h.handleMachineLogOutCommon(writer, req, *machine, machineKey) + + return + } + + // If machine is not expired, and is register, we have a already accepted this machine, + // let it proceed with a valid registration + if !machine.isExpired() { + h.handleMachineValidRegistrationCommon(writer, req, *machine, machineKey) + + return + } + } + + // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && + !machine.isExpired() { + h.handleMachineRefreshKeyCommon( + writer, + req, + registerRequest, + *machine, + machineKey, + ) + + return + } + + // The machine has expired + h.handleMachineExpiredCommon(writer, req, registerRequest, *machine, machineKey) + + return + } + +} + +// handleAuthKeyCommon contains the logic to manage auth key client registration +// It is used both by the legacy and the new Noise protocol. +// When using Noise, the machineKey is Zero. +// +// TODO: check if any locks are needed around IP allocation. +func (h *Headscale) handleAuthKeyCommon( + writer http.ResponseWriter, + req *http.Request, + registerRequest tailcfg.RegisterRequest, + machineKey key.MachinePublic, +) { + log.Debug(). + Str("func", "handleAuthKeyCommon"). + Str("machine", registerRequest.Hostinfo.Hostname). + Bool("noise", machineKey.IsZero()). + Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname) + resp := tailcfg.RegisterResponse{} + + pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey) + if err != nil { + log.Error(). + Caller(). + Str("func", "handleAuthKeyCommon"). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Err(err). + Msg("Failed authentication via AuthKey") + resp.MachineAuthorized = false + + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Str("func", "handleAuthKeyCommon"). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Err(err). + Msg("Cannot encode message") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + + return + } + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusUnauthorized) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to write response") + } + + log.Error(). + Caller(). + Str("func", "handleAuthKeyCommon"). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("Failed authentication via AuthKey") + + if pak != nil { + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + } else { + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc() + } + + return + } + + log.Debug(). + Str("func", "handleAuthKeyCommon"). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("Authentication key was valid, proceeding to acquire IP addresses") + + nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey) + + // retrieve machine information if it exist + // The error is not important, because if it does not + // exist, then this is a new machine and we will move + // on to registration. + machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) + if machine != nil { + log.Trace(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("machine was already registered before, refreshing with new auth key") + + machine.NodeKey = nodeKey + machine.AuthKeyID = uint(pak.ID) + err := h.RefreshMachine(machine, registerRequest.Expiry) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Err(err). + Msg("Failed to refresh machine") + + return + } + } else { + now := time.Now().UTC() + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.Hostinfo.Hostname). + Err(err) + + return + } + + machineToRegister := Machine{ + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NamespaceID: pak.Namespace.ID, + MachineKey: MachinePublicKeyStripPrefix(machineKey), + RegisterMethod: RegisterMethodAuthKey, + Expiry: ®isterRequest.Expiry, + NodeKey: nodeKey, + LastSeen: &now, + AuthKeyID: uint(pak.ID), + } + + machine, err = h.RegisterMachine( + machineToRegister, + ) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("could not register machine") + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + } + + err = h.UsePreAuthKey(pak) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to use pre-auth key") + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + resp.MachineAuthorized = true + resp.User = *pak.Namespace.toUser() + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("func", "handleAuthKeyCommon"). + Str("machine", registerRequest.Hostinfo.Hostname). + Err(err). + Msg("Cannot encode message") + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). + Inc() + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Str("func", "handleAuthKeyCommon"). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). + Msg("Successfully authenticated via AuthKey") +} + +// handleNewMachineCommon exposes for both legacy and Noise the functionality to get a URL +// for authorizing the machine. This url is then showed to the user by the local Tailscale client. +func (h *Headscale) handleNewMachineCommon( + writer http.ResponseWriter, + req *http.Request, + registerRequest tailcfg.RegisterRequest, + machineKey key.MachinePublic, +) { + resp := tailcfg.RegisterResponse{} + + // The machine registration is new, redirect the client to the registration URL + log.Debug(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("The node seems to be new, sending auth url") + if h.cfg.OIDC.Issuer != "" { + resp.AuthURL = fmt.Sprintf( + "%s/oidc/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), + NodePublicKeyStripPrefix(registerRequest.NodeKey), + ) + } else { + resp.AuthURL = fmt.Sprintf("%s/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), + NodePublicKeyStripPrefix(registerRequest.NodeKey)) + } + + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Cannot encode message") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Bool("noise", machineKey.IsZero()). + Caller(). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", registerRequest.Hostinfo.Hostname). + Msg("Successfully sent auth url") +} + +func (h *Headscale) handleMachineLogOutCommon( + writer http.ResponseWriter, + req *http.Request, + machine Machine, + machineKey key.MachinePublic, +) { + resp := tailcfg.RegisterResponse{} + + log.Info(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Client requested logout") + + err := h.ExpireMachine(&machine) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("func", "handleMachineLogOutCommon"). + Err(err). + Msg("Failed to expire machine") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + resp.AuthURL = "" + resp.MachineAuthorized = false + resp.User = *machine.Namespace.toUser() + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Cannot encode message") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Bool("noise", machineKey.IsZero()). + Caller(). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Successfully logged out") +} + +func (h *Headscale) handleMachineValidRegistrationCommon( + writer http.ResponseWriter, + req *http.Request, + machine Machine, + machineKey key.MachinePublic, +) { + resp := tailcfg.RegisterResponse{} + + // The machine registration is valid, respond with redirect to /map + log.Debug(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Client is registered and we have the current NodeKey. All clear to /map") + + resp.AuthURL = "" + resp.MachineAuthorized = true + resp.User = *machine.Namespace.toUser() + resp.Login = *machine.Namespace.toLogin() + + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Cannot encode message") + machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name). + Inc() + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Machine successfully authorized") +} + +func (h *Headscale) handleMachineRefreshKeyCommon( + writer http.ResponseWriter, + req *http.Request, + registerRequest tailcfg.RegisterRequest, + machine Machine, + machineKey key.MachinePublic, +) { + resp := tailcfg.RegisterResponse{} + + log.Debug(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("We have the OldNodeKey in the database. This is a key refresh") + machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) + + if err := h.db.Save(&machine).Error; err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to update machine key in the database") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + resp.AuthURL = "" + resp.User = *machine.Namespace.toUser() + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Cannot encode message") + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("old_node_key", registerRequest.OldNodeKey.ShortString()). + Str("machine", machine.Hostname). + Msg("Machine successfully refreshed") + +} + +func (h *Headscale) handleMachineExpiredCommon( + writer http.ResponseWriter, + req *http.Request, + registerRequest tailcfg.RegisterRequest, + machine Machine, + machineKey key.MachinePublic, +) { + resp := tailcfg.RegisterResponse{} + + // The client has registered before, but has expired + log.Debug(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Machine registration has expired. Sending a authurl to register") + + if registerRequest.Auth.AuthKey != "" { + h.handleAuthKeyCommon(writer, req, registerRequest, machineKey) + + return + } + + if h.cfg.OIDC.Issuer != "" { + resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), + NodePublicKeyStripPrefix(registerRequest.NodeKey)) + } else { + resp.AuthURL = fmt.Sprintf("%s/register/%s", + strings.TrimSuffix(h.cfg.ServerURL, "/"), + NodePublicKeyStripPrefix(registerRequest.NodeKey)) + } + + respBody, err := h.marshalResponse(resp, machineKey) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Cannot encode message") + machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name). + Inc() + http.Error(writer, "Internal server error", http.StatusInternalServerError) + + return + } + machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). + Inc() + + writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.WriteHeader(http.StatusOK) + _, err = writer.Write(respBody) + if err != nil { + log.Error(). + Caller(). + Bool("noise", machineKey.IsZero()). + Err(err). + Msg("Failed to write response") + } + + log.Info(). + Caller(). + Bool("noise", machineKey.IsZero()). + Str("machine", machine.Hostname). + Msg("Auth URL for reauthenticate successfully sent") +} diff --git a/protocol_legacy.go b/protocol_legacy.go index 5c23b172..b943dc9f 100644 --- a/protocol_legacy.go +++ b/protocol_legacy.go @@ -1,14 +1,11 @@ package headscale import ( - "errors" "io" "net/http" - "time" "github.com/gorilla/mux" "github.com/rs/zerolog/log" - "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" ) @@ -57,143 +54,6 @@ func (h *Headscale) RegistrationHandler( return } - now := time.Now().UTC() - machine, err := h.GetMachineByMachineKey(machineKey) - if errors.Is(err, gorm.ErrRecordNotFound) { - machineKeyStr := MachinePublicKeyStripPrefix(machineKey) + h.handleRegisterCommon(writer, req, registerRequest, machineKey) - // If the machine has AuthKey set, handle registration via PreAuthKeys - if registerRequest.Auth.AuthKey != "" { - h.handleAuthKey(writer, req, machineKey, registerRequest) - - return - } - - // Check if the node is waiting for interactive login. - // - // TODO(juan): We could use this field to improve our protocol implementation, - // and hold the request until the client closes it, or the interactive - // login is completed (i.e., the user registers the machine). - // This is not implemented yet, as it is no strictly required. The only side-effect - // is that the client will hammer headscale with requests until it gets a - // successful RegisterResponse. - if registerRequest.Followup != "" { - if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("Machine is waiting for interactive login") - - ticker := time.NewTicker(registrationHoldoff) - select { - case <-req.Context().Done(): - return - case <-ticker.C: - h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) - - return - } - } - } - - log.Info(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("New machine not yet in the database") - - givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err) - - return - } - - // The machine did not have a key to authenticate, which means - // that we rely on a method that calls back some how (OpenID or CLI) - // We create the machine and then keep it around until a callback - // happens - newMachine := Machine{ - MachineKey: machineKeyStr, - Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, - NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), - LastSeen: &now, - Expiry: &time.Time{}, - } - - if !registerRequest.Expiry.IsZero() { - log.Trace(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Time("expiry", registerRequest.Expiry). - Msg("Non-zero expiry time requested") - newMachine.Expiry = ®isterRequest.Expiry - } - - h.registrationCache.Set( - newMachine.NodeKey, - newMachine, - registerCacheExpiration, - ) - - h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) - - return - } - - // The machine is already registered, so we need to pass through reauth or key update. - if machine != nil { - // If the NodeKey stored in headscale is the same as the key presented in a registration - // request, then we have a node that is either: - // - Trying to log out (sending a expiry in the past) - // - A valid, registered machine, looking for the node map - // - Expired machine wanting to reauthenticate - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { - // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) - // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 - if !registerRequest.Expiry.IsZero() && - registerRequest.Expiry.UTC().Before(now) { - h.handleMachineLogOut(writer, req, machineKey, *machine) - - return - } - - // If machine is not expired, and is register, we have a already accepted this machine, - // let it proceed with a valid registration - if !machine.isExpired() { - h.handleMachineValidRegistration(writer, req, machineKey, *machine) - - return - } - } - - // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && - !machine.isExpired() { - h.handleMachineRefreshKey( - writer, - req, - machineKey, - registerRequest, - *machine, - ) - - return - } - - // The machine has expired - h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine) - - return - } } From 43ad0d4416dcfbc0499829379ebf0897514a7d1b Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 19 Aug 2022 14:24:43 +0200 Subject: [PATCH 53/63] Removed unused method --- utils.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/utils.go b/utils.go index 7727a245..e7fb13aa 100644 --- a/utils.go +++ b/utils.go @@ -135,19 +135,6 @@ func decode( return nil } -func encode( - v interface{}, - pubKey *key.MachinePublic, - privKey *key.MachinePrivate, -) ([]byte, error) { - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - - return privKey.SealTo(*pubKey, b), nil -} - func (h *Headscale) getAvailableIPs() (MachineAddresses, error) { var ips MachineAddresses var err error From b6e3cd81c6dc318c8b1bfef05d386d98a2f73638 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 19 Aug 2022 14:27:40 +0200 Subject: [PATCH 54/63] Fixed minor linting things --- protocol_common.go | 4 ---- protocol_legacy.go | 1 - 2 files changed, 5 deletions(-) diff --git a/protocol_common.go b/protocol_common.go index 998b2026..49c9138a 100644 --- a/protocol_common.go +++ b/protocol_common.go @@ -100,11 +100,9 @@ func (h *Headscale) handleRegisterCommon( registerRequest tailcfg.RegisterRequest, machineKey key.MachinePublic, ) { - now := time.Now().UTC() machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) if errors.Is(err, gorm.ErrRecordNotFound) { - // If the machine has AuthKey set, handle registration via PreAuthKeys if registerRequest.Auth.AuthKey != "" { h.handleAuthKeyCommon(writer, req, registerRequest, machineKey) @@ -242,7 +240,6 @@ func (h *Headscale) handleRegisterCommon( return } - } // handleAuthKeyCommon contains the logic to manage auth key client registration @@ -676,7 +673,6 @@ func (h *Headscale) handleMachineRefreshKeyCommon( Str("old_node_key", registerRequest.OldNodeKey.ShortString()). Str("machine", machine.Hostname). Msg("Machine successfully refreshed") - } func (h *Headscale) handleMachineExpiredCommon( diff --git a/protocol_legacy.go b/protocol_legacy.go index b943dc9f..4e75d127 100644 --- a/protocol_legacy.go +++ b/protocol_legacy.go @@ -55,5 +55,4 @@ func (h *Headscale) RegistrationHandler( } h.handleRegisterCommon(writer, req, registerRequest, machineKey) - } From c894db3dd41c97c5fb989973d0351010c98fb31a Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 19 Aug 2022 16:29:04 +0200 Subject: [PATCH 55/63] Use common core for noise registration --- protocol_noise.go | 466 +--------------------------------------------- 1 file changed, 2 insertions(+), 464 deletions(-) diff --git a/protocol_noise.go b/protocol_noise.go index 98a66d22..46f7a03b 100644 --- a/protocol_noise.go +++ b/protocol_noise.go @@ -2,16 +2,12 @@ package headscale import ( "encoding/json" - "errors" - "fmt" "io" "net/http" - "strings" - "time" "github.com/rs/zerolog/log" - "gorm.io/gorm" "tailscale.com/tailcfg" + "tailscale.com/types/key" ) // // NoiseRegistrationHandler handles the actual registration process of a machine. @@ -38,463 +34,5 @@ func (h *Headscale) NoiseRegistrationHandler( return } - log.Trace().Caller(). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("old_node_key", registerRequest.OldNodeKey.ShortString()). - Msg("New node is registering") - - now := time.Now().UTC() - machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) - if errors.Is(err, gorm.ErrRecordNotFound) { - // If the machine has AuthKey set, handle registration via PreAuthKeys - if registerRequest.Auth.AuthKey != "" { - h.handleNoiseAuthKey(writer, req, registerRequest) - - return - } - - // Check if the node is waiting for interactive login. - // - // TODO(juan): We could use this field to improve our protocol implementation, - // and hold the request until the client closes it, or the interactive - // login is completed (i.e., the user registers the machine). - // This is not implemented yet, as it is no strictly required. The only side-effect - // is that the client will hammer headscale with requests until it gets a - // successful RegisterResponse. - if registerRequest.Followup != "" { - if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("Machine is waiting for interactive login") - - ticker := time.NewTicker(registrationHoldoff) - select { - case <-req.Context().Done(): - return - case <-ticker.C: - h.handleNoiseMachineRegistrationNew(writer, req, registerRequest) - - return - } - } - } - - log.Info(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("node_key", registerRequest.NodeKey.ShortString()). - Str("node_key_old", registerRequest.OldNodeKey.ShortString()). - Str("follow_up", registerRequest.Followup). - Msg("New Noise machine not yet in the database") - - givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err) - - return - } - - // The machine did not have a key to authenticate, which means - // that we rely on a method that calls back some how (OpenID or CLI) - // We create the machine and then keep it around until a callback - // happens - newMachine := Machine{ - MachineKey: "", - Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, - NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), - LastSeen: &now, - Expiry: &time.Time{}, - } - - if !registerRequest.Expiry.IsZero() { - log.Trace(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Time("expiry", registerRequest.Expiry). - Msg("Non-zero expiry time requested") - newMachine.Expiry = ®isterRequest.Expiry - } - - h.registrationCache.Set( - NodePublicKeyStripPrefix(registerRequest.NodeKey), - newMachine, - registerCacheExpiration, - ) - - h.handleNoiseMachineRegistrationNew(writer, req, registerRequest) - - return - } - - // The machine is already registered, so we need to pass through reauth or key update. - if machine != nil { - // If the NodeKey stored in headscale is the same as the key presented in a registration - // request, then we have a node that is either: - // - Trying to log out (sending a expiry in the past) - // - A valid, registered machine, looking for the node map - // - Expired machine wanting to reauthenticate - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) { - // The client sends an Expiry in the past if the client is requesting to expire the key (aka logout) - // https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648 - if !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) { - h.handleNoiseNodeLogOut(writer, req, *machine) - - return - } - - // If machine is not expired, and is register, we have a already accepted this machine, - // let it proceed with a valid registration - if !machine.isExpired() { - h.handleNoiseNodeValidRegistration(writer, req, *machine) - - return - } - } - - // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration - if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && - !machine.isExpired() { - h.handleNoiseNodeRefreshKey(writer, req, registerRequest, *machine) - - return - } - - // The node has expired - h.handleNoiseNodeExpired(writer, req, registerRequest, *machine) - - return - } -} - -func (h *Headscale) handleNoiseAuthKey( - writer http.ResponseWriter, - req *http.Request, - registerRequest tailcfg.RegisterRequest, -) { - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname) - resp := tailcfg.RegisterResponse{} - - pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey) - if err != nil { - log.Error(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Err(err). - Msg("Failed authentication via AuthKey") - resp.MachineAuthorized = false - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusUnauthorized) - err = json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to encode response") - } - - log.Error(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("Failed authentication via AuthKey over Noise") - - if pak != nil { - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - } else { - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc() - } - - return - } - - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("Authentication key was valid, proceeding to acquire IP addresses") - - nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey) - - // retrieve machine information if it exist - // The error is not important, because if it does not - // exist, then this is a new machine and we will move - // on to registration. - machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey) - if machine != nil { - log.Trace(). - Caller(). - Str("machine", machine.Hostname). - Msg("machine already registered, refreshing with new auth key") - - machine.NodeKey = nodeKey - machine.AuthKeyID = uint(pak.ID) - err = h.RefreshMachine(machine, registerRequest.Expiry) - if err != nil { - log.Error(). - Caller(). - Str("machine", machine.Hostname). - Err(err). - Msg("Failed to refresh machine") - - return - } - } else { - now := time.Now().UTC() - - givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err) - - return - } - - machineToRegister := Machine{ - Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, - NamespaceID: pak.Namespace.ID, - MachineKey: "", - RegisterMethod: RegisterMethodAuthKey, - Expiry: ®isterRequest.Expiry, - NodeKey: nodeKey, - LastSeen: &now, - AuthKeyID: uint(pak.ID), - } - - machine, err = h.RegisterMachine( - machineToRegister, - ) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("could not register machine") - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - } - - err = h.UsePreAuthKey(pak) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to use pre-auth key") - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name). - Inc() - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - resp.MachineAuthorized = true - resp.User = *pak.Namespace.toUser() - - machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name). - Inc() - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err = json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to encode response") - - return - } - - log.Info(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). - Msg("Successfully authenticated via AuthKey on Noise") -} - -func (h *Headscale) handleNoiseNodeValidRegistration( - writer http.ResponseWriter, - req *http.Request, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - // The machine registration is valid, respond with redirect to /map - log.Debug(). - Str("machine", machine.Hostname). - Msg("Client is registered and we have the current NodeKey. All clear to /map") - - resp.AuthURL = "" - resp.MachineAuthorized = true - resp.User = *machine.Namespace.toUser() - resp.Login = *machine.Namespace.toLogin() - - machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name). - Inc() - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err := json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to encode response") - } -} - -func (h *Headscale) handleNoiseMachineRegistrationNew( - writer http.ResponseWriter, - req *http.Request, - registerRequest tailcfg.RegisterRequest, -) { - resp := tailcfg.RegisterResponse{} - - // The machine registration is new, redirect the client to the registration URL - log.Debug(). - Caller(). - Str("machine", registerRequest.Hostinfo.Hostname). - Msg("The node seems to be new, sending auth url") - if h.cfg.OIDC.Issuer != "" { - resp.AuthURL = fmt.Sprintf( - "%s/oidc/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), - NodePublicKeyStripPrefix(registerRequest.NodeKey), - ) - } else { - resp.AuthURL = fmt.Sprintf("%s/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err := json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to encode response") - } -} - -func (h *Headscale) handleNoiseNodeLogOut( - writer http.ResponseWriter, - req *http.Request, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - log.Info(). - Str("machine", machine.Hostname). - Msg("Client requested logout") - - err := h.ExpireMachine(&machine) - if err != nil { - log.Error(). - Caller(). - Str("func", "handleMachineLogOut"). - Err(err). - Msg("Failed to expire machine") - http.Error(writer, "Internal server error", http.StatusInternalServerError) - - return - } - - resp.AuthURL = "" - resp.MachineAuthorized = false - resp.User = *machine.Namespace.toUser() - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err = json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("could not encode response") - } -} - -func (h *Headscale) handleNoiseNodeRefreshKey( - writer http.ResponseWriter, - req *http.Request, - registerRequest tailcfg.RegisterRequest, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - log.Debug(). - Str("machine", machine.Hostname). - Msg("We have the OldNodeKey in the database. This is a key refresh") - machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) - h.db.Save(&machine) - - resp.AuthURL = "" - resp.User = *machine.Namespace.toUser() - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err := json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to encode response") - } -} - -func (h *Headscale) handleNoiseNodeExpired( - writer http.ResponseWriter, - req *http.Request, - registerRequest tailcfg.RegisterRequest, - machine Machine, -) { - resp := tailcfg.RegisterResponse{} - - // The client has registered before, but has expired - log.Debug(). - Caller(). - Str("machine", machine.Hostname). - Msg("Machine registration has expired. Sending a authurl to register") - - if registerRequest.Auth.AuthKey != "" { - h.handleNoiseAuthKey(writer, req, registerRequest) - - return - } - - if h.cfg.OIDC.Issuer != "" { - resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } else { - resp.AuthURL = fmt.Sprintf("%s/register/%s", - strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey)) - } - - machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name). - Inc() - - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) - err := json.NewEncoder(writer).Encode(resp) - if err != nil { - log.Error().Caller().Err(err).Msg("Failed to encode response") - } + h.handleRegisterCommon(writer, req, registerRequest, key.MachinePublic{}) } From 2f554133c5d849d2a4ca92cd8020dd1dd3a589b7 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 19 Aug 2022 23:49:06 +0200 Subject: [PATCH 56/63] Move comment up Co-authored-by: Kristoffer Dalby --- machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine.go b/machine.go index 89f0c7e7..960683eb 100644 --- a/machine.go +++ b/machine.go @@ -600,8 +600,8 @@ func (machine Machine) toNode( } var machineKey key.MachinePublic + // MachineKey is only used in the legacy protocol if machine.MachineKey != "" { - // MachineKey is only used in the legacy protocol err = machineKey.UnmarshalText( []byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)), ) From e9906b522fcab56e826a1234b0e7d3c24e21c0cc Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 20 Aug 2022 00:06:26 +0200 Subject: [PATCH 57/63] Use upstream AcceptHTTP for the Noise upgrade --- noise.go | 102 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 100 deletions(-) diff --git a/noise.go b/noise.go index cf18fbfb..c8e6674d 100644 --- a/noise.go +++ b/noise.go @@ -1,34 +1,18 @@ package headscale import ( - "encoding/base64" "net/http" "github.com/rs/zerolog/log" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" - "tailscale.com/control/controlbase" + "tailscale.com/control/controlhttp" "tailscale.com/net/netutil" ) -const ( - errWrongConnectionUpgrade = Error("wrong connection upgrade") - errCannotHijack = Error("cannot hijack connection") - errNoiseHandshakeFailed = Error("noise handshake failed") -) - const ( // ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade. ts2021UpgradePath = "/ts2021" - - // upgradeHeader is the value of the Upgrade HTTP header used to - // indicate the Tailscale control protocol. - upgradeHeaderValue = "tailscale-control-protocol" - - // handshakeHeaderName is the HTTP request header that can - // optionally contain base64-encoded initial handshake - // payload, to save an RTT. - handshakeHeaderName = "X-Tailscale-Handshake" ) // NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn @@ -39,14 +23,7 @@ func (h *Headscale) NoiseUpgradeHandler( ) { log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr) - // Under normal circumstances, we should be able to use the controlhttp.AcceptHTTP() - // function to do this - kindly left there by the Tailscale authors for us to use. - // (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go) - // - // When we used to use Gin, we had troubles here as Gin seems to do some - // fun stuff, and not flusing the writer properly. - // So have getNoiseConnection() that is essentially an AcceptHTTP, but in our side. - noiseConn, err := h.getNoiseConnection(writer, req) + noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey) if err != nil { log.Error().Err(err).Msg("noise upgrade failed") http.Error(writer, err.Error(), http.StatusInternalServerError) @@ -61,78 +38,3 @@ func (h *Headscale) NoiseUpgradeHandler( log.Info().Err(err).Msg("The HTTP2 server was closed") } } - -// getNoiseConnection is basically AcceptHTTP from tailscale -// TODO(juan): Figure out why we need to do this at all. -func (h *Headscale) getNoiseConnection( - writer http.ResponseWriter, - req *http.Request, -) (*controlbase.Conn, error) { - next := req.Header.Get("Upgrade") - if next == "" { - http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest) - - return nil, errWrongConnectionUpgrade - } - if next != upgradeHeaderValue { - http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest) - - return nil, errWrongConnectionUpgrade - } - - initB64 := req.Header.Get(handshakeHeaderName) - if initB64 == "" { - log.Warn(). - Caller(). - Msg("no handshake header") - http.Error(writer, "missing Tailscale handshake header", http.StatusBadRequest) - - return nil, errWrongConnectionUpgrade - } - - init, err := base64.StdEncoding.DecodeString(initB64) - if err != nil { - log.Warn().Err(err).Msg("invalid handshake header") - http.Error(writer, "invalid tailscale handshake header", http.StatusBadRequest) - - return nil, errWrongConnectionUpgrade - } - - hijacker, ok := writer.(http.Hijacker) - if !ok { - log.Error().Caller().Err(err).Msgf("Hijack failed") - http.Error(writer, errCannotHijack.Error(), http.StatusInternalServerError) - - return nil, errCannotHijack - } - - // This is what changes from the original AcceptHTTP() function. - writer.Header().Set("Upgrade", upgradeHeaderValue) - writer.Header().Set("Connection", "upgrade") - writer.WriteHeader(http.StatusSwitchingProtocols) - // end - - netConn, conn, err := hijacker.Hijack() - if err != nil { - log.Error().Caller().Err(err).Msgf("Hijack failed") - http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError) - - return nil, errCannotHijack - } - if err := conn.Flush(); err != nil { - netConn.Close() - - return nil, errCannotHijack - } - - netConn = netutil.NewDrainBufConn(netConn, conn.Reader) - - noiseConn, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init) - if err != nil { - netConn.Close() - - return nil, errNoiseHandshakeFailed - } - - return noiseConn, nil -} From 04e4fa785b70b7cecbc595e6004c7fc19fbabb91 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 20 Aug 2022 00:11:07 +0200 Subject: [PATCH 58/63] Updated dependencies --- go.mod | 4 +++- go.sum | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c01a9ff7..c934a461 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 google.golang.org/genproto v0.0.0-20220808204814-fd01256a5276 @@ -63,6 +64,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/glebarez/go-sqlite v1.17.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-github v17.0.0+incompatible // indirect @@ -126,7 +128,6 @@ require ( go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect - golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d // indirect golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect golang.org/x/text v0.3.7 // indirect @@ -139,4 +140,5 @@ require ( modernc.org/mathutil v1.4.1 // indirect modernc.org/memory v1.1.1 // indirect modernc.org/sqlite v1.17.3 // indirect + nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/go.sum b/go.sum index 7b245ca8..6870e458 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o= github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= @@ -64,6 +65,7 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= @@ -236,6 +238,10 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/glebarez/go-sqlite v1.17.3 h1:Rji9ROVSTTfjuWD6j5B+8DtkNvPILoUC3xRhkQzGxvk= github.com/glebarez/go-sqlite v1.17.3/go.mod h1:Hg+PQuhUy98XCxWEJEaWob8x7lhJzhNYF1nZbUiRGIY= github.com/glebarez/sqlite v1.4.6 h1:D5uxD2f6UJ82cHnVtO2TZ9pqsLyto3fpDKHIk2OsR8A= @@ -254,6 +260,13 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -273,6 +286,12 @@ github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2 github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= @@ -291,6 +310,8 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -398,6 +419,7 @@ github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b0 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= @@ -538,8 +560,10 @@ github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6k github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -556,6 +580,7 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= @@ -585,6 +610,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -675,9 +702,11 @@ github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdx github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= @@ -918,7 +947,11 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1 github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -1023,7 +1056,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1050,6 +1085,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1374,6 +1410,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1582,6 +1619,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= +honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83 h1:lZ9GIYaU+o5+X6ST702I/Ntyq9Y2oIMZ42rBQpem64A= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU= inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= @@ -1614,9 +1653,12 @@ mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= tailscale.com v1.28.0 h1:eW5bJMqw6eu7YUjBcgJY94uIcm5Zv+xpyTxxa7ztZOM= tailscale.com v1.28.0/go.mod h1:T9uKhlkxVPdSu1Qvp882evcS/hQ1+TAyZ7sJ/VACGRI= From 175dfa1ede2b9be7e9bd13d9b5e3d3b42698921c Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 20 Aug 2022 00:15:46 +0200 Subject: [PATCH 59/63] Update flake.nix sum --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0c257ff5..62631bfa 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorSha256 = "sha256-1VYegqEearzbqEX8ZLbsvHrRKbM/HIm/XIqQjMbvxkA="; + vendorSha256 = "sha256-paDdPsi5OfxsmgX7c5NSDSLYDipFqxxcxV3K4Tc77nQ="; ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ]; }; From f0a8a2857bce64e0585263b4edd8beff60df80ff Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 20 Aug 2022 00:23:33 +0200 Subject: [PATCH 60/63] Clarified why we have a different key --- app.go | 1 + protocol_common.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 851805dc..6e37fcd4 100644 --- a/app.go +++ b/app.go @@ -132,6 +132,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) { return nil, ErrFailedPrivateKey } + // TS2021 requires to have a different key from the legacy protocol. noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath) if err != nil { return nil, ErrFailedNoisePrivateKey diff --git a/protocol_common.go b/protocol_common.go index 49c9138a..3cce7602 100644 --- a/protocol_common.go +++ b/protocol_common.go @@ -56,8 +56,8 @@ func (h *Headscale) KeyHandler( return } + // TS2021 (Tailscale v2 protocol) requires to have a different key if clientCapabilityVersion >= NoiseCapabilityVersion { - // Tailscale has a different key for the TS2021 protocol resp := tailcfg.OverTLSPublicKeyResponse{ LegacyPublicKey: h.privateKey.Public(), PublicKey: h.noisePrivateKey.Public(), From 4424a9abc0e1c81aae6709a70c635fc71b5e308f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 21 Aug 2022 10:42:23 +0200 Subject: [PATCH 61/63] Noise private key now a nested field in config --- cmd/headscale/headscale_test.go | 20 ++++++++++++------- config-example.yaml | 14 +++++++------ config.go | 6 +++--- docs/running-headscale-container.md | 3 ++- .../etc/alt-config.dump.gold.yaml | 3 ++- integration_test/etc/alt-config.yaml | 3 ++- integration_test/etc/config.dump.gold.yaml | 3 ++- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index b0667f78..007d2801 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -163,10 +163,12 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { c.Fatal(err) } // defer os.RemoveAll(tmpDir) - - configYaml := []byte( - "---\nnoise_private_key_path: \"noise_private.key\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"\n", - ) + configYaml := []byte(`--- +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: "" +tls_cert_path: abc.pem +noise: + private_key_path: noise_private.key`) writeConfig(c, tmpDir, configYaml) // Check configuration validation errors (1) @@ -191,9 +193,13 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { ) // Check configuration validation errors (2) - configYaml = []byte( - "---\nnoise_private_key_path: \"noise_private.key\"\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", - ) + configYaml = []byte(`--- +noise: + private_key_path: noise_private.key +server_url: http://127.0.0.1:8080 +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: TLS-ALPN-01 +`) writeConfig(c, tmpDir, configYaml) err = headscale.LoadConfig(tmpDir, false) c.Assert(err, check.IsNil) diff --git a/config-example.yaml b/config-example.yaml index 5ebc1300..2019a133 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -41,12 +41,14 @@ grpc_allow_insecure: false # autogenerated if it's missing private_key_path: /var/lib/headscale/private.key -# The Noise private key is used to encrypt the -# traffic between headscale and Tailscale clients when -# using the new Noise-based TS2021 protocol. -# The noise private key file which will be -# autogenerated if it's missing -noise_private_key_path: /var/lib/headscale/noise_private.key +# The Noise section includes specific configuration for the +# TS2021 Noise procotol +noise: + # The Noise private key is used to encrypt the + # traffic between headscale and Tailscale clients when + # using the new Noise-based protocol. It must be different + # from the legacy private key. + private_key_path: /var/lib/headscale/noise_private.key # List of IP prefixes to allocate tailaddresses from. # Each prefix consists of either an IPv4 or IPv6 address, diff --git a/config.go b/config.go index e503b615..00247318 100644 --- a/config.go +++ b/config.go @@ -185,8 +185,8 @@ func LoadConfig(path string, isFile bool) error { errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n" } - if !viper.IsSet("noise_private_key_path") { - errorText += "Fatal config error: headscale now requires a new `noise_private_key_path` field in the config file for the Tailscale v2 protocol\n" + if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" { + errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n" } if (viper.GetString("tls_letsencrypt_hostname") != "") && @@ -494,7 +494,7 @@ func GetHeadscaleConfig() (*Config, error) { viper.GetString("private_key_path"), ), NoisePrivateKeyPath: AbsolutePathFromConfigPath( - viper.GetString("noise_private_key_path"), + viper.GetString("noise.private_key_path"), ), BaseDomain: baseDomain, diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 4a9f1518..d341bb7c 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -54,7 +54,8 @@ metrics_listen_addr: 0.0.0.0:9090 # The default /var/lib/headscale path is not writable in the container private_key_path: /etc/headscale/private.key # The default /var/lib/headscale path is not writable in the container -noise_private_key_path: /var/lib/headscale/noise_private.key +noise: + private_key_path: /var/lib/headscale/noise_private.key # The default /var/lib/headscale path is not writable in the container db_path: /etc/headscale/db.sqlite ``` diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml index e71c9576..3d38b128 100644 --- a/integration_test/etc/alt-config.dump.gold.yaml +++ b/integration_test/etc/alt-config.dump.gold.yaml @@ -39,7 +39,8 @@ oidc: - email strip_email_domain: true private_key_path: private.key -noise_private_key_path: noise_private.key +noise: + private_key_path: noise_private.key server_url: http://headscale:18080 tls_client_auth_mode: relaxed tls_letsencrypt_cache_dir: /var/www/.cache diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml index 35dd9e48..179fdcd5 100644 --- a/integration_test/etc/alt-config.yaml +++ b/integration_test/etc/alt-config.yaml @@ -15,7 +15,8 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key -noise_private_key_path: noise_private.key +noise: + private_key_path: noise_private.key listen_addr: 0.0.0.0:18080 metrics_listen_addr: 127.0.0.1:19090 server_url: http://headscale:18080 diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml index 71539656..91ca5b93 100644 --- a/integration_test/etc/config.dump.gold.yaml +++ b/integration_test/etc/config.dump.gold.yaml @@ -39,7 +39,8 @@ oidc: - email strip_email_domain: true private_key_path: private.key -noise_private_key_path: noise_private.key +noise: + private_key_path: noise_private.key server_url: http://headscale:8080 tls_client_auth_mode: relaxed tls_letsencrypt_cache_dir: /var/www/.cache From 71d22dc994c716508de9499983ca3a6751a58776 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 21 Aug 2022 10:47:45 +0200 Subject: [PATCH 62/63] Added missing files --- integration_test/etc/config.yaml | 3 ++- integration_test/etc_embedded_derp/config.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml index 9866d554..da842cc4 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -15,7 +15,8 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key -noise_private_key_path: noise_private.key +noise: + private_key_path: noise_private.key listen_addr: 0.0.0.0:8080 metrics_listen_addr: 127.0.0.1:9090 server_url: http://headscale:8080 diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml index 86a59848..ed4d51a0 100644 --- a/integration_test/etc_embedded_derp/config.yaml +++ b/integration_test/etc_embedded_derp/config.yaml @@ -14,7 +14,8 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key -noise_private_key_path: noise_private.key +noise: + private_key_path: noise_private.key listen_addr: 0.0.0.0:443 server_url: https://headscale:443 tls_cert_path: "/etc/headscale/tls/server.crt" From 4aafe6c9d1bba90266fcc5cdd1ba7c79fcfe6907 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 21 Aug 2022 12:32:01 +0200 Subject: [PATCH 63/63] Added line in CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e72bcc7..2f2fd517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.17.0 (2022-XX-XX) +- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738) + ## 0.16.4 (2022-08-21) ### Changes