From 40c52639276818d0b03643cb7be0de01ecc2774b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 19 Sep 2021 17:54:41 +0100 Subject: [PATCH 1/6] Add initial code for generating Apple profiles This code adds new http handlers that will generate iOS and macOS configuration profiles allowing us to override the Control server of the official Tailscale.app. Currently, macOS is working, as I have not found the correct "key" to inject for iOS. This means that a profile will allow users to no longer log in via the command line, but they can use the app. --- apple_mobileconfig.go | 179 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 3 files changed, 182 insertions(+) create mode 100644 apple_mobileconfig.go diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go new file mode 100644 index 0000000..cfaffa0 --- /dev/null +++ b/apple_mobileconfig.go @@ -0,0 +1,179 @@ +package headscale + +import ( + "bytes" + "net/http" + "text/template" + + "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/ios

+

curl {{.Url}}/apple/macos

+ +

Profiles

+

+ iOS +

+ +

+ macOS +

+ + +`)) + + config := map[string]interface{}{ + "Url": h.cfg.ServerURL, + } + + var payload bytes.Buffer + if err := t.Execute(&payload, config); err != nil { + c.Error(err) + 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 { + c.Error(err) + return + } + + contentId, err := uuid.NewV4() + if err != nil { + c.Error(err) + 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 { + c.Error(err) + return + } + case "ios": + if err := iosTemplate.Execute(&payload, platformConfig); err != nil { + c.Error(err) + 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 { + c.Error(err) + 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 0d8c86b..88c314f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.0.5 github.com/gin-gonic/gin v1.7.2 + github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.1 github.com/lib/pq v1.10.2 // indirect diff --git a/go.sum b/go.sum index 4751eaa..88cb077 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= From dfcab2b6d59a3349dcf9cfb9a50a70c3fd9ff9f6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 19 Sep 2021 17:56:29 +0100 Subject: [PATCH 2/6] Wire up new handlers --- app.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.go b/app.go index fa9b011..c4411a6 100644 --- a/app.go +++ b/app.go @@ -146,6 +146,8 @@ func (h *Headscale) Serve() error { r.GET("/register", h.RegisterWebAPI) r.POST("/machine/:id/map", h.PollNetMapHandler) r.POST("/machine/:id", h.RegistrationHandler) + r.GET("/apple", h.AppleMobileConfig) + r.GET("/apple/:platform", h.ApplePlatformConfig) var err error if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { From b3efd1e47b168c143ed13de1fe9465567685ca40 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 20 Sep 2021 07:54:18 +0100 Subject: [PATCH 3/6] Handle errors --- apple_mobileconfig.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go index cfaffa0..85df81b 100644 --- a/apple_mobileconfig.go +++ b/apple_mobileconfig.go @@ -5,6 +5,8 @@ import ( "net/http" "text/template" + "github.com/rs/zerolog/log" + "github.com/gin-gonic/gin" "github.com/gofrs/uuid" ) @@ -46,7 +48,11 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) { var payload bytes.Buffer if err := t.Execute(&payload, config); err != nil { - c.Error(err) + 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 } @@ -58,13 +64,21 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) { id, err := uuid.NewV4() if err != nil { - c.Error(err) + 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 { - c.Error(err) + 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 } @@ -78,12 +92,20 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) { switch platform { case "macos": if err := macosTemplate.Execute(&payload, platformConfig); err != nil { - c.Error(err) + 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 { - c.Error(err) + 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: @@ -99,7 +121,11 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) { var content bytes.Buffer if err := commonTemplate.Execute(&content, config); err != nil { - c.Error(err) + 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 } From 8e588ae146d75a0b8c61da60090c0d8c875fa03e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 23 Sep 2021 20:22:07 +0100 Subject: [PATCH 4/6] Add a more comprehensive macOS explaination --- apple_mobileconfig.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go index 85df81b..9277713 100644 --- a/apple_mobileconfig.go +++ b/apple_mobileconfig.go @@ -30,14 +30,31 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {

curl {{.Url}}/apple/ios

curl {{.Url}}/apple/macos

-

Profiles

+

Profiles

+ +

iOS

- iOS + iOS profile

+

macOS

+

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

- macOS + 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.

`)) From 59c3d4bcfebec46e54aec8b37d807153b3d830dd Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 26 Sep 2021 20:41:48 +0100 Subject: [PATCH 5/6] Comment out iOS from /apple for now --- apple_mobileconfig.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go index 9277713..f3956e3 100644 --- a/apple_mobileconfig.go +++ b/apple_mobileconfig.go @@ -27,15 +27,19 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {

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:

From 237a14858aca491a8409da05de0b211eaba4770e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 26 Sep 2021 20:47:39 +0100 Subject: [PATCH 6/6] Add apple endpoint to readme --- README.md | 148 +++++++++++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 1a60db1..4d7c83a 100644 --- a/README.md +++ b/README.md @@ -22,28 +22,26 @@ 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] 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] 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 - ```shell - docker pull headscale/headscale:x.x.x - ``` -