diff --git a/.goreleaser.yml b/.goreleaser.yml index fd25563c..7b1ea605 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -19,7 +19,7 @@ builds: flags: - -mod=readonly ldflags: - - -s -w -X main.version={{.Version}} + - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - id: linux-armhf main: ./cmd/headscale/headscale.go mod_timestamp: '{{ .CommitTimestamp }}' @@ -39,7 +39,7 @@ builds: flags: - -mod=readonly ldflags: - - -s -w -X main.version={{.Version}} + - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} - id: linux-amd64 @@ -54,6 +54,8 @@ builds: - 7 main: ./cmd/headscale/headscale.go mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} archives: - id: golang-cross diff --git a/README.md b/README.md index 8dd9c8d6..5f691a6c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,17 @@ Headscale implements this coordination server. - [x] Share nodes between ~~users~~ namespaces - [ ] MagicDNS / Smart DNS +## Client OS support + +| OS | Supports headscale | +| --- | --- | +| Linux | Yes | +| OpenBSD | Yes | +| macOS | Yes (see `/apple` on your headscale for more information) | +| Windows | Yes | +| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) | +| iOS | Not yet | + ## Roadmap 🤷 Suggestions/PRs welcomed! @@ -114,7 +125,7 @@ Suggestions/PRs welcomed! 7. Add your first machine ```shell - tailscale up -login-server YOUR_HEADSCALE_URL + tailscale up --login-server YOUR_HEADSCALE_URL ``` 8. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key. @@ -154,7 +165,7 @@ Alternatively, you can use Auth Keys to register your machines: 2. Use the authkey from your machine to register it ```shell - tailscale up -login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY + tailscale up --login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY ``` If you create an authkey with the `--ephemeral` flag, that key will create ephemeral nodes. This implies that `--reusable` is true. diff --git a/app.go b/app.go index c8c799b1..44c445e4 100644 --- a/app.go +++ b/app.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/gin-gonic/gin" + "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" "inet.af/netaddr" @@ -46,6 +47,9 @@ type Config struct { TLSCertPath string TLSKeyPath string + ACMEURL string + ACMEEmail string + DNSConfig *tailcfg.DNSConfig } @@ -185,16 +189,18 @@ func (h *Headscale) Serve() error { r.GET("/apple/:platform", h.ApplePlatformConfig) var err error - timeout := 30 * time.Second - go h.watchForKVUpdates(5000) go h.expireEphemeralNodes(5000) s := &http.Server{ - Addr: h.cfg.Addr, - Handler: r, - ReadTimeout: timeout, - WriteTimeout: timeout, + Addr: h.cfg.Addr, + Handler: r, + ReadTimeout: 30 * time.Second, + // 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 + // https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming + WriteTimeout: 0, } if h.cfg.TLSLetsEncryptHostname != "" { @@ -206,14 +212,14 @@ func (h *Headscale) Serve() error { Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname), Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir), + Client: &acme.Client{ + DirectoryURL: h.cfg.ACMEURL, + }, + Email: h.cfg.ACMEEmail, } - s := &http.Server{ - Addr: h.cfg.Addr, - TLSConfig: m.TLSConfig(), - Handler: r, - ReadTimeout: timeout, - WriteTimeout: timeout, - } + + s.TLSConfig = m.TLSConfig() + if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" { // Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) // The RFC requires that the validation is done on port 443; in other words, headscale @@ -224,7 +230,6 @@ func (h *Headscale) Serve() error { // port 80 for the certificate validation in addition to the headscale // service, which can be configured to run on any other port. go func() { - log.Fatal(). Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))). Msg("failed to set up a HTTP server") diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 8f2fbdf1..9aa63525 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -186,6 +186,9 @@ func getHeadscaleApp() (*headscale.Headscale, error) { TLSKeyPath: absPath(viper.GetString("tls_key_path")), DNSConfig: dnsConfig, + + ACMEEmail: viper.GetString("acme_email"), + ACMEURL: viper.GetString("acme_url"), } h, err := headscale.NewHeadscale(cfg) diff --git a/config.json.postgres.example b/config.json.postgres.example index 5e5c5395..9b6f737f 100644 --- a/config.json.postgres.example +++ b/config.json.postgres.example @@ -10,6 +10,8 @@ "db_name": "headscale", "db_user": "foo", "db_pass": "bar", + "acme_url": "https://acme-v02.api.letsencrypt.org/directory", + "acme_email": "", "tls_letsencrypt_hostname": "", "tls_letsencrypt_listen": ":http", "tls_letsencrypt_cache_dir": ".cache", diff --git a/config.json.sqlite.example b/config.json.sqlite.example index a357bc85..74e15902 100644 --- a/config.json.sqlite.example +++ b/config.json.sqlite.example @@ -6,6 +6,8 @@ "ephemeral_node_inactivity_timeout": "30m", "db_type": "sqlite3", "db_path": "db.sqlite", + "acme_url": "https://acme-v02.api.letsencrypt.org/directory", + "acme_email": "", "tls_letsencrypt_hostname": "", "tls_letsencrypt_listen": ":http", "tls_letsencrypt_cache_dir": ".cache", diff --git a/integration_test.go b/integration_test.go index 00cff388..01441591 100644 --- a/integration_test.go +++ b/integration_test.go @@ -433,7 +433,7 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { command := []string{ "tailscale", "ping", "--timeout=1s", - "--c=20", + "--c=10", "--until-direct=true", ip.String(), } diff --git a/machine.go b/machine.go index 3b33e82c..ca0e5faa 100644 --- a/machine.go +++ b/machine.go @@ -317,7 +317,8 @@ func (h *Headscale) notifyChangesToPeers(m *Machine) { Str("func", "notifyChangesToPeers"). Str("machine", m.Name). Str("peer", p.Name). - Msgf("Peer %s does not appear to be polling", p.Name) + Msgf("Peer %s does not have an open update client, skipping.", p.Name) + continue } log.Trace(). Str("func", "notifyChangesToPeers"). @@ -388,11 +389,12 @@ func (h *Headscale) sendRequestOnUpdateChannel(m *tailcfg.Node) error { Msgf("Notified machine %s", m.Name) } } else { + err := errors.New("machine does not have an open update channel") log.Info(). Str("func", "requestUpdate"). Str("machine", m.Name). - Msgf("Machine %s does not appear to be polling", m.Name) - return errors.New("machine does not seem to be polling") + Msgf("Machine %s does not have an open update channel", m.Name) + return err } return nil } diff --git a/poll.go b/poll.go index 40b3e28a..f128fab1 100644 --- a/poll.go +++ b/poll.go @@ -230,6 +230,7 @@ func (h *Headscale) PollNetMapStream( Str("channel", "pollData"). Err(err). Msg("Cannot write data") + return false } log.Trace(). Str("handler", "PollNetMapStream"). @@ -237,7 +238,7 @@ func (h *Headscale) PollNetMapStream( Str("channel", "pollData"). Int("bytes", len(data)). Msg("Data from pollData channel written successfully") - // TODO: Abstract away all the database calls, this can cause race conditions + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. err = h.UpdateMachine(&m) @@ -258,7 +259,7 @@ func (h *Headscale) PollNetMapStream( Str("machine", m.Name). Str("channel", "pollData"). Int("bytes", len(data)). - Msg("Machine updated successfully after sending pollData") + Msg("Machine entry in database updated successfully after sending pollData") return true case data := <-keepAliveChan: @@ -276,6 +277,7 @@ func (h *Headscale) PollNetMapStream( Str("channel", "keepAlive"). Err(err). Msg("Cannot write keep alive message") + return false } log.Trace(). Str("handler", "PollNetMapStream"). @@ -283,7 +285,7 @@ func (h *Headscale) PollNetMapStream( Str("channel", "keepAlive"). Int("bytes", len(data)). Msg("Keep alive sent successfully") - // TODO: Abstract away all the database calls, this can cause race conditions + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. err = h.UpdateMachine(&m) @@ -336,6 +338,7 @@ func (h *Headscale) PollNetMapStream( Str("channel", "update"). Err(err). Msg("Could not write the map response") + return false } log.Trace(). Str("handler", "PollNetMapStream"). @@ -347,7 +350,7 @@ func (h *Headscale) PollNetMapStream( // we sometimes end in a state were the update // is not picked up by a client and we use this // to determine if we should "force" an update. - // TODO: Abstract away all the database calls, this can cause race conditions + // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. err = h.UpdateMachine(&m) @@ -393,13 +396,33 @@ func (h *Headscale) PollNetMapStream( m.LastSeen = &now h.db.Save(&m) + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "Done"). + Msg("Cancelling keepAlive channel") cancelKeepAlive <- struct{}{} + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "Done"). + Msg("Closing update channel") h.closeUpdateChannel(&m) close(pollDataChan) + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "Done"). + Msg("Closing pollData channel") close(keepAliveChan) + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "Done"). + Msg("Closing keepAliveChan channel") return false }