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 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 diff --git a/api.go b/api.go index ac5f2a3f..18ac72fe 100644 --- a/api.go +++ b/api.go @@ -2,22 +2,13 @@ package headscale import ( "bytes" - "encoding/binary" "encoding/json" - "errors" - "fmt" "html/template" - "io" "net/http" - "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" ) const ( @@ -70,23 +61,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, -) { - 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 } @@ -164,761 +138,3 @@ func (h *Headscale) RegisterWebAPI( Msg("Failed to write response") } } - -// 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) - - machine.Expiry = &time.Time{} - h.registrationCache.Set( - NodePublicKeyStripPrefix(registerRequest.NodeKey), - *machine, - registerCacheExpiration, - ) - - return - } -} - -func (h *Headscale) getMapResponse( - 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) - 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) - 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) getMapKeepAliveResponse( - machineKey key.MachinePublic, - mapRequest tailcfg.MapRequest, -) ([]byte, error) { - mapResponse := tailcfg.MapResponse{ - KeepAlive: true, - } - var respBody []byte - var err error - if mapRequest.Compress == "zstd" { - 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, - 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(). - 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/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 +} diff --git a/app.go b/app.go index 7ed9d2ed..6e37fcd4 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 ( @@ -72,12 +76,15 @@ 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 + + noiseMux *mux.Router DERPMap *tailcfg.DERPMap DERPServer *DERPServer @@ -120,9 +127,19 @@ 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) + 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 + } + + if privateKey.Equal(*noisePrivateKey) { + return nil, ErrSamePrivateKeys } var dbString string @@ -161,7 +178,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{}, @@ -257,7 +275,7 @@ func (h *Headscale) expireEphemeralNodesWorker() { } if expiredFound { - h.setLastStateChangeToNow(namespace.Name) + h.setLastStateChangeToNow() } } } @@ -425,6 +443,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) @@ -454,6 +474,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) + + return router +} + // Serve launches a GIN server with the Headscale API. func (h *Headscale) Serve() error { var err error @@ -607,9 +636,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, 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/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 917ae432..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( - "---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"", - ) + 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( - "---\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 b369029c..2019a133 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -41,6 +41,15 @@ grpc_allow_insecure: false # autogenerated if it's missing private_key_path: /var/lib/headscale/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, # and the associated prefix length, delimited by a slash. diff --git a/config.go b/config.go index 3c241b29..00247318 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 @@ -184,6 +185,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") || 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") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { @@ -488,6 +493,9 @@ func GetHeadscaleConfig() (*Config, error) { PrivateKeyPath: AbsolutePathFromConfigPath( viper.GetString("private_key_path"), ), + NoisePrivateKeyPath: AbsolutePathFromConfigPath( + viper.GetString("noise.private_key_path"), + ), BaseDomain: baseDomain, DERP: derpConfig, diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index b36f3bbf..d341bb7c 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -53,6 +53,9 @@ 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 ``` diff --git a/flake.nix b/flake.nix index 0e8bdc94..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-RzmnAh81BN4tbzAGzJbb6CMuws8kuPJDw7aPkRRnSS8="; + vendorSha256 = "sha256-paDdPsi5OfxsmgX7c5NSDSLYDipFqxxcxV3K4Tc77nQ="; ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ]; }; 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= diff --git a/integration_general_test.go b/integration_general_test.go index d4c64c38..9f2ca3ea 100644 --- a/integration_general_test.go +++ b/integration_general_test.go @@ -562,13 +562,25 @@ 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][1]), + 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, @@ -582,6 +594,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { ) return err }) + assert.Nil(t, err) }) } @@ -683,6 +696,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 +718,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) diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml index e8934230..3d38b128 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 @@ -38,6 +39,8 @@ 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..179fdcd5 100644 --- a/integration_test/etc/alt-config.yaml +++ b/integration_test/etc/alt-config.yaml @@ -11,9 +11,12 @@ 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 +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..91ca5b93 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 @@ -38,6 +39,8 @@ 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/config.yaml b/integration_test/etc/config.yaml index e6b34afa..da842cc4 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -11,9 +11,12 @@ 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 +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 e6ad3b00..ed4d51a0 100644 --- a/integration_test/etc_embedded_derp/config.yaml +++ b/integration_test/etc_embedded_derp/config.yaml @@ -14,8 +14,10 @@ dns_config: - 1.1.1.1 db_path: /tmp/integration_test_db.sqlite3 private_key_path: private.key -listen_addr: 0.0.0.0:8443 -server_url: https://headscale:8443 +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" tls_key_path: "/etc/headscale/tls/server.key" tls_client_auth_mode: disabled diff --git a/machine.go b/machine.go index 1a48a3e1..4399029c 100644 --- a/machine.go +++ b/machine.go @@ -245,8 +245,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 @@ -376,6 +376,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 { @@ -398,7 +411,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) @@ -412,7 +425,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) @@ -439,7 +452,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) @@ -455,7 +468,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( @@ -588,11 +601,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) + // MachineKey is only used in the legacy protocol + if machine.MachineKey != "" { + 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 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) diff --git a/noise.go b/noise.go new file mode 100644 index 00000000..c8e6674d --- /dev/null +++ b/noise.go @@ -0,0 +1,40 @@ +package headscale + +import ( + "net/http" + + "github.com/rs/zerolog/log" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "tailscale.com/control/controlhttp" + "tailscale.com/net/netutil" +) + +const ( + // ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade. + ts2021UpgradePath = "/ts2021" +) + +// 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) + + 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) + + return + } + + server := http.Server{} + server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{}) + err = server.Serve(netutil.NewOneConnListener(noiseConn, nil)) + if err != nil { + log.Info().Err(err).Msg("The HTTP2 server was closed") + } +} diff --git a/protocol_common.go b/protocol_common.go new file mode 100644 index 00000000..154c14c9 --- /dev/null +++ b/protocol_common.go @@ -0,0 +1,749 @@ +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 ( + // 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( + 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 != "" { + 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") + writer.WriteHeader(http.StatusBadRequest) + _, err := writer.Write([]byte("Wrong params")) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + + // TS2021 (Tailscale v2 protocol) requires to have a different key + if clientCapabilityVersion >= NoiseCapabilityVersion { + 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 + } + } + 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(MachinePublicKeyStripPrefix(h.privateKey.Public()))) + if err != nil { + log.Error(). + Caller(). + Err(err). + 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) + + machine.Expiry = &time.Time{} + h.registrationCache.Set( + NodePublicKeyStripPrefix(registerRequest.NodeKey), + *machine, + registerCacheExpiration, + ) + + 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/poll.go b/protocol_common_poll.go similarity index 83% rename from poll.go rename to protocol_common_poll.go index 9c17b5cb..65dcb556 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,15 @@ 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( +// 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, + 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,11 +34,11 @@ 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(). - Str("func", "handleAuthKey"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Err(err) } @@ -133,7 +60,8 @@ func (h *Headscale) PollNetMapHandler( if err != nil { log.Error(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Bool("noise", isNoise). + Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). Msg("Failed to persist/update machine in the database") @@ -143,11 +71,12 @@ func (h *Headscale) PollNetMapHandler( } } - data, err := h.getMapResponse(machineKey, mapRequest, machine) + mapResp, err := h.getMapResponseData(mapRequest, machine, isNoise) if err != nil { log.Error(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Bool("noise", isNoise). + Str("node_key", machine.NodeKey). Str("machine", machine.Hostname). Err(err). Msg("Failed to get Map response") @@ -163,7 +92,7 @@ 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). + Bool("noise", isNoise). Str("machine", machine.Hostname). Bool("readOnly", mapRequest.ReadOnly). Bool("omitPeers", mapRequest.OmitPeers). @@ -173,12 +102,13 @@ func (h *Headscale) PollNetMapHandler( 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") 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,20 +116,24 @@ func (h *Headscale) PollNetMapHandler( Msg("Failed to write response") } + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + 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) + h.setLastStateChangeToNow() // 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", "PollNetMap"). - Str("id", machineKeyStr). + Caller(). + Bool("noise", isNoise). Str("machine", machine.Hostname). Msg("Loading or creating update channel") @@ -214,11 +148,12 @@ func (h *Headscale) PollNetMapHandler( 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") writer.WriteHeader(http.StatusOK) - _, err := writer.Write(data) + _, err := writer.Write(mapResp) if err != nil { log.Error(). Caller(). @@ -235,6 +170,7 @@ func (h *Headscale) PollNetMapHandler( } 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) @@ -244,51 +180,54 @@ func (h *Headscale) PollNetMapHandler( 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 <- data + 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"). Inc() updateChan <- struct{}{} - h.PollNetMapStream( + h.pollNetMapStream( writer, req, machine, mapRequest, - machineKey, pollDataChan, keepAliveChan, updateChan, + isNoise, ) + log.Trace(). Str("handler", "PollNetMap"). - Str("id", machineKeyStr). + Bool("noise", isNoise). 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,18 +241,20 @@ func (h *Headscale) PollNetMapStream( ctx, updateChan, keepAliveChan, - machineKey, mapRequest, machine, + isNoise, ) 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) @@ -322,6 +263,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)). @@ -330,6 +272,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). @@ -343,6 +286,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") @@ -352,6 +296,7 @@ func (h *Headscale) PollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Int("bytes", len(data)). @@ -363,6 +308,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). @@ -383,6 +329,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). @@ -393,6 +340,7 @@ func (h *Headscale) PollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "pollData"). Int("bytes", len(data)). @@ -409,6 +357,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). @@ -421,6 +370,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") @@ -430,6 +380,7 @@ func (h *Headscale) PollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Int("bytes", len(data)). @@ -441,6 +392,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). @@ -456,6 +408,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). @@ -466,6 +419,7 @@ func (h *Headscale) PollNetMapStream( log.Trace(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "keepAlive"). Int("bytes", len(data)). @@ -474,6 +428,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") @@ -487,14 +442,16 @@ 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)). Msgf("There has been updates since the last successful update to %s", machine.Hostname) - data, err := h.getMapResponse(machineKey, mapRequest, machine) + data, err := h.getMapResponseData(mapRequest, machine, false) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). + Bool("noise", isNoise). Str("machine", machine.Hostname). Str("channel", "update"). Err(err). @@ -506,6 +463,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). @@ -521,6 +479,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") @@ -530,6 +489,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") @@ -547,6 +507,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). @@ -566,6 +527,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). @@ -580,6 +542,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)). @@ -598,6 +561,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). @@ -613,6 +577,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). @@ -625,6 +590,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") @@ -637,9 +603,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,10 +627,11 @@ 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"). + Bool("noise", isNoise). Err(err). Msg("Error generating the keep alive msg") @@ -674,6 +641,7 @@ func (h *Headscale) scheduledPollWorker( log.Debug(). Str("func", "keepAlive"). Str("machine", machine.Hostname). + Bool("noise", isNoise). Msg("Sending keepalive") keepAliveChan <- data @@ -681,6 +649,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() diff --git a/protocol_common_utils.go b/protocol_common_utils.go new file mode 100644 index 00000000..3dc435f3 --- /dev/null +++ b/protocol_common_utils.go @@ -0,0 +1,120 @@ +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) getMapResponseData( + mapRequest tailcfg.MapRequest, + machine *Machine, + isNoise bool, +) ([]byte, error) { + mapResponse, err := h.generateMapResponse(mapRequest, machine) + if err != nil { + return nil, err + } + + if isNoise { + return h.marshalMapResponse(mapResponse, key.MachinePublic{}, mapRequest.Compress) + } + + 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.marshalMapResponse(mapResponse, machineKey, mapRequest.Compress) +} + +func (h *Headscale) getMapKeepAliveResponseData( + mapRequest tailcfg.MapRequest, + machine *Machine, + isNoise bool, +) ([]byte, error) { + keepAliveResponse := tailcfg.MapResponse{ + KeepAlive: true, + } + + if isNoise { + return h.marshalMapResponse(keepAliveResponse, key.MachinePublic{}, mapRequest.Compress) + } + + 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.marshalMapResponse(keepAliveResponse, machineKey, mapRequest.Compress) +} + +func (h *Headscale) marshalResponse( + resp interface{}, + 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 { + log.Error(). + Caller(). + Err(err). + Msg("Cannot marshal map response") + } + + var respBody []byte + if compression == ZstdCompression { + encoder, _ := zstd.NewWriter(nil) + respBody = encoder.EncodeAll(jsonBody, nil) + if !machineKey.IsZero() { // if legacy protocol + respBody = h.privateKey.SealTo(machineKey, respBody) + } + } else { + if !machineKey.IsZero() { // if legacy protocol + respBody = h.privateKey.SealTo(machineKey, jsonBody) + } else { + respBody = jsonBody + } + } + + data := make([]byte, reservedResponseHeaderSize) + binary.LittleEndian.PutUint32(data, uint32(len(respBody))) + data = append(data, respBody...) + + return data, nil +} diff --git a/protocol_legacy.go b/protocol_legacy.go new file mode 100644 index 00000000..4e75d127 --- /dev/null +++ b/protocol_legacy.go @@ -0,0 +1,58 @@ +package headscale + +import ( + "io" + "net/http" + + "github.com/gorilla/mux" + "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(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 + } + + h.handleRegisterCommon(writer, req, registerRequest, machineKey) +} diff --git a/protocol_legacy_poll.go b/protocol_legacy_poll.go new file mode 100644 index 00000000..f7ef6548 --- /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("A machine is entering polling via the legacy protocol") + + h.handlePollCommon(writer, req, machine, mapRequest, false) +} diff --git a/protocol_noise.go b/protocol_noise.go new file mode 100644 index 00000000..46f7a03b --- /dev/null +++ b/protocol_noise.go @@ -0,0 +1,38 @@ +package headscale + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// // 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 + } + + h.handleRegisterCommon(writer, req, registerRequest, key.MachinePublic{}) +} diff --git a/protocol_noise_poll.go b/protocol_noise_poll.go new file mode 100644 index 00000000..8498dcfb --- /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("A machine is entering polling via the Noise protocol") + + h.handlePollCommon(writer, req, machine, mapRequest, true) +} diff --git a/utils.go b/utils.go index b4362535..e7fb13aa 100644 --- a/utils.go +++ b/utils.go @@ -59,6 +59,8 @@ const ( privateHexPrefix = "privkey:" PermissionFallback = 0o700 + + ZstdCompression = "zstd" ) func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string { @@ -116,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 { @@ -130,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