Merge pull request #113 from kradalby/apple-mobileconfig

Apple macOS profile support
This commit is contained in:
Kristoffer Dalby 2021-09-26 21:34:11 +01:00 committed by GitHub
commit 0bbf343348
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 330 additions and 88 deletions

189
README.md
View file

@ -22,21 +22,18 @@ Headscale implements this coordination server.
- [x] Namespace support (~equivalent to multi-user in Tailscale.com) - [x] Namespace support (~equivalent to multi-user in Tailscale.com)
- [x] Routing (advertise & accept, including exit nodes) - [x] Routing (advertise & accept, including exit nodes)
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support) - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
- [X] JSON-formatted output - [x] JSON-formatted output
- [X] ACLs - [x] ACLs
- [X] Taildrop (File Sharing) - [x] Taildrop (File Sharing)
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [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] DNS (passing DNS servers to nodes)
- [X] Share nodes between ~~users~~ namespaces - [x] Share nodes between ~~users~~ namespaces
- [ ] MagicDNS / Smart DNS - [ ] MagicDNS / Smart DNS
## Roadmap 🤷 ## Roadmap 🤷
Suggestions/PRs welcomed! Suggestions/PRs welcomed!
## Running it ## 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 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 ```shell
docker pull headscale/headscale:x.x.x docker pull headscale/headscale:x.x.x
``` ```
<!--
or <!--
```shell or
docker pull ghrc.io/juanfont/headscale:x.x.x ```shell
``` --> docker pull ghrc.io/juanfont/headscale:x.x.x
``` -->
2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running 2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running
```shell ```shell
docker run --name headscale -e POSTGRES_DB=headscale -e \ docker run --name headscale -e POSTGRES_DB=headscale -e \
POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -p 5432:5432 -d postgres
``` ```
3. Set some stuff up (headscale Wireguard keys & the config.json file) 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 ```shell
cp config.json.postgres.example config.json wg genkey > private.key
# or wg pubkey < private.key > public.key # not needed
# SQLite
cp config.json.sqlite.example config.json # 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) 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
```shell headscale namespaces create myfirstnamespace
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 docker:
or if your server is already running in docker:
```shell the db.sqlite mount is only needed if you use sqlite
docker exec <container_name> headscale create myfirstnamespace
``` ```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 <container_name> headscale create myfirstnamespace
```
5. Run the server 5. Run the server
```shell
headscale serve
```
or docker:
the db.sqlite mount is only needed if you use sqlite ```shell
```shell headscale serve
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 ```
```
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 6. If you used tailscale.com before in your nodes, make sure you clear the tailscald data folder
```shell
systemctl stop tailscaled ```shell
rm -fr /var/lib/tailscale systemctl stop tailscaled
systemctl start tailscaled rm -fr /var/lib/tailscale
``` systemctl start tailscaled
```
7. Add your first machine 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. 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 9. In the server, register your machine to a namespace with the CLI
```shell ```shell
headscale -n myfirstnamespace node register YOURMACHINEKEY headscale -n myfirstnamespace node register YOURMACHINEKEY
``` ```
or docker: or docker:
```shell ```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 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: or if your server is already running in docker:
```shell ```shell
docker exec <container_name> headscale -n myfistnamespace node register YOURMACHINEKEY docker exec <container_name> headscale -n myfistnamespace node register YOURMACHINEKEY
``` ```
Alternatively, you can use Auth Keys to register your machines: Alternatively, you can use Auth Keys to register your machines:
1. Create an authkey 1. Create an authkey
```shell
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 docker:
```
or if your server is already running in docker: ```shell
```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
docker exec <container_name> headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h ```
```
or if your server is already running in docker:
```shell
docker exec <container_name> headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
```
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.
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 ## Configuration reference
@ -163,6 +176,7 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal
``` ```
"log_level": "debug" "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`. `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. The fields starting with `db_` are used for the PostgreSQL connection information.
### Running the service via TLS (optional) ### 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. 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 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. 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 ## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc. 1. We have nothing to do with Tailscale, or Tailscale Inc.
2. The purpose of writing this was to learn how Tailscale works. 2. The purpose of writing this was to learn how Tailscale works.
## More on Tailscale ## More on Tailscale
- https://tailscale.com/blog/how-tailscale-works/ - https://tailscale.com/blog/how-tailscale-works/
- https://tailscale.com/blog/tailscale-key-management/ - https://tailscale.com/blog/tailscale-key-management/
- https://tailscale.com/blog/an-unlikely-database-migration/ - https://tailscale.com/blog/an-unlikely-database-migration/

2
app.go
View file

@ -168,6 +168,8 @@ func (h *Headscale) Serve() error {
r.GET("/register", h.RegisterWebAPI) r.GET("/register", h.RegisterWebAPI)
r.POST("/machine/:id/map", h.PollNetMapHandler) r.POST("/machine/:id/map", h.PollNetMapHandler)
r.POST("/machine/:id", h.RegistrationHandler) r.POST("/machine/:id", h.RegistrationHandler)
r.GET("/apple", h.AppleMobileConfig)
r.GET("/apple/:platform", h.ApplePlatformConfig)
var err error var err error
timeout := 30 * time.Second timeout := 30 * time.Second

226
apple_mobileconfig.go Normal file
View file

@ -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(`
<html>
<body>
<h1>Apple configuration profiles</h1>
<p>
This page provides <a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web">configuration profiles</a> for the official Tailscale clients for <a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1">iOS</a> and <a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12">macOS</a>.
</p>
<p>
The profiles will configure Tailscale.app to use {{.Url}} as its control server.
</p>
<h3>Caution</h3>
<p>You should always inspect the profile before installing it:</p>
<!--
<p><code>curl {{.Url}}/apple/ios</code></p>
-->
<p><code>curl {{.Url}}/apple/macos</code></p>
<h2>Profiles</h2>
<!--
<h3>iOS</h3>
<p>
<a href="/apple/ios" download="headscale_ios.mobileconfig">iOS profile</a>
</p>
-->
<h3>macOS</h3>
<p>Headscale can be set to the default server by installing a Headscale configuration profile:</p>
<p>
<a href="/apple/macos" download="headscale_macos.mobileconfig">macOS profile</a>
</p>
<ol>
<li>Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed</li>
<li>Open System Preferences and go to "Profiles"</li>
<li>Find and install the Headscale profile</li>
<li>Restart Tailscale.app and log in</li>
</ol>
<p>Or</p>
<p>Use your terminal to configure the default setting for Tailscale by issuing:</p>
<code>defaults write io.tailscale.ipn.macos ControlURL {{.Url}}</code>
<p>Restart Tailscale.app and log in.</p>
</body>
</html>`))
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(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadDisplayName</key>
<string>Headscale</string>
<key>PayloadDescription</key>
<string>Configure Tailscale login server to: {{.Url}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<array>
{{.Payload}}
</array>
</dict>
</plist>`))
var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.ios</string>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.Url}}</string>
</dict>
`))
var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
<dict>
<key>PayloadType</key>
<string>io.tailscale.ipn.macos</string>
<key>PayloadUUID</key>
<string>{{.UUID}}</string>
<key>PayloadIdentifier</key>
<string>com.github.juanfont.headscale</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadEnabled</key>
<true/>
<key>ControlURL</key>
<string>{{.Url}}</string>
</dict>
`))

1
go.mod
View file

@ -10,6 +10,7 @@ require (
github.com/docker/cli v20.10.8+incompatible // indirect github.com/docker/cli v20.10.8+incompatible // indirect
github.com/docker/docker v20.10.8+incompatible // indirect github.com/docker/docker v20.10.8+incompatible // indirect
github.com/efekarakus/termcolor v1.0.1 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/gin-gonic/gin v1.7.4
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/klauspost/compress v1.13.5 github.com/klauspost/compress v1.13.5