Remove support for non-noise clients (pre-1.32) (#1611)

This commit is contained in:
Kristoffer Dalby 2023-11-23 08:31:33 +01:00 committed by GitHub
parent b918aa03fc
commit a59aab2081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 319 additions and 679 deletions

View file

@ -77,7 +77,6 @@ type Headscale struct {
dbString string
dbType string
dbDebug bool
privateKey2019 *key.MachinePrivate
noisePrivateKey *key.MachinePrivate
DERPMap *tailcfg.DERPMap
@ -101,21 +100,11 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
runtime.SetBlockProfileRate(1)
}
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err)
}
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}
if privateKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
}
var dbString string
switch cfg.DBtype {
case db.Postgres:
@ -156,7 +145,6 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey2019: privateKey,
noisePrivateKey: noisePrivateKey,
registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{},
@ -199,10 +187,18 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
}
if cfg.DERP.ServerEnabled {
// TODO(kradalby): replace this key with a dedicated DERP key.
derpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create DERP server private key: %w", err)
}
if derpServerKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf("DERP server private key and noise private key are the same: %w", err)
}
embeddedDERPServer, err := derpServer.NewDERPServer(
cfg.ServerURL,
key.NodePrivate(*privateKey),
key.NodePrivate(*derpServerKey),
&cfg.DERP,
)
if err != nil {
@ -450,7 +446,6 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{mkey}", h.RegisterWebAPI).Methods(http.MethodGet)
h.addLegacyHandlers(router)
router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
@ -914,12 +909,6 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
var machineKey key.MachinePrivate
if err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil {
log.Info().
Str("path", path).
Msg("This might be due to a legacy (headscale pre-0.12) private key. " +
"If the key is in WireGuard format, delete the key and restart headscale. " +
"A new key will automatically be generated. All Tailscale clients will have to be restarted")
return nil, fmt.Errorf("failed to parse private key: %w", err)
}

View file

@ -1,13 +1,13 @@
package hscontrol
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/juanfont/headscale/hscontrol/mapper"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
@ -16,22 +16,19 @@ import (
"tailscale.com/types/key"
)
// handleRegister is the common logic for registering a client in the legacy and Noise protocols
//
// When using Noise, the machineKey is Zero.
// handleRegister is the logic for registering a client.
func (h *Headscale) handleRegister(
writer http.ResponseWriter,
req *http.Request,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
isNoise bool,
) {
now := time.Now().UTC()
node, err := h.db.GetNodeByAnyKey(machineKey, registerRequest.NodeKey, registerRequest.OldNodeKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
// If the node has AuthKey set, handle registration via PreAuthKeys
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(writer, registerRequest, machineKey, isNoise)
h.handleAuthKey(writer, registerRequest, machineKey)
return
}
@ -53,14 +50,13 @@ func (h *Headscale) handleRegister(
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
Str("follow_up", registerRequest.Followup).
Bool("noise", isNoise).
Msg("Node is waiting for interactive login")
select {
case <-req.Context().Done():
return
case <-time.After(registrationHoldoff):
h.handleNewNode(writer, registerRequest, machineKey, isNoise)
h.handleNewNode(writer, registerRequest, machineKey)
return
}
@ -74,7 +70,6 @@ func (h *Headscale) handleRegister(
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
Str("follow_up", registerRequest.Followup).
Bool("noise", isNoise).
Msg("New node not yet in the database")
givenName, err := h.db.GenerateGivenName(
@ -108,7 +103,6 @@ func (h *Headscale) handleRegister(
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested")
@ -121,7 +115,7 @@ func (h *Headscale) handleRegister(
registerCacheExpiration,
)
h.handleNewNode(writer, registerRequest, machineKey, isNoise)
h.handleNewNode(writer, registerRequest, machineKey)
return
}
@ -157,7 +151,7 @@ func (h *Headscale) handleRegister(
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !registerRequest.Expiry.IsZero() &&
registerRequest.Expiry.UTC().Before(now) {
h.handleNodeLogOut(writer, *node, machineKey, isNoise)
h.handleNodeLogOut(writer, *node, machineKey)
return
}
@ -165,7 +159,7 @@ func (h *Headscale) handleRegister(
// If node is not expired, and it is register, we have a already accepted this node,
// let it proceed with a valid registration
if !node.IsExpired() {
h.handleNodeWithValidRegistration(writer, *node, machineKey, isNoise)
h.handleNodeWithValidRegistration(writer, *node, machineKey)
return
}
@ -179,7 +173,6 @@ func (h *Headscale) handleRegister(
registerRequest,
*node,
machineKey,
isNoise,
)
return
@ -194,7 +187,7 @@ func (h *Headscale) handleRegister(
}
// The node has expired or it is logged out
h.handleNodeExpiredOrLoggedOut(writer, registerRequest, *node, machineKey, isNoise)
h.handleNodeExpiredOrLoggedOut(writer, registerRequest, *node, machineKey)
// TODO(juan): RegisterRequest includes an Expiry time, that we could optionally use
node.Expiry = &time.Time{}
@ -215,7 +208,6 @@ func (h *Headscale) handleRegister(
}
// handleAuthKey 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.
@ -223,12 +215,10 @@ func (h *Headscale) handleAuthKey(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
isNoise bool,
) {
log.Debug().
Caller().
Str("node", registerRequest.Hostinfo.Hostname).
Bool("noise", isNoise).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
@ -236,17 +226,15 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
@ -263,14 +251,12 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey")
@ -286,7 +272,6 @@ func (h *Headscale) handleAuthKey(
log.Debug().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
@ -300,7 +285,6 @@ func (h *Headscale) handleAuthKey(
if node != nil {
log.Trace().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("node was already registered before, refreshing with new auth key")
@ -310,7 +294,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Err(err).
Msg("Failed to refresh node")
@ -318,7 +301,7 @@ func (h *Headscale) handleAuthKey(
return
}
aclTags := pak.Proto().AclTags
aclTags := pak.Proto().GetAclTags()
if len(aclTags) > 0 {
// This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login
err = h.db.SetTags(node, aclTags)
@ -326,7 +309,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Strs("aclTags", aclTags).
Err(err).
@ -342,7 +324,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("func", "RegistrationHandler").
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
Err(err).
@ -361,7 +342,7 @@ func (h *Headscale) handleAuthKey(
NodeKey: nodeKey,
LastSeen: &now,
AuthKeyID: uint(pak.ID),
ForcedTags: pak.Proto().AclTags,
ForcedTags: pak.Proto().GetAclTags(),
}
node, err = h.db.RegisterNode(
@ -370,7 +351,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("could not register node")
nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name).
@ -385,7 +365,6 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to use pre-auth key")
nodeRegistrations.WithLabelValues("new", util.RegisterMethodAuthKey, "error", pak.User.Name).
@ -401,11 +380,10 @@ func (h *Headscale) handleAuthKey(
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
resp.Login = *pak.User.TailscaleLogin()
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Err(err).
Msg("Cannot encode message")
@ -423,32 +401,29 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(node.IPAddresses.StringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey")
}
// handleNewNode exposes for both legacy and Noise the functionality to get a URL
// for authorizing the node. This url is then showed to the user by the local Tailscale client.
// handleNewNode returns the authorisation URL to the client based on what type
// of registration headscale is configured with.
// This url is then showed to the user by the local Tailscale client.
func (h *Headscale) handleNewNode(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
// The node registration is new, redirect the client to the registration URL
log.Debug().
Caller().
Bool("noise", isNoise).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("The node seems to be new, sending auth url")
@ -464,11 +439,10 @@ func (h *Headscale) handleNewNode(
machineKey.String())
}
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@ -481,7 +455,6 @@ func (h *Headscale) handleNewNode(
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Bool("noise", isNoise).
Caller().
Err(err).
Msg("Failed to write response")
@ -489,7 +462,6 @@ func (h *Headscale) handleNewNode(
log.Info().
Caller().
Bool("noise", isNoise).
Str("AuthURL", resp.AuthURL).
Str("node", registerRequest.Hostinfo.Hostname).
Msg("Successfully sent auth url")
@ -499,12 +471,10 @@ func (h *Headscale) handleNodeLogOut(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Client requested logout")
@ -513,7 +483,6 @@ func (h *Headscale) handleNodeLogOut(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to expire node")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@ -525,11 +494,10 @@ func (h *Headscale) handleNodeLogOut(
resp.MachineAuthorized = false
resp.NodeKeyExpired = true
resp.User = *node.User.TailscaleUser()
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@ -542,7 +510,6 @@ func (h *Headscale) handleNodeLogOut(
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Bool("noise", isNoise).
Caller().
Err(err).
Msg("Failed to write response")
@ -564,7 +531,6 @@ func (h *Headscale) handleNodeLogOut(
log.Info().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Successfully logged out")
}
@ -573,14 +539,12 @@ func (h *Headscale) handleNodeWithValidRegistration(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
// The node registration is valid, respond with redirect to /map
log.Debug().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
@ -589,11 +553,10 @@ func (h *Headscale) handleNodeWithValidRegistration(
resp.User = *node.User.TailscaleUser()
resp.Login = *node.User.TailscaleLogin()
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
nodeRegistrations.WithLabelValues("update", "web", "error", node.User.Name).
@ -611,14 +574,12 @@ func (h *Headscale) handleNodeWithValidRegistration(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("Node successfully authorized")
}
@ -628,13 +589,11 @@ func (h *Headscale) handleNodeKeyRefresh(
registerRequest tailcfg.RegisterRequest,
node types.Node,
machineKey key.MachinePublic,
isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
log.Info().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh")
@ -651,11 +610,10 @@ func (h *Headscale) handleNodeKeyRefresh(
resp.AuthURL = ""
resp.User = *node.User.TailscaleUser()
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
@ -669,14 +627,12 @@ func (h *Headscale) handleNodeKeyRefresh(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Info().
Caller().
Bool("noise", isNoise).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
Str("node", node.Hostname).
@ -688,12 +644,11 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
registerRequest tailcfg.RegisterRequest,
node types.Node,
machineKey key.MachinePublic,
isNoise bool,
) {
resp := tailcfg.RegisterResponse{}
if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(writer, registerRequest, machineKey, isNoise)
h.handleAuthKey(writer, registerRequest, machineKey)
return
}
@ -701,7 +656,6 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
// The client has registered before, but has expired or logged out
log.Trace().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Str("machine_key", machineKey.ShortString()).
Str("node_key", registerRequest.NodeKey.ShortString()).
@ -718,11 +672,10 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
machineKey.String())
}
respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey)
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Cannot encode message")
nodeRegistrations.WithLabelValues("reauth", "web", "error", node.User.Name).
@ -740,14 +693,12 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
if err != nil {
log.Error().
Caller().
Bool("noise", isNoise).
Err(err).
Msg("Failed to write response")
}
log.Trace().
Caller().
Bool("noise", isNoise).
Str("machine_key", machineKey.ShortString()).
Str("node_key", registerRequest.NodeKey.ShortString()).
Str("node_key_old", registerRequest.OldNodeKey.ShortString()).

View file

@ -1,61 +0,0 @@
//go:build ts2019
package hscontrol
import (
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"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("mkey:" + machineKeyStr))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse machine key")
nodeRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
return
}
registerRequest := tailcfg.RegisterRequest{}
err = util.DecodeAndUnmarshalNaCl(body, &registerRequest, &machineKey, h.privateKey2019)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot decode message")
nodeRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
h.handleRegister(writer, req, registerRequest, machineKey, false)
}

View file

@ -39,7 +39,19 @@ func (ns *noiseServer) NoiseRegistrationHandler(
return
}
// Reject unsupported versions
if registerRequest.Version < MinimumCapVersion {
log.Info().
Caller().
Int("min_version", int(MinimumCapVersion)).
Int("client_version", int(registerRequest.Version)).
Msg("unsupported client connected")
http.Error(writer, "Internal error", http.StatusBadRequest)
return
}
ns.nodeKey = registerRequest.NodeKey
ns.headscale.handleRegister(writer, req, registerRequest, ns.conn.Peer(), true)
ns.headscale.handleRegister(writer, req, registerRequest, ns.conn.Peer())
}

View file

@ -198,5 +198,5 @@ func (*Suite) TestPreAuthKeyACLTags(c *check.C) {
listedPaks, err := db.ListPreAuthKeys("test8")
c.Assert(err, check.IsNil)
c.Assert(listedPaks[0].Proto().AclTags, check.DeepEquals, tags)
c.Assert(listedPaks[0].Proto().GetAclTags(), check.DeepEquals, tags)
}

View file

@ -1,15 +0,0 @@
//go:build ts2019
package hscontrol
import (
"net/http"
"github.com/gorilla/mux"
)
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
router.HandleFunc("/machine/{mkey}/map", h.PollNetMapHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/{mkey}", h.RegistrationHandler).Methods(http.MethodPost)
}

View file

@ -1,8 +0,0 @@
//go:build !ts2019
package hscontrol
import "github.com/gorilla/mux"
func (h *Headscale) addLegacyHandlers(router *mux.Router) {
}

View file

@ -8,7 +8,6 @@ import (
"html/template"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
@ -63,26 +62,6 @@ func (h *Headscale) KeyHandler(
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
capVer, err := parseCabailityVersion(req)
if err != nil {
if errors.Is(err, ErrNoCapabilityVersion) {
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")
writer.WriteHeader(http.StatusOK)
_, err := writer.Write(
[]byte(strings.TrimPrefix(h.privateKey2019.Public().String(), "mkey:")),
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
log.Error().
Caller().
Err(err).
@ -101,7 +80,7 @@ func (h *Headscale) KeyHandler(
log.Debug().
Str("handler", "/key").
Int("v", int(capVer)).
Int("cap_ver", int(capVer)).
Msg("New noise client")
if err != nil {
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@ -120,8 +99,7 @@ func (h *Headscale) KeyHandler(
// TS2021 (Tailscale v2 protocol) requires to have a different key
if capVer >= NoiseCapabilityVersion {
resp := tailcfg.OverTLSPublicKeyResponse{
LegacyPublicKey: h.privateKey2019.Public(),
PublicKey: h.noisePrivateKey.Public(),
PublicKey: h.noisePrivateKey.Public(),
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)

View file

@ -25,7 +25,6 @@ import (
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
)
const (
@ -48,10 +47,6 @@ var debugDumpMapResponsePath = envknob.String("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_
// - Create a "minifier" that removes info not needed for the node
type Mapper struct {
privateKey2019 *key.MachinePrivate
isNoise bool
capVer tailcfg.CapabilityVersion
// Configuration
// TODO(kradalby): figure out if this is the format we want this in
derpMap *tailcfg.DERPMap
@ -73,9 +68,6 @@ type Mapper struct {
func NewMapper(
node *types.Node,
peers types.Nodes,
privateKey *key.MachinePrivate,
isNoise bool,
capVer tailcfg.CapabilityVersion,
derpMap *tailcfg.DERPMap,
baseDomain string,
dnsCfg *tailcfg.DNSConfig,
@ -84,17 +76,12 @@ func NewMapper(
) *Mapper {
log.Debug().
Caller().
Bool("noise", isNoise).
Str("node", node.Hostname).
Msg("creating new mapper")
uid, _ := util.GenerateRandomStringDNSSafe(mapperIDLength)
return &Mapper{
privateKey2019: privateKey,
isNoise: isNoise,
capVer: capVer,
derpMap: derpMap,
baseDomain: baseDomain,
dnsCfg: dnsCfg,
@ -212,10 +199,11 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
func (m *Mapper) fullMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
peers := nodeMapToList(m.peers)
resp, err := m.baseWithConfigMapResponse(node, pol)
resp, err := m.baseWithConfigMapResponse(node, pol, capVer)
if err != nil {
return nil, err
}
@ -224,7 +212,7 @@ func (m *Mapper) fullMapResponse(
resp,
pol,
node,
m.capVer,
capVer,
peers,
peers,
m.baseDomain,
@ -247,15 +235,11 @@ func (m *Mapper) FullMapResponse(
m.mu.Lock()
defer m.mu.Unlock()
resp, err := m.fullMapResponse(node, pol)
resp, err := m.fullMapResponse(node, pol, mapRequest.Version)
if err != nil {
return nil, err
}
if m.isNoise {
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
}
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
}
@ -267,15 +251,11 @@ func (m *Mapper) LiteMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
) ([]byte, error) {
resp, err := m.baseWithConfigMapResponse(node, pol)
resp, err := m.baseWithConfigMapResponse(node, pol, mapRequest.Version)
if err != nil {
return nil, err
}
if m.isNoise {
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
}
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress)
}
@ -325,7 +305,7 @@ func (m *Mapper) PeerChangedResponse(
&resp,
pol,
node,
m.capVer,
mapRequest.Version,
nodeMapToList(m.peers),
changed,
m.baseDomain,
@ -414,15 +394,8 @@ func (m *Mapper) marshalMapResponse(
var respBody []byte
if compression == util.ZstdCompression {
respBody = zstdEncode(jsonBody)
if !m.isNoise { // if legacy protocol
respBody = m.privateKey2019.SealTo(node.MachineKey, respBody)
}
} else {
if !m.isNoise { // if legacy protocol
respBody = m.privateKey2019.SealTo(node.MachineKey, jsonBody)
} else {
respBody = jsonBody
}
respBody = jsonBody
}
data := make([]byte, reservedResponseHeaderSize)
@ -432,32 +405,6 @@ func (m *Mapper) marshalMapResponse(
return data, nil
}
// MarshalResponse takes an Tailscale Response, marhsal it to JSON.
// If isNoise is set, then the JSON body will be returned
// If !isNoise and privateKey2019 is set, the JSON body will be sealed in a Nacl box.
func MarshalResponse(
resp interface{},
isNoise bool,
privateKey2019 *key.MachinePrivate,
machineKey key.MachinePublic,
) ([]byte, error) {
jsonBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot marshal response")
return nil, err
}
if !isNoise && privateKey2019 != nil {
return privateKey2019.SealTo(machineKey, jsonBody), nil
}
return jsonBody, nil
}
func zstdEncode(in []byte) []byte {
encoder, ok := zstdEncoderPool.Get().(*zstd.Encoder)
if !ok {
@ -503,10 +450,11 @@ func (m *Mapper) baseMapResponse() tailcfg.MapResponse {
func (m *Mapper) baseWithConfigMapResponse(
node *types.Node,
pol *policy.ACLPolicy,
capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
resp := m.baseMapResponse()
tailnode, err := tailNode(node, m.capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort)
tailnode, err := tailNode(node, capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort)
if err != nil {
return nil, err
}

View file

@ -472,9 +472,6 @@ func Test_fullMapResponse(t *testing.T) {
mappy := NewMapper(
tt.node,
tt.peers,
nil,
false,
0,
tt.derpMap,
tt.baseDomain,
tt.dnsConfig,
@ -485,6 +482,7 @@ func Test_fullMapResponse(t *testing.T) {
got, err := mappy.fullMapResponse(
tt.node,
tt.pol,
0,
)
if (err != nil) != tt.wantErr {

View file

@ -25,12 +25,10 @@ type UpdateNode func()
func logPollFunc(
mapRequest tailcfg.MapRequest,
node *types.Node,
isNoise bool,
) (func(string), func(error, string)) {
return func(msg string) {
log.Info().
Caller().
Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
@ -41,7 +39,6 @@ func logPollFunc(
func(err error, msg string) {
log.Error().
Caller().
Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
@ -52,8 +49,8 @@ func logPollFunc(
}
}
// handlePoll is the common code for the legacy and Noise protocols to
// managed the poll loop.
// handlePoll ensures the node gets the appropriate updates from either
// polling or immediate responses.
//
//nolint:gocyclo
func (h *Headscale) handlePoll(
@ -61,10 +58,8 @@ func (h *Headscale) handlePoll(
ctx context.Context,
node *types.Node,
mapRequest tailcfg.MapRequest,
isNoise bool,
capVer tailcfg.CapabilityVersion,
) {
logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
logInfo, logErr := logPollFunc(mapRequest, node)
// This is the mechanism where the node gives us inforamtion about its
// current configuration.
@ -77,12 +72,12 @@ func (h *Headscale) handlePoll(
if mapRequest.OmitPeers && !mapRequest.Stream && !mapRequest.ReadOnly {
log.Info().
Caller().
Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
Str("node_key", node.NodeKey.ShortString()).
Str("node", node.Hostname).
Int("cap_ver", int(mapRequest.Version)).
Msg("Received endpoint update")
now := time.Now().UTC()
@ -129,7 +124,7 @@ func (h *Headscale) handlePoll(
// The intended use is for clients to discover the DERP map at
// start-up before their first real endpoint update.
} else if mapRequest.OmitPeers && !mapRequest.Stream && mapRequest.ReadOnly {
h.handleLiteRequest(writer, node, mapRequest, isNoise, capVer)
h.handleLiteRequest(writer, node, mapRequest)
return
} else if mapRequest.OmitPeers && mapRequest.Stream {
@ -160,9 +155,6 @@ func (h *Headscale) handlePoll(
mapp := mapper.NewMapper(
node,
peers,
h.privateKey2019,
isNoise,
capVer,
h.DERPMap,
h.cfg.BaseDomain,
h.cfg.DNSConfig,
@ -337,7 +329,6 @@ func (h *Headscale) handlePoll(
log.Info().
Caller().
Bool("noise", isNoise).
Bool("readOnly", mapRequest.ReadOnly).
Bool("omitPeers", mapRequest.OmitPeers).
Bool("stream", mapRequest.Stream).
@ -382,19 +373,14 @@ func (h *Headscale) handleLiteRequest(
writer http.ResponseWriter,
node *types.Node,
mapRequest tailcfg.MapRequest,
isNoise bool,
capVer tailcfg.CapabilityVersion,
) {
logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
logInfo, logErr := logPollFunc(mapRequest, node)
mapp := mapper.NewMapper(
node,
// TODO(kradalby): It might not be acceptable to send
// an empty peer list here.
types.Nodes{},
h.privateKey2019,
isNoise,
capVer,
h.DERPMap,
h.cfg.BaseDomain,
h.cfg.DNSConfig,

View file

@ -1,108 +0,0 @@
//go:build ts2019
package hscontrol
import (
"errors"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/util"
"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("mkey:" + 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 = util.DecodeAndUnmarshalNaCl(body, &mapRequest, &machineKey, h.privateKey2019)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot decode message")
http.Error(writer, "Cannot decode message", http.StatusBadRequest)
return
}
node, err := h.db.GetNodeByMachineKey(machineKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find node with key %s", machineKey.String())
http.Error(writer, "", http.StatusUnauthorized)
return
}
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch node from the database with Machine key: %s", machineKey.String())
http.Error(writer, "", http.StatusInternalServerError)
return
}
log.Trace().
Str("handler", "PollNetMap").
Str("id", machineKeyStr).
Str("node", node.Hostname).
Msg("A node is sending a MapRequest via legacy protocol")
capVer, err := parseCabailityVersion(req)
if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
log.Error().
Caller().
Err(err).
Msg("failed to parse capVer")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
h.handlePoll(writer, req.Context(), node, mapRequest, false, capVer)
}

View file

@ -12,6 +12,10 @@ import (
"tailscale.com/types/key"
)
const (
MinimumCapVersion tailcfg.CapabilityVersion = 36
)
// 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
@ -47,6 +51,18 @@ func (ns *noiseServer) NoisePollNetMapHandler(
return
}
// Reject unsupported versions
if mapRequest.Version < MinimumCapVersion {
log.Info().
Caller().
Int("min_version", int(MinimumCapVersion)).
Int("client_version", int(mapRequest.Version)).
Msg("unsupported client connected")
http.Error(writer, "Internal error", http.StatusBadRequest)
return
}
ns.nodeKey = mapRequest.NodeKey
node, err := ns.headscale.db.GetNodeByAnyKey(
@ -73,20 +89,8 @@ func (ns *noiseServer) NoisePollNetMapHandler(
log.Debug().
Str("handler", "NoisePollNetMap").
Str("node", node.Hostname).
Int("cap_ver", int(mapRequest.Version)).
Msg("A node sending a MapRequest with Noise protocol")
capVer, err := parseCabailityVersion(req)
if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
log.Error().
Caller().
Err(err).
Msg("failed to parse capVer")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
// TODO(kradalby): since we are now passing capVer, we could arguably stop passing
// isNoise, and rather have a isNoise function that takes capVer
ns.headscale.handlePoll(writer, req.Context(), node, mapRequest, true, capVer)
ns.headscale.handlePoll(writer, req.Context(), node, mapRequest)
}

View file

@ -40,7 +40,6 @@ func (s *Suite) ResetDB(c *check.C) {
c.Fatal(err)
}
cfg := types.Config{
PrivateKeyPath: tmpDir + "/private.key",
NoisePrivateKeyPath: tmpDir + "/noise_private.key",
DBtype: "sqlite3",
DBpath: tmpDir + "/headscale_test.db",

View file

@ -41,7 +41,6 @@ type Config struct {
EphemeralNodeInactivityTimeout time.Duration
NodeUpdateCheckInterval time.Duration
IPPrefixes []netip.Prefix
PrivateKeyPath string
NoisePrivateKeyPath string
BaseDomain string
Log LogConfig
@ -108,15 +107,16 @@ type OIDCConfig struct {
}
type DERPConfig struct {
ServerEnabled bool
ServerRegionID int
ServerRegionCode string
ServerRegionName string
STUNAddr string
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
ServerEnabled bool
ServerRegionID int
ServerRegionCode string
ServerRegionName string
ServerPrivateKeyPath string
STUNAddr string
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
type LogTailConfig struct {
@ -286,6 +286,7 @@ func GetDERPConfig() DERPConfig {
serverRegionCode := viper.GetString("derp.server.region_code")
serverRegionName := viper.GetString("derp.server.region_name")
stunAddr := viper.GetString("derp.server.stun_listen_addr")
privateKeyPath := util.AbsolutePathFromConfigPath(viper.GetString("derp.server.private_key_path"))
if serverEnabled && stunAddr == "" {
log.Fatal().
@ -313,15 +314,16 @@ func GetDERPConfig() DERPConfig {
updateFrequency := viper.GetDuration("derp.update_frequency")
return DERPConfig{
ServerEnabled: serverEnabled,
ServerRegionID: serverRegionID,
ServerRegionCode: serverRegionCode,
ServerRegionName: serverRegionName,
STUNAddr: stunAddr,
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
ServerEnabled: serverEnabled,
ServerRegionID: serverRegionID,
ServerRegionCode: serverRegionCode,
ServerRegionName: serverRegionName,
ServerPrivateKeyPath: privateKeyPath,
STUNAddr: stunAddr,
URLs: urls,
Paths: paths,
AutoUpdate: autoUpdate,
UpdateFrequency: updateFrequency,
}
}
@ -582,9 +584,6 @@ func GetHeadscaleConfig() (*Config, error) {
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
IPPrefixes: prefixes,
PrivateKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("private_key_path"),
),
NoisePrivateKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("noise.private_key_path"),
),