diff --git a/.golangci.yaml b/.golangci.yaml index 8edc1b5a..9b1e2389 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -29,7 +29,6 @@ linters: - testpackage - stylecheck - wrapcheck - - gomnd - goerr113 - errorlint - forcetypeassert diff --git a/acls.go b/acls.go index a5e54400..994b8963 100644 --- a/acls.go +++ b/acls.go @@ -24,6 +24,14 @@ const ( errorInvalidPortFormat = Error("invalid port format") ) +const ( + PORT_RANGE_BEGIN = 0 + PORT_RANGE_END = 65535 + BASE_10 = 10 + BIT_SIZE_16 = 16 + EXPECTED_TOKEN_ITEMS = 2 +) + // LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules. func (h *Headscale) LoadACLPolicy(path string) error { policyFile, err := os.Open(path) @@ -114,7 +122,7 @@ func (h *Headscale) generateACLPolicyDestPorts( d string, ) ([]tailcfg.NetPortRange, error) { tokens := strings.Split(d, ":") - if len(tokens) < 2 || len(tokens) > 3 { + if len(tokens) < EXPECTED_TOKEN_ITEMS || len(tokens) > 3 { return nil, errorInvalidPortFormat } @@ -125,7 +133,7 @@ func (h *Headscale) generateACLPolicyDestPorts( // tag:montreal-webserver:80,443 // tag:api-server:443 // example-host-1:* - if len(tokens) == 2 { + if len(tokens) == EXPECTED_TOKEN_ITEMS { alias = tokens[0] } else { alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) @@ -248,14 +256,16 @@ func (h *Headscale) expandAlias(s string) ([]string, error) { func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) { if s == "*" { - return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil + return &[]tailcfg.PortRange{ + {First: PORT_RANGE_BEGIN, Last: PORT_RANGE_END}, + }, nil } ports := []tailcfg.PortRange{} for _, p := range strings.Split(s, ",") { rang := strings.Split(p, "-") if len(rang) == 1 { - pi, err := strconv.ParseUint(rang[0], 10, 16) + pi, err := strconv.ParseUint(rang[0], BASE_10, BIT_SIZE_16) if err != nil { return nil, err } @@ -263,12 +273,12 @@ func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) { First: uint16(pi), Last: uint16(pi), }) - } else if len(rang) == 2 { - start, err := strconv.ParseUint(rang[0], 10, 16) + } else if len(rang) == EXPECTED_TOKEN_ITEMS { + start, err := strconv.ParseUint(rang[0], BASE_10, BIT_SIZE_16) if err != nil { return nil, err } - last, err := strconv.ParseUint(rang[1], 10, 16) + last, err := strconv.ParseUint(rang[1], BASE_10, BIT_SIZE_16) if err != nil { return nil, err } diff --git a/api.go b/api.go index d8d4b512..211b4868 100644 --- a/api.go +++ b/api.go @@ -18,10 +18,12 @@ import ( "tailscale.com/types/wgkey" ) +const RESERVED_RESPONSE_HEADER_SIZE = 4 + // KeyHandler provides the Headscale pub key // Listens in /key. func (h *Headscale) KeyHandler(c *gin.Context) { - c.Data(200, "text/plain; charset=utf-8", []byte(h.publicKey.HexString())) + c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(h.publicKey.HexString())) } // RegisterWebAPI shows a simple message in the browser to point to the CLI @@ -139,7 +141,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { return } - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) return } @@ -170,7 +172,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } machineRegistrations.WithLabelValues("update", "web", "success", m.Namespace.Name). Inc() - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) return } @@ -213,7 +215,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } machineRegistrations.WithLabelValues("new", "web", "success", m.Namespace.Name). Inc() - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) return } @@ -239,7 +241,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { return } - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) return } @@ -275,7 +277,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { return } - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) } func (h *Headscale) getMapResponse( @@ -360,7 +362,7 @@ func (h *Headscale) getMapResponse( } } // declare the incoming size on the first 4 bytes - data := make([]byte, 4) + data := make([]byte, RESERVED_RESPONSE_HEADER_SIZE) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) data = append(data, respBody...) @@ -390,7 +392,7 @@ func (h *Headscale) getMapKeepAliveResponse( return nil, err } } - data := make([]byte, 4) + data := make([]byte, RESERVED_RESPONSE_HEADER_SIZE) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) data = append(data, respBody...) @@ -430,7 +432,7 @@ func (h *Headscale) handleAuthKey( return } - c.Data(401, "application/json; charset=utf-8", respBody) + c.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody) log.Error(). Str("func", "handleAuthKey"). Str("machine", m.Name). @@ -490,7 +492,7 @@ func (h *Headscale) handleAuthKey( } machineRegistrations.WithLabelValues("new", "authkey", "success", m.Namespace.Name). Inc() - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(http.StatusOK, "application/json; charset=utf-8", respBody) log.Info(). Str("func", "handleAuthKey"). Str("machine", m.Name). diff --git a/app.go b/app.go index 9c66ca21..5b732d56 100644 --- a/app.go +++ b/app.go @@ -47,9 +47,11 @@ import ( ) const ( - AUTH_PREFIX = "Bearer " - POSTGRESQL = "postgresql" - SQLITE = "sqlite3" + AUTH_PREFIX = "Bearer " + POSTGRESQL = "postgresql" + SQLITE = "sqlite3" + UPDATE_RATE_MILLISECONDS = 5000 + HTTP_READ_TIMEOUT = 30 * time.Second ) // Config contains the initial Headscale configuration. @@ -507,14 +509,13 @@ func (h *Headscale) Serve() error { } // I HATE THIS - updateMillisecondsWait := int64(5000) - go h.watchForKVUpdates(updateMillisecondsWait) - go h.expireEphemeralNodes(updateMillisecondsWait) + go h.watchForKVUpdates(UPDATE_RATE_MILLISECONDS) + go h.expireEphemeralNodes(UPDATE_RATE_MILLISECONDS) httpServer := &http.Server{ Addr: h.cfg.Addr, Handler: r, - ReadTimeout: 30 * time.Second, + ReadTimeout: HTTP_READ_TIMEOUT, // Go does not handle timeouts in HTTP very well, and there is // no good way to handle streaming timeouts, therefore we need to // keep this at unlimited and be careful to clean up connections diff --git a/cmd/headscale/cli/namespaces.go b/cmd/headscale/cli/namespaces.go index 9e1be07f..8c69ac56 100644 --- a/cmd/headscale/cli/namespaces.go +++ b/cmd/headscale/cli/namespaces.go @@ -195,7 +195,8 @@ var renameNamespaceCmd = &cobra.Command{ Use: "rename OLD_NAME NEW_NAME", Short: "Renames a namespace", Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 2 { + expectedArguments := 2 + if len(args) < expectedArguments { return fmt.Errorf("Missing parameters") } diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 8398bd8c..83d6b1f5 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -7,6 +7,7 @@ import ( "time" survey "github.com/AlecAivazis/survey/v2" + "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -450,7 +451,7 @@ func nodesToPtables( d = append( d, []string{ - strconv.FormatUint(machine.Id, 10), + strconv.FormatUint(machine.Id, headscale.BASE_10), machine.Name, nodeKey.ShortString(), namespace, diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 55df5aa2..13a30942 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -12,6 +12,10 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +const ( + DEFAULT_PRE_AUTH_KEY_EXPIRY = 24 * time.Hour +) + func init() { rootCmd.AddCommand(preauthkeysCmd) preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace") @@ -27,7 +31,7 @@ func init() { createPreAuthKeyCmd.PersistentFlags(). Bool("ephemeral", false, "Preauthkey for ephemeral nodes") createPreAuthKeyCmd.Flags(). - DurationP("expiration", "e", 24*time.Hour, "Human-readable expiration of the key (30m, 24h, 365d...)") + DurationP("expiration", "e", DEFAULT_PRE_AUTH_KEY_EXPIRY, "Human-readable expiration of the key (30m, 24h, 365d...)") } var preauthkeysCmd = &cobra.Command{ diff --git a/derp.go b/derp.go index a2481acf..cf128a88 100644 --- a/derp.go +++ b/derp.go @@ -32,7 +32,7 @@ func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) { } func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), HTTP_READ_TIMEOUT) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil) @@ -41,7 +41,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) { } client := http.Client{ - Timeout: 10 * time.Second, + Timeout: HTTP_READ_TIMEOUT, } resp, err := client.Do(req) diff --git a/dns.go b/dns.go index d50fae63..d7480b15 100644 --- a/dns.go +++ b/dns.go @@ -10,6 +10,10 @@ import ( "tailscale.com/util/dnsname" ) +const ( + BYTE_SIZE = 8 +) + // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // server (listening in 100.100.100.100 udp/53) should be used for. @@ -43,10 +47,10 @@ func generateMagicDNSRootDomains( maskBits, _ := netRange.Mask.Size() // lastOctet is the last IP byte covered by the mask - lastOctet := maskBits / 8 + lastOctet := maskBits / BYTE_SIZE // wildcardBits is the number of bits not under the mask in the lastOctet - wildcardBits := 8 - maskBits%8 + wildcardBits := BYTE_SIZE - maskBits%BYTE_SIZE // min is the value in the lastOctet byte of the IP // max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1 diff --git a/machine.go b/machine.go index 6443c828..e8d2f72a 100644 --- a/machine.go +++ b/machine.go @@ -523,7 +523,7 @@ func (m Machine) toNode( n := tailcfg.Node{ ID: tailcfg.NodeID(m.ID), // this is the actual ID StableID: tailcfg.StableNodeID( - strconv.FormatUint(m.ID, 10), + strconv.FormatUint(m.ID, BASE_10), ), // in headscale, unlike tailcontrol server, IDs are permanent Name: hostname, User: tailcfg.UserID(m.NamespaceID), diff --git a/namespaces.go b/namespaces.go index d3ef8242..bea922de 100644 --- a/namespaces.go +++ b/namespaces.go @@ -320,7 +320,7 @@ func getMapResponseUserProfiles(m Machine, peers Machines) []tailcfg.UserProfile func (n *Namespace) toProto() *v1.Namespace { return &v1.Namespace{ - Id: strconv.FormatUint(uint64(n.ID), 10), + Id: strconv.FormatUint(uint64(n.ID), BASE_10), Name: n.Name, CreatedAt: timestamppb.New(n.CreatedAt), } diff --git a/oidc.go b/oidc.go index 234a0bf3..aea89e20 100644 --- a/oidc.go +++ b/oidc.go @@ -17,6 +17,12 @@ import ( "golang.org/x/oauth2" ) +const ( + OIDC_STATE_CACHE_EXPIRATION = time.Minute * 5 + OIDC_STATE_CACHE_CLEANUP_INTERVAL = time.Minute * 10 + RANDOM_BYTE_SIZE = 16 +) + type IDTokenClaims struct { Name string `json:"name,omitempty"` Groups []string `json:"groups,omitempty"` @@ -50,7 +56,10 @@ func (h *Headscale) initOIDC() error { // init the state cache if it hasn't been already if h.oidcStateCache == nil { - h.oidcStateCache = cache.New(time.Minute*5, time.Minute*10) + h.oidcStateCache = cache.New( + OIDC_STATE_CACHE_EXPIRATION, + OIDC_STATE_CACHE_CLEANUP_INTERVAL, + ) } return nil @@ -67,7 +76,7 @@ func (h *Headscale) RegisterOIDC(c *gin.Context) { return } - b := make([]byte, 16) + b := make([]byte, RANDOM_BYTE_SIZE) if _, err := rand.Read(b); err != nil { log.Error().Msg("could not read 16 bytes from rand") c.String(http.StatusInternalServerError, "could not read 16 bytes from rand") @@ -78,7 +87,7 @@ func (h *Headscale) RegisterOIDC(c *gin.Context) { stateStr := hex.EncodeToString(b)[:32] // place the machine key into the state cache, so it can be retrieved later - h.oidcStateCache.Set(stateStr, mKeyStr, time.Minute*5) + h.oidcStateCache.Set(stateStr, mKeyStr, OIDC_STATE_CACHE_EXPIRATION) authUrl := h.oauth2Config.AuthCodeURL(stateStr) log.Debug().Msgf("Redirecting to %s for authentication", authUrl) diff --git a/poll.go b/poll.go index 47717998..3f7b2939 100644 --- a/poll.go +++ b/poll.go @@ -15,6 +15,11 @@ import ( "tailscale.com/types/wgkey" ) +const ( + KEEP_ALIVE_INTERVAL = 60 * time.Second + UPDATE_CHECK_INTERVAL = 10 * time.Second +) + // PollNetMapHandler takes care of /machine/:id/map // // This is the busiest endpoint, as it keeps the HTTP long poll that updates @@ -127,7 +132,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Client is starting up. Probably interested in a DERP map") - c.Data(200, "application/json; charset=utf-8", data) + c.Data(http.StatusOK, "application/json; charset=utf-8", data) return } @@ -159,7 +164,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Client sent endpoint update and is ok with a response without peer list") - c.Data(200, "application/json; charset=utf-8", data) + c.Data(http.StatusOK, "application/json; charset=utf-8", data) // It sounds like we should update the nodes when we have received a endpoint update // even tho the comments in the tailscale code dont explicitly say so. @@ -483,8 +488,8 @@ func (h *Headscale) scheduledPollWorker( req tailcfg.MapRequest, m *Machine, ) { - keepAliveTicker := time.NewTicker(60 * time.Second) - updateCheckerTicker := time.NewTicker(10 * time.Second) + keepAliveTicker := time.NewTicker(KEEP_ALIVE_INTERVAL) + updateCheckerTicker := time.NewTicker(UPDATE_CHECK_INTERVAL) for { select { diff --git a/preauth_keys.go b/preauth_keys.go index a477d985..1a00077a 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -156,7 +156,7 @@ func (h *Headscale) generateKey() (string, error) { func (key *PreAuthKey) toProto() *v1.PreAuthKey { protoKey := v1.PreAuthKey{ Namespace: key.Namespace.Name, - Id: strconv.FormatUint(key.ID, 10), + Id: strconv.FormatUint(key.ID, BASE_10), Key: key.Key, Ephemeral: key.Ephemeral, Reusable: key.Reusable,