mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-29 18:33:05 +00:00
Merge branch 'main' into magic-dns-support
This commit is contained in:
commit
040a18e6f8
9 changed files with 76 additions and 26 deletions
|
@ -19,7 +19,7 @@ builds:
|
||||||
flags:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
- id: linux-armhf
|
- id: linux-armhf
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
@ -39,7 +39,7 @@ builds:
|
||||||
flags:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}}
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
|
|
||||||
|
|
||||||
- id: linux-amd64
|
- id: linux-amd64
|
||||||
|
@ -54,6 +54,8 @@ builds:
|
||||||
- 7
|
- 7
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: golang-cross
|
- id: golang-cross
|
||||||
|
|
15
README.md
15
README.md
|
@ -30,6 +30,17 @@ Headscale implements this coordination server.
|
||||||
- [x] Share nodes between ~~users~~ namespaces
|
- [x] Share nodes between ~~users~~ namespaces
|
||||||
- [ ] MagicDNS / Smart DNS
|
- [ ] 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 🤷
|
## Roadmap 🤷
|
||||||
|
|
||||||
Suggestions/PRs welcomed!
|
Suggestions/PRs welcomed!
|
||||||
|
@ -114,7 +125,7 @@ Suggestions/PRs welcomed!
|
||||||
7. Add your first machine
|
7. Add your first machine
|
||||||
|
|
||||||
```shell
|
```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.
|
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
|
2. Use the authkey from your machine to register it
|
||||||
```shell
|
```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.
|
If you create an authkey with the `--ephemeral` flag, that key will create ephemeral nodes. This implies that `--reusable` is true.
|
||||||
|
|
33
app.go
33
app.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/crypto/acme"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
@ -46,6 +47,9 @@ type Config struct {
|
||||||
TLSCertPath string
|
TLSCertPath string
|
||||||
TLSKeyPath string
|
TLSKeyPath string
|
||||||
|
|
||||||
|
ACMEURL string
|
||||||
|
ACMEEmail string
|
||||||
|
|
||||||
DNSConfig *tailcfg.DNSConfig
|
DNSConfig *tailcfg.DNSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,16 +189,18 @@ func (h *Headscale) Serve() error {
|
||||||
r.GET("/apple/:platform", h.ApplePlatformConfig)
|
r.GET("/apple/:platform", h.ApplePlatformConfig)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
timeout := 30 * time.Second
|
|
||||||
|
|
||||||
go h.watchForKVUpdates(5000)
|
go h.watchForKVUpdates(5000)
|
||||||
go h.expireEphemeralNodes(5000)
|
go h.expireEphemeralNodes(5000)
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Addr: h.cfg.Addr,
|
Addr: h.cfg.Addr,
|
||||||
Handler: r,
|
Handler: r,
|
||||||
ReadTimeout: timeout,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 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
|
||||||
|
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming
|
||||||
|
WriteTimeout: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.cfg.TLSLetsEncryptHostname != "" {
|
if h.cfg.TLSLetsEncryptHostname != "" {
|
||||||
|
@ -206,14 +212,14 @@ func (h *Headscale) Serve() error {
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
|
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
|
||||||
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
|
||||||
|
Client: &acme.Client{
|
||||||
|
DirectoryURL: h.cfg.ACMEURL,
|
||||||
|
},
|
||||||
|
Email: h.cfg.ACMEEmail,
|
||||||
}
|
}
|
||||||
s := &http.Server{
|
|
||||||
Addr: h.cfg.Addr,
|
s.TLSConfig = m.TLSConfig()
|
||||||
TLSConfig: m.TLSConfig(),
|
|
||||||
Handler: r,
|
|
||||||
ReadTimeout: timeout,
|
|
||||||
WriteTimeout: timeout,
|
|
||||||
}
|
|
||||||
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
|
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
|
||||||
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
|
// 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
|
// 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
|
// port 80 for the certificate validation in addition to the headscale
|
||||||
// service, which can be configured to run on any other port.
|
// service, which can be configured to run on any other port.
|
||||||
go func() {
|
go func() {
|
||||||
|
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))).
|
||||||
Msg("failed to set up a HTTP server")
|
Msg("failed to set up a HTTP server")
|
||||||
|
|
|
@ -186,6 +186,9 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
TLSKeyPath: absPath(viper.GetString("tls_key_path")),
|
||||||
|
|
||||||
DNSConfig: dnsConfig,
|
DNSConfig: dnsConfig,
|
||||||
|
|
||||||
|
ACMEEmail: viper.GetString("acme_email"),
|
||||||
|
ACMEURL: viper.GetString("acme_url"),
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := headscale.NewHeadscale(cfg)
|
h, err := headscale.NewHeadscale(cfg)
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
"db_name": "headscale",
|
"db_name": "headscale",
|
||||||
"db_user": "foo",
|
"db_user": "foo",
|
||||||
"db_pass": "bar",
|
"db_pass": "bar",
|
||||||
|
"acme_url": "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
"acme_email": "",
|
||||||
"tls_letsencrypt_hostname": "",
|
"tls_letsencrypt_hostname": "",
|
||||||
"tls_letsencrypt_listen": ":http",
|
"tls_letsencrypt_listen": ":http",
|
||||||
"tls_letsencrypt_cache_dir": ".cache",
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
"ephemeral_node_inactivity_timeout": "30m",
|
"ephemeral_node_inactivity_timeout": "30m",
|
||||||
"db_type": "sqlite3",
|
"db_type": "sqlite3",
|
||||||
"db_path": "db.sqlite",
|
"db_path": "db.sqlite",
|
||||||
|
"acme_url": "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
"acme_email": "",
|
||||||
"tls_letsencrypt_hostname": "",
|
"tls_letsencrypt_hostname": "",
|
||||||
"tls_letsencrypt_listen": ":http",
|
"tls_letsencrypt_listen": ":http",
|
||||||
"tls_letsencrypt_cache_dir": ".cache",
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
|
|
|
@ -433,7 +433,7 @@ func (s *IntegrationTestSuite) TestPingAllPeers() {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale", "ping",
|
"tailscale", "ping",
|
||||||
"--timeout=1s",
|
"--timeout=1s",
|
||||||
"--c=20",
|
"--c=10",
|
||||||
"--until-direct=true",
|
"--until-direct=true",
|
||||||
ip.String(),
|
ip.String(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,7 +317,8 @@ func (h *Headscale) notifyChangesToPeers(m *Machine) {
|
||||||
Str("func", "notifyChangesToPeers").
|
Str("func", "notifyChangesToPeers").
|
||||||
Str("machine", m.Name).
|
Str("machine", m.Name).
|
||||||
Str("peer", p.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().
|
log.Trace().
|
||||||
Str("func", "notifyChangesToPeers").
|
Str("func", "notifyChangesToPeers").
|
||||||
|
@ -388,11 +389,12 @@ func (h *Headscale) sendRequestOnUpdateChannel(m *tailcfg.Node) error {
|
||||||
Msgf("Notified machine %s", m.Name)
|
Msgf("Notified machine %s", m.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
err := errors.New("machine does not have an open update channel")
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("func", "requestUpdate").
|
Str("func", "requestUpdate").
|
||||||
Str("machine", m.Name).
|
Str("machine", m.Name).
|
||||||
Msgf("Machine %s does not appear to be polling", m.Name)
|
Msgf("Machine %s does not have an open update channel", m.Name)
|
||||||
return errors.New("machine does not seem to be polling")
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
31
poll.go
31
poll.go
|
@ -230,6 +230,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("channel", "pollData").
|
Str("channel", "pollData").
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Cannot write data")
|
Msg("Cannot write data")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
|
@ -237,7 +238,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("channel", "pollData").
|
Str("channel", "pollData").
|
||||||
Int("bytes", len(data)).
|
Int("bytes", len(data)).
|
||||||
Msg("Data from pollData channel written successfully")
|
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
|
// when an outdated machine object is kept alive, e.g. db is update from
|
||||||
// command line, but then overwritten.
|
// command line, but then overwritten.
|
||||||
err = h.UpdateMachine(&m)
|
err = h.UpdateMachine(&m)
|
||||||
|
@ -258,7 +259,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("machine", m.Name).
|
Str("machine", m.Name).
|
||||||
Str("channel", "pollData").
|
Str("channel", "pollData").
|
||||||
Int("bytes", len(data)).
|
Int("bytes", len(data)).
|
||||||
Msg("Machine updated successfully after sending pollData")
|
Msg("Machine entry in database updated successfully after sending pollData")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case data := <-keepAliveChan:
|
case data := <-keepAliveChan:
|
||||||
|
@ -276,6 +277,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("channel", "keepAlive").
|
Str("channel", "keepAlive").
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Cannot write keep alive message")
|
Msg("Cannot write keep alive message")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
|
@ -283,7 +285,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("channel", "keepAlive").
|
Str("channel", "keepAlive").
|
||||||
Int("bytes", len(data)).
|
Int("bytes", len(data)).
|
||||||
Msg("Keep alive sent successfully")
|
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
|
// when an outdated machine object is kept alive, e.g. db is update from
|
||||||
// command line, but then overwritten.
|
// command line, but then overwritten.
|
||||||
err = h.UpdateMachine(&m)
|
err = h.UpdateMachine(&m)
|
||||||
|
@ -336,6 +338,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
Str("channel", "update").
|
Str("channel", "update").
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Could not write the map response")
|
Msg("Could not write the map response")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("handler", "PollNetMapStream").
|
Str("handler", "PollNetMapStream").
|
||||||
|
@ -347,7 +350,7 @@ func (h *Headscale) PollNetMapStream(
|
||||||
// we sometimes end in a state were the update
|
// we sometimes end in a state were the update
|
||||||
// is not picked up by a client and we use this
|
// is not picked up by a client and we use this
|
||||||
// to determine if we should "force" an update.
|
// 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
|
// when an outdated machine object is kept alive, e.g. db is update from
|
||||||
// command line, but then overwritten.
|
// command line, but then overwritten.
|
||||||
err = h.UpdateMachine(&m)
|
err = h.UpdateMachine(&m)
|
||||||
|
@ -393,13 +396,33 @@ func (h *Headscale) PollNetMapStream(
|
||||||
m.LastSeen = &now
|
m.LastSeen = &now
|
||||||
h.db.Save(&m)
|
h.db.Save(&m)
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Str("handler", "PollNetMapStream").
|
||||||
|
Str("machine", m.Name).
|
||||||
|
Str("channel", "Done").
|
||||||
|
Msg("Cancelling keepAlive channel")
|
||||||
cancelKeepAlive <- struct{}{}
|
cancelKeepAlive <- struct{}{}
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Str("handler", "PollNetMapStream").
|
||||||
|
Str("machine", m.Name).
|
||||||
|
Str("channel", "Done").
|
||||||
|
Msg("Closing update channel")
|
||||||
h.closeUpdateChannel(&m)
|
h.closeUpdateChannel(&m)
|
||||||
|
|
||||||
close(pollDataChan)
|
close(pollDataChan)
|
||||||
|
log.Trace().
|
||||||
|
Str("handler", "PollNetMapStream").
|
||||||
|
Str("machine", m.Name).
|
||||||
|
Str("channel", "Done").
|
||||||
|
Msg("Closing pollData channel")
|
||||||
|
|
||||||
close(keepAliveChan)
|
close(keepAliveChan)
|
||||||
|
log.Trace().
|
||||||
|
Str("handler", "PollNetMapStream").
|
||||||
|
Str("machine", m.Name).
|
||||||
|
Str("channel", "Done").
|
||||||
|
Msg("Closing keepAliveChan channel")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue