diff --git a/README.md b/README.md index 1047ff3a..35f4c184 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,18 @@ Headscale implements this coordination server. - [x] Namespace support (~equivalent to multi-user in Tailscale.com) - [x] Routing (advertise & accept, including exit nodes) - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support) -- [X] JSON-formatted output -- [X] ACLs -- [X] Taildrop (File Sharing) -- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) -- [X] DNS (passing DNS servers to nodes) -- [X] Share nodes between ~~users~~ namespaces +- [x] JSON-formatted output +- [x] ACLs +- [x] Taildrop (File Sharing) +- [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) +- [x] DNS (passing DNS servers to nodes) +- [x] Share nodes between ~~users~~ namespaces - [ ] MagicDNS / Smart DNS - ## Roadmap 🤷 Suggestions/PRs welcomed! - - ## Running it 1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH or use the docker container @@ -44,109 +41,125 @@ Suggestions/PRs welcomed! ```shell docker pull headscale/headscale:x.x.x ``` - + + 2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running - ```shell - docker run --name headscale -e POSTGRES_DB=headscale -e \ - POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres - ``` + ```shell + docker run --name headscale -e POSTGRES_DB=headscale -e \ + POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres + ``` 3. Set some stuff up (headscale Wireguard keys & the config.json file) - ```shell - wg genkey > private.key - wg pubkey < private.key > public.key # not needed - # Postgres - cp config.json.postgres.example config.json - # or - # SQLite - cp config.json.sqlite.example config.json - ``` + ```shell + wg genkey > private.key + wg pubkey < private.key > public.key # not needed + + # Postgres + cp config.json.postgres.example config.json + # or + # SQLite + cp config.json.sqlite.example config.json + ``` 4. Create a namespace (a namespace is a 'tailnet', a group of Tailscale nodes that can talk to each other) - ```shell - headscale namespaces create myfirstnamespace - ``` - or docker: - the db.sqlite mount is only needed if you use sqlite - ```shell - touch db.sqlite - docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8000:8000 headscale/headscale:x.x.x headscale namespaces create myfirstnamespace - ``` - or if your server is already running in docker: - ```shell - docker exec headscale create myfirstnamespace - ``` + ```shell + headscale namespaces create myfirstnamespace + ``` + + or docker: + + the db.sqlite mount is only needed if you use sqlite + + ```shell + touch db.sqlite + docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8000:8000 headscale/headscale:x.x.x headscale namespaces create myfirstnamespace + ``` + + or if your server is already running in docker: + + ```shell + docker exec headscale create myfirstnamespace + ``` 5. Run the server - ```shell - headscale serve - ``` - or docker: - the db.sqlite mount is only needed if you use sqlite - ```shell - docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8000:8000 headscale/headscale:x.x.x headscale serve - ``` + ```shell + headscale serve + ``` + + or docker: + + the db.sqlite mount is only needed if you use sqlite + + ```shell + docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite -p 127.0.0.1:8000:8000 headscale/headscale:x.x.x headscale serve + ``` 6. If you used tailscale.com before in your nodes, make sure you clear the tailscald data folder - ```shell - systemctl stop tailscaled - rm -fr /var/lib/tailscale - systemctl start tailscaled - ``` + + ```shell + systemctl stop tailscaled + rm -fr /var/lib/tailscale + systemctl start tailscaled + ``` 7. Add your first machine - ```shell - tailscale up -login-server YOUR_HEADSCALE_URL - ``` + + ```shell + 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. 9. In the server, register your machine to a namespace with the CLI - ```shell - headscale -n myfirstnamespace node register YOURMACHINEKEY - ``` - or docker: - ```shell - docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml headscale/headscale:x.x.x headscale -n myfirstnamespace node register YOURMACHINEKEY - ``` - or if your server is already running in docker: - ```shell - docker exec headscale -n myfistnamespace node register YOURMACHINEKEY - ``` + ```shell + headscale -n myfirstnamespace node register YOURMACHINEKEY + ``` + or docker: + ```shell + docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derp.yaml:/derp.yaml headscale/headscale:x.x.x headscale -n myfirstnamespace node register YOURMACHINEKEY + ``` + or if your server is already running in docker: + ```shell + docker exec headscale -n myfistnamespace node register YOURMACHINEKEY + ``` Alternatively, you can use Auth Keys to register your machines: 1. Create an authkey - ```shell - headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h - ``` - or docker: - ```shell - docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v$(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite headscale/headscale:x.x.x headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h - ``` - or if your server is already running in docker: - ```shell - docker exec headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h - ``` + + ```shell + headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h + ``` + + or docker: + + ```shell + docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v$(pwd)/derp.yaml:/derp.yaml -v $(pwd)/db.sqlite:/db.sqlite headscale/headscale:x.x.x headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h + ``` + + or if your server is already running in docker: + + ```shell + docker exec headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h + ``` 2. Use the authkey from your machine to register it - ```shell - tailscale up -login-server YOUR_HEADSCALE_URL --authkey YOURAUTHKEY - ``` + ```shell + 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. -Please bear in mind that all the commands from headscale support adding `-o json` or `-o json-line` to get a nicely JSON-formatted output. - +Please bear in mind that all the commands from headscale support adding `-o json` or `-o json-line` to get a nicely JSON-formatted output. ## Configuration reference @@ -163,6 +176,7 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal ``` "log_level": "debug" ``` + `log_level` can be used to set the Log level for Headscale, it defaults to `debug`, and the available levels are: `trace`, `debug`, `info`, `warn` and `error`. ``` @@ -193,7 +207,6 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal The fields starting with `db_` are used for the PostgreSQL connection information. - ### Running the service via TLS (optional) ``` @@ -227,21 +240,21 @@ Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment. For instance, instead of referring to users when defining groups you must - use namespaces (which are the equivalent to user/logins in Tailscale.com). +use namespaces (which are the equivalent to user/logins in Tailscale.com). Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples. +### Apple devices + +An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance. ## Disclaimer 1. We have nothing to do with Tailscale, or Tailscale Inc. 2. The purpose of writing this was to learn how Tailscale works. - - ## More on Tailscale - https://tailscale.com/blog/how-tailscale-works/ - https://tailscale.com/blog/tailscale-key-management/ - https://tailscale.com/blog/an-unlikely-database-migration/ - diff --git a/app.go b/app.go index 81871a87..6f24c82b 100644 --- a/app.go +++ b/app.go @@ -174,6 +174,9 @@ func (h *Headscale) Serve() error { r.POST("/machine/:id", h.RegistrationHandler) r.GET("/oidc/register/:mKey", h.RegisterOIDC) r.GET("/oidc/callback", h.OIDCCallback) + r.GET("/apple", h.AppleMobileConfig) + r.GET("/apple/:platform", h.ApplePlatformConfig) + var err error timeout := 30 * time.Second diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go new file mode 100644 index 00000000..f3956e3c --- /dev/null +++ b/apple_mobileconfig.go @@ -0,0 +1,226 @@ +package headscale + +import ( + "bytes" + "net/http" + "text/template" + + "github.com/rs/zerolog/log" + + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" +) + +// AppleMobileConfig shows a simple message in the browser to point to the CLI +// Listens in /register +func (h *Headscale) AppleMobileConfig(c *gin.Context) { + t := template.Must(template.New("apple").Parse(` + + +

Apple configuration profiles

+

+ This page provides configuration profiles for the official Tailscale clients for iOS and macOS. +

+

+ The profiles will configure Tailscale.app to use {{.Url}} as its control server. +

+ +

Caution

+

You should always inspect the profile before installing it:

+ +

curl {{.Url}}/apple/macos

+ +

Profiles

+ + + +

macOS

+

Headscale can be set to the default server by installing a Headscale configuration profile:

+

+ macOS profile +

+ +
    +
  1. Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed
  2. +
  3. Open System Preferences and go to "Profiles"
  4. +
  5. Find and install the Headscale profile
  6. +
  7. Restart Tailscale.app and log in
  8. +
+ +

Or

+

Use your terminal to configure the default setting for Tailscale by issuing:

+ defaults write io.tailscale.ipn.macos ControlURL {{.Url}} + +

Restart Tailscale.app and log in.

+ + +`)) + + config := map[string]interface{}{ + "Url": h.cfg.ServerURL, + } + + var payload bytes.Buffer + if err := t.Execute(&payload, config); err != nil { + log.Error(). + Str("handler", "AppleMobileConfig"). + Err(err). + Msg("Could not render Apple index template") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple index template")) + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes()) +} + +func (h *Headscale) ApplePlatformConfig(c *gin.Context) { + platform := c.Param("platform") + + id, err := uuid.NewV4() + if err != nil { + log.Error(). + Str("handler", "ApplePlatformConfig"). + Err(err). + Msg("Failed not create UUID") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID")) + return + } + + contentId, err := uuid.NewV4() + if err != nil { + log.Error(). + Str("handler", "ApplePlatformConfig"). + Err(err). + Msg("Failed not create UUID") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID")) + return + } + + platformConfig := AppleMobilePlatformConfig{ + UUID: contentId, + Url: h.cfg.ServerURL, + } + + var payload bytes.Buffer + + switch platform { + case "macos": + if err := macosTemplate.Execute(&payload, platformConfig); err != nil { + log.Error(). + Str("handler", "ApplePlatformConfig"). + Err(err). + Msg("Could not render Apple macOS template") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple macOS template")) + return + } + case "ios": + if err := iosTemplate.Execute(&payload, platformConfig); err != nil { + log.Error(). + Str("handler", "ApplePlatformConfig"). + Err(err). + Msg("Could not render Apple iOS template") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple iOS template")) + return + } + default: + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("Invalid platform, only ios and macos is supported")) + return + } + + config := AppleMobileConfig{ + UUID: id, + Url: h.cfg.ServerURL, + Payload: payload.String(), + } + + var content bytes.Buffer + if err := commonTemplate.Execute(&content, config); err != nil { + log.Error(). + Str("handler", "ApplePlatformConfig"). + Err(err). + Msg("Could not render Apple platform template") + c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple platform template")) + return + } + + c.Data(http.StatusOK, "application/x-apple-aspen-config; charset=utf-8", content.Bytes()) +} + +type AppleMobileConfig struct { + UUID uuid.UUID + Url string + Payload string +} + +type AppleMobilePlatformConfig struct { + UUID uuid.UUID + Url string +} + +var commonTemplate = template.Must(template.New("mobileconfig").Parse(` + + + + PayloadUUID + {{.UUID}} + PayloadDisplayName + Headscale + PayloadDescription + Configure Tailscale login server to: {{.Url}} + PayloadIdentifier + com.github.juanfont.headscale + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadVersion + 1 + PayloadContent + + {{.Payload}} + + +`)) + +var iosTemplate = template.Must(template.New("iosTemplate").Parse(` + + PayloadType + io.tailscale.ipn.ios + PayloadUUID + {{.UUID}} + PayloadIdentifier + com.github.juanfont.headscale + PayloadVersion + 1 + PayloadEnabled + + + ControlURL + {{.Url}} + +`)) + +var macosTemplate = template.Must(template.New("macosTemplate").Parse(` + + PayloadType + io.tailscale.ipn.macos + PayloadUUID + {{.UUID}} + PayloadIdentifier + com.github.juanfont.headscale + PayloadVersion + 1 + PayloadEnabled + + + ControlURL + {{.Url}} + +`)) diff --git a/go.mod b/go.mod index 031460e8..a6d10477 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/docker/cli v20.10.8+incompatible // indirect github.com/docker/docker v20.10.8+incompatible // indirect github.com/efekarakus/termcolor v1.0.1 + github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gin-gonic/gin v1.7.4 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.5